sqlmesh 0.217.1.dev1__py3-none-any.whl → 0.227.2.dev20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlmesh/__init__.py +12 -2
- sqlmesh/_version.py +2 -2
- sqlmesh/cli/project_init.py +10 -2
- sqlmesh/core/_typing.py +1 -0
- sqlmesh/core/audit/definition.py +8 -2
- sqlmesh/core/config/__init__.py +1 -1
- sqlmesh/core/config/connection.py +20 -5
- sqlmesh/core/config/dbt.py +13 -0
- sqlmesh/core/config/janitor.py +12 -0
- sqlmesh/core/config/loader.py +7 -0
- sqlmesh/core/config/model.py +2 -0
- sqlmesh/core/config/root.py +3 -0
- sqlmesh/core/console.py +80 -2
- sqlmesh/core/constants.py +1 -1
- sqlmesh/core/context.py +112 -35
- sqlmesh/core/dialect.py +3 -0
- sqlmesh/core/engine_adapter/_typing.py +2 -0
- sqlmesh/core/engine_adapter/base.py +330 -23
- sqlmesh/core/engine_adapter/base_postgres.py +17 -1
- sqlmesh/core/engine_adapter/bigquery.py +146 -7
- sqlmesh/core/engine_adapter/clickhouse.py +17 -13
- sqlmesh/core/engine_adapter/databricks.py +50 -2
- sqlmesh/core/engine_adapter/fabric.py +110 -29
- sqlmesh/core/engine_adapter/mixins.py +142 -48
- sqlmesh/core/engine_adapter/mssql.py +15 -4
- sqlmesh/core/engine_adapter/mysql.py +2 -2
- sqlmesh/core/engine_adapter/postgres.py +9 -3
- sqlmesh/core/engine_adapter/redshift.py +4 -0
- sqlmesh/core/engine_adapter/risingwave.py +1 -0
- sqlmesh/core/engine_adapter/shared.py +6 -0
- sqlmesh/core/engine_adapter/snowflake.py +82 -11
- sqlmesh/core/engine_adapter/spark.py +14 -10
- sqlmesh/core/engine_adapter/trino.py +5 -2
- sqlmesh/core/janitor.py +181 -0
- sqlmesh/core/lineage.py +1 -0
- sqlmesh/core/linter/rules/builtin.py +15 -0
- sqlmesh/core/loader.py +17 -30
- sqlmesh/core/macros.py +35 -13
- sqlmesh/core/model/common.py +2 -0
- sqlmesh/core/model/definition.py +72 -4
- sqlmesh/core/model/kind.py +66 -2
- sqlmesh/core/model/meta.py +107 -2
- sqlmesh/core/node.py +101 -2
- sqlmesh/core/plan/builder.py +15 -10
- sqlmesh/core/plan/common.py +196 -2
- sqlmesh/core/plan/definition.py +21 -6
- sqlmesh/core/plan/evaluator.py +72 -113
- sqlmesh/core/plan/explainer.py +90 -8
- sqlmesh/core/plan/stages.py +42 -21
- sqlmesh/core/renderer.py +26 -18
- sqlmesh/core/scheduler.py +60 -19
- sqlmesh/core/selector.py +137 -9
- sqlmesh/core/signal.py +64 -1
- sqlmesh/core/snapshot/__init__.py +1 -0
- sqlmesh/core/snapshot/definition.py +109 -25
- sqlmesh/core/snapshot/evaluator.py +610 -50
- sqlmesh/core/state_sync/__init__.py +0 -1
- sqlmesh/core/state_sync/base.py +31 -27
- sqlmesh/core/state_sync/cache.py +12 -4
- sqlmesh/core/state_sync/common.py +216 -111
- sqlmesh/core/state_sync/db/facade.py +30 -15
- sqlmesh/core/state_sync/db/interval.py +27 -7
- sqlmesh/core/state_sync/db/migrator.py +14 -8
- sqlmesh/core/state_sync/db/snapshot.py +119 -87
- sqlmesh/core/table_diff.py +2 -2
- sqlmesh/core/test/definition.py +14 -9
- sqlmesh/core/test/discovery.py +4 -0
- sqlmesh/dbt/adapter.py +20 -11
- sqlmesh/dbt/basemodel.py +52 -41
- sqlmesh/dbt/builtin.py +27 -11
- sqlmesh/dbt/column.py +17 -5
- sqlmesh/dbt/common.py +4 -2
- sqlmesh/dbt/context.py +14 -1
- sqlmesh/dbt/loader.py +60 -8
- sqlmesh/dbt/manifest.py +136 -8
- sqlmesh/dbt/model.py +105 -25
- sqlmesh/dbt/package.py +16 -1
- sqlmesh/dbt/profile.py +3 -3
- sqlmesh/dbt/project.py +12 -7
- sqlmesh/dbt/seed.py +1 -1
- sqlmesh/dbt/source.py +6 -1
- sqlmesh/dbt/target.py +25 -6
- sqlmesh/dbt/test.py +31 -1
- sqlmesh/integrations/github/cicd/controller.py +6 -2
- sqlmesh/lsp/context.py +4 -2
- sqlmesh/magics.py +1 -1
- sqlmesh/migrations/v0000_baseline.py +3 -6
- sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +2 -5
- sqlmesh/migrations/v0062_add_model_gateway.py +2 -2
- sqlmesh/migrations/v0063_change_signals.py +2 -4
- sqlmesh/migrations/v0064_join_when_matched_strings.py +2 -4
- sqlmesh/migrations/v0065_add_model_optimize.py +2 -2
- sqlmesh/migrations/v0066_add_auto_restatements.py +2 -6
- sqlmesh/migrations/v0067_add_tsql_date_full_precision.py +2 -2
- sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py +2 -2
- sqlmesh/migrations/v0069_update_dev_table_suffix.py +2 -4
- sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py +2 -2
- sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +2 -6
- sqlmesh/migrations/v0072_add_environment_statements.py +2 -4
- sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +2 -4
- sqlmesh/migrations/v0074_add_partition_by_time_column_property.py +2 -2
- sqlmesh/migrations/v0075_remove_validate_query.py +2 -4
- sqlmesh/migrations/v0076_add_cron_tz.py +2 -2
- sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py +2 -2
- sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py +2 -4
- sqlmesh/migrations/v0079_add_gateway_managed_property.py +7 -9
- sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py +2 -2
- sqlmesh/migrations/v0081_update_partitioned_by.py +2 -4
- sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py +2 -4
- sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py +2 -2
- sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py +2 -2
- sqlmesh/migrations/v0085_deterministic_repr.py +2 -4
- sqlmesh/migrations/v0086_check_deterministic_bug.py +2 -4
- sqlmesh/migrations/v0087_normalize_blueprint_variables.py +2 -4
- sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +2 -4
- sqlmesh/migrations/v0089_add_virtual_environment_mode.py +2 -2
- sqlmesh/migrations/v0090_add_forward_only_column.py +2 -6
- sqlmesh/migrations/v0091_on_additive_change.py +2 -2
- sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py +2 -4
- sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py +2 -2
- sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py +2 -6
- sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py +2 -4
- sqlmesh/migrations/v0096_remove_plan_dags_table.py +2 -4
- sqlmesh/migrations/v0097_add_dbt_name_in_node.py +2 -2
- sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py +103 -0
- sqlmesh/migrations/v0099_add_last_altered_to_intervals.py +25 -0
- sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py +9 -0
- sqlmesh/utils/__init__.py +8 -1
- sqlmesh/utils/cache.py +5 -1
- sqlmesh/utils/date.py +1 -1
- sqlmesh/utils/errors.py +4 -0
- sqlmesh/utils/git.py +3 -1
- sqlmesh/utils/jinja.py +25 -2
- sqlmesh/utils/pydantic.py +6 -6
- sqlmesh/utils/windows.py +13 -3
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/METADATA +5 -5
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/RECORD +188 -183
- sqlmesh_dbt/cli.py +70 -7
- sqlmesh_dbt/console.py +14 -6
- sqlmesh_dbt/operations.py +103 -24
- sqlmesh_dbt/selectors.py +39 -1
- web/client/dist/assets/{Audits-Ucsx1GzF.js → Audits-CBiYyyx-.js} +1 -1
- web/client/dist/assets/{Banner-BWDzvavM.js → Banner-DSRbUlO5.js} +1 -1
- web/client/dist/assets/{ChevronDownIcon-D2VL13Ah.js → ChevronDownIcon-MK_nrjD_.js} +1 -1
- web/client/dist/assets/{ChevronRightIcon-DWGYbf1l.js → ChevronRightIcon-CLWtT22Q.js} +1 -1
- web/client/dist/assets/{Content-DdHDZM3I.js → Content-BNuGZN5l.js} +1 -1
- web/client/dist/assets/{Content-Bikfy8fh.js → Content-CSHJyW0n.js} +1 -1
- web/client/dist/assets/{Data-CzAJH7rW.js → Data-C1oRDbLx.js} +1 -1
- web/client/dist/assets/{DataCatalog-BJF11g8f.js → DataCatalog-HXyX2-_j.js} +1 -1
- web/client/dist/assets/{Editor-s0SBpV2y.js → Editor-BDyfpUuw.js} +1 -1
- web/client/dist/assets/{Editor-DgLhgKnm.js → Editor-D0jNItwC.js} +1 -1
- web/client/dist/assets/{Errors-D0m0O1d3.js → Errors-BfuFLcPi.js} +1 -1
- web/client/dist/assets/{FileExplorer-CEv0vXkt.js → FileExplorer-BR9IE3he.js} +1 -1
- web/client/dist/assets/{Footer-BwzXn8Ew.js → Footer-CgBEtiAh.js} +1 -1
- web/client/dist/assets/{Header-6heDkEqG.js → Header-DSqR6nSO.js} +1 -1
- web/client/dist/assets/{Input-obuJsD6k.js → Input-B-oZ6fGO.js} +1 -1
- web/client/dist/assets/Lineage-DYQVwDbD.js +1 -0
- web/client/dist/assets/{ListboxShow-HM9_qyrt.js → ListboxShow-BE5-xevs.js} +1 -1
- web/client/dist/assets/{ModelLineage-zWdKo0U2.js → ModelLineage-DkIFAYo4.js} +1 -1
- web/client/dist/assets/{Models-Bcu66SRz.js → Models-D5dWr8RB.js} +1 -1
- web/client/dist/assets/{Page-BWEEQfIt.js → Page-C-XfU5BR.js} +1 -1
- web/client/dist/assets/{Plan-C4gXCqlf.js → Plan-ZEuTINBq.js} +1 -1
- web/client/dist/assets/{PlusCircleIcon-CVDO651q.js → PlusCircleIcon-DVXAHG8_.js} +1 -1
- web/client/dist/assets/{ReportErrors-BT6xFwAr.js → ReportErrors-B7FEPzMB.js} +1 -1
- web/client/dist/assets/{Root-ryJoBK4h.js → Root-8aZyhPxF.js} +1 -1
- web/client/dist/assets/{SearchList-DB04sPb9.js → SearchList-W_iT2G82.js} +1 -1
- web/client/dist/assets/{SelectEnvironment-CUYcXUu6.js → SelectEnvironment-C65jALmO.js} +1 -1
- web/client/dist/assets/{SourceList-Doo_9ZGp.js → SourceList-DSLO6nVJ.js} +1 -1
- web/client/dist/assets/{SourceListItem-D5Mj7Dly.js → SourceListItem-BHt8d9-I.js} +1 -1
- web/client/dist/assets/{SplitPane-qHmkD1qy.js → SplitPane-CViaZmw6.js} +1 -1
- web/client/dist/assets/{Tests-DH1Z74ML.js → Tests-DhaVt5t1.js} +1 -1
- web/client/dist/assets/{Welcome-DqUJUNMF.js → Welcome-DvpjH-_4.js} +1 -1
- web/client/dist/assets/context-BctCsyGb.js +71 -0
- web/client/dist/assets/{context-Dr54UHLi.js → context-DFNeGsFF.js} +1 -1
- web/client/dist/assets/{editor-DYIP1yQ4.js → editor-CcO28cqd.js} +1 -1
- web/client/dist/assets/{file-DarlIDVi.js → file-CvJN3aZO.js} +1 -1
- web/client/dist/assets/{floating-ui.react-dom-BH3TFvkM.js → floating-ui.react-dom-CjE-JNW1.js} +1 -1
- web/client/dist/assets/{help-Bl8wqaQc.js → help-DuPhjipa.js} +1 -1
- web/client/dist/assets/{index-D1sR7wpN.js → index-C-dJH7yZ.js} +1 -1
- web/client/dist/assets/{index-O3mjYpnE.js → index-Dj0i1-CA.js} +2 -2
- web/client/dist/assets/{plan-CehRrJUG.js → plan-BTRSbjKn.js} +1 -1
- web/client/dist/assets/{popover-CqgMRE0G.js → popover-_Sf0yvOI.js} +1 -1
- web/client/dist/assets/{project-6gxepOhm.js → project-BvSOI8MY.js} +1 -1
- web/client/dist/index.html +1 -1
- web/client/dist/assets/Lineage-D0Hgdz2v.js +0 -1
- web/client/dist/assets/context-DgX0fp2E.js +0 -68
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/WHEEL +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/top_level.txt +0 -0
|
@@ -7,9 +7,10 @@ from dataclasses import dataclass
|
|
|
7
7
|
|
|
8
8
|
from sqlglot import exp, parse_one
|
|
9
9
|
from sqlglot.helper import seq_get
|
|
10
|
+
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
|
|
10
11
|
|
|
11
12
|
from sqlmesh.core.engine_adapter.base import EngineAdapter
|
|
12
|
-
from sqlmesh.core.engine_adapter.shared import
|
|
13
|
+
from sqlmesh.core.engine_adapter.shared import DataObjectType
|
|
13
14
|
from sqlmesh.core.node import IntervalUnit
|
|
14
15
|
from sqlmesh.core.dialect import schema_
|
|
15
16
|
from sqlmesh.core.schema_diff import TableAlterOperation
|
|
@@ -17,7 +18,12 @@ from sqlmesh.utils.errors import SQLMeshError
|
|
|
17
18
|
|
|
18
19
|
if t.TYPE_CHECKING:
|
|
19
20
|
from sqlmesh.core._typing import TableName
|
|
20
|
-
from sqlmesh.core.engine_adapter._typing import
|
|
21
|
+
from sqlmesh.core.engine_adapter._typing import (
|
|
22
|
+
DCL,
|
|
23
|
+
DF,
|
|
24
|
+
GrantsConfig,
|
|
25
|
+
QueryOrDF,
|
|
26
|
+
)
|
|
21
27
|
from sqlmesh.core.engine_adapter.base import QueryOrDF
|
|
22
28
|
|
|
23
29
|
logger = logging.getLogger(__name__)
|
|
@@ -75,52 +81,6 @@ class PandasNativeFetchDFSupportMixin(EngineAdapter):
|
|
|
75
81
|
return df
|
|
76
82
|
|
|
77
83
|
|
|
78
|
-
class InsertOverwriteWithMergeMixin(EngineAdapter):
|
|
79
|
-
def _insert_overwrite_by_condition(
|
|
80
|
-
self,
|
|
81
|
-
table_name: TableName,
|
|
82
|
-
source_queries: t.List[SourceQuery],
|
|
83
|
-
target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
|
|
84
|
-
where: t.Optional[exp.Condition] = None,
|
|
85
|
-
insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None,
|
|
86
|
-
**kwargs: t.Any,
|
|
87
|
-
) -> None:
|
|
88
|
-
"""
|
|
89
|
-
Some engines do not support `INSERT OVERWRITE` but instead support
|
|
90
|
-
doing an "INSERT OVERWRITE" using a Merge expression but with the
|
|
91
|
-
predicate being `False`.
|
|
92
|
-
"""
|
|
93
|
-
target_columns_to_types = target_columns_to_types or self.columns(table_name)
|
|
94
|
-
for source_query in source_queries:
|
|
95
|
-
with source_query as query:
|
|
96
|
-
query = self._order_projections_and_filter(
|
|
97
|
-
query, target_columns_to_types, where=where
|
|
98
|
-
)
|
|
99
|
-
columns = [exp.column(col) for col in target_columns_to_types]
|
|
100
|
-
when_not_matched_by_source = exp.When(
|
|
101
|
-
matched=False,
|
|
102
|
-
source=True,
|
|
103
|
-
condition=where,
|
|
104
|
-
then=exp.Delete(),
|
|
105
|
-
)
|
|
106
|
-
when_not_matched_by_target = exp.When(
|
|
107
|
-
matched=False,
|
|
108
|
-
source=False,
|
|
109
|
-
then=exp.Insert(
|
|
110
|
-
this=exp.Tuple(expressions=columns),
|
|
111
|
-
expression=exp.Tuple(expressions=columns),
|
|
112
|
-
),
|
|
113
|
-
)
|
|
114
|
-
self._merge(
|
|
115
|
-
target_table=table_name,
|
|
116
|
-
query=query,
|
|
117
|
-
on=exp.false(),
|
|
118
|
-
whens=exp.Whens(
|
|
119
|
-
expressions=[when_not_matched_by_source, when_not_matched_by_target]
|
|
120
|
-
),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
84
|
class HiveMetastoreTablePropertiesMixin(EngineAdapter):
|
|
125
85
|
MAX_TABLE_COMMENT_LENGTH = 4000
|
|
126
86
|
MAX_COLUMN_COMMENT_LENGTH = 4000
|
|
@@ -595,3 +555,137 @@ class RowDiffMixin(EngineAdapter):
|
|
|
595
555
|
|
|
596
556
|
def _normalize_boolean_value(self, expr: exp.Expression) -> exp.Expression:
|
|
597
557
|
return exp.cast(expr, "INT")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class GrantsFromInfoSchemaMixin(EngineAdapter):
|
|
561
|
+
CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.func("current_user")
|
|
562
|
+
SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = False
|
|
563
|
+
USE_CATALOG_IN_GRANTS = False
|
|
564
|
+
GRANT_INFORMATION_SCHEMA_TABLE_NAME = "table_privileges"
|
|
565
|
+
|
|
566
|
+
@staticmethod
|
|
567
|
+
@abc.abstractmethod
|
|
568
|
+
def _grant_object_kind(table_type: DataObjectType) -> t.Optional[str]:
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
@abc.abstractmethod
|
|
572
|
+
def _get_current_schema(self) -> str:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
def _dcl_grants_config_expr(
|
|
576
|
+
self,
|
|
577
|
+
dcl_cmd: t.Type[DCL],
|
|
578
|
+
table: exp.Table,
|
|
579
|
+
grants_config: GrantsConfig,
|
|
580
|
+
table_type: DataObjectType = DataObjectType.TABLE,
|
|
581
|
+
) -> t.List[exp.Expression]:
|
|
582
|
+
expressions: t.List[exp.Expression] = []
|
|
583
|
+
if not grants_config:
|
|
584
|
+
return expressions
|
|
585
|
+
|
|
586
|
+
object_kind = self._grant_object_kind(table_type)
|
|
587
|
+
for privilege, principals in grants_config.items():
|
|
588
|
+
args: t.Dict[str, t.Any] = {
|
|
589
|
+
"privileges": [exp.GrantPrivilege(this=exp.Var(this=privilege))],
|
|
590
|
+
"securable": table.copy(),
|
|
591
|
+
}
|
|
592
|
+
if object_kind:
|
|
593
|
+
args["kind"] = exp.Var(this=object_kind)
|
|
594
|
+
if self.SUPPORTS_MULTIPLE_GRANT_PRINCIPALS:
|
|
595
|
+
args["principals"] = [
|
|
596
|
+
normalize_identifiers(
|
|
597
|
+
parse_one(principal, into=exp.GrantPrincipal, dialect=self.dialect),
|
|
598
|
+
dialect=self.dialect,
|
|
599
|
+
)
|
|
600
|
+
for principal in principals
|
|
601
|
+
]
|
|
602
|
+
expressions.append(dcl_cmd(**args)) # type: ignore[arg-type]
|
|
603
|
+
else:
|
|
604
|
+
for principal in principals:
|
|
605
|
+
args["principals"] = [
|
|
606
|
+
normalize_identifiers(
|
|
607
|
+
parse_one(principal, into=exp.GrantPrincipal, dialect=self.dialect),
|
|
608
|
+
dialect=self.dialect,
|
|
609
|
+
)
|
|
610
|
+
]
|
|
611
|
+
expressions.append(dcl_cmd(**args)) # type: ignore[arg-type]
|
|
612
|
+
|
|
613
|
+
return expressions
|
|
614
|
+
|
|
615
|
+
def _apply_grants_config_expr(
|
|
616
|
+
self,
|
|
617
|
+
table: exp.Table,
|
|
618
|
+
grants_config: GrantsConfig,
|
|
619
|
+
table_type: DataObjectType = DataObjectType.TABLE,
|
|
620
|
+
) -> t.List[exp.Expression]:
|
|
621
|
+
return self._dcl_grants_config_expr(exp.Grant, table, grants_config, table_type)
|
|
622
|
+
|
|
623
|
+
def _revoke_grants_config_expr(
|
|
624
|
+
self,
|
|
625
|
+
table: exp.Table,
|
|
626
|
+
grants_config: GrantsConfig,
|
|
627
|
+
table_type: DataObjectType = DataObjectType.TABLE,
|
|
628
|
+
) -> t.List[exp.Expression]:
|
|
629
|
+
return self._dcl_grants_config_expr(exp.Revoke, table, grants_config, table_type)
|
|
630
|
+
|
|
631
|
+
def _get_grant_expression(self, table: exp.Table) -> exp.Expression:
|
|
632
|
+
schema_identifier = table.args.get("db") or normalize_identifiers(
|
|
633
|
+
exp.to_identifier(self._get_current_schema(), quoted=True), dialect=self.dialect
|
|
634
|
+
)
|
|
635
|
+
schema_name = schema_identifier.this
|
|
636
|
+
table_name = table.args.get("this").this # type: ignore
|
|
637
|
+
|
|
638
|
+
grant_conditions = [
|
|
639
|
+
exp.column("table_schema").eq(exp.Literal.string(schema_name)),
|
|
640
|
+
exp.column("table_name").eq(exp.Literal.string(table_name)),
|
|
641
|
+
exp.column("grantor").eq(self.CURRENT_USER_OR_ROLE_EXPRESSION),
|
|
642
|
+
exp.column("grantee").neq(self.CURRENT_USER_OR_ROLE_EXPRESSION),
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
info_schema_table = normalize_identifiers(
|
|
646
|
+
exp.table_(self.GRANT_INFORMATION_SCHEMA_TABLE_NAME, db="information_schema"),
|
|
647
|
+
dialect=self.dialect,
|
|
648
|
+
)
|
|
649
|
+
if self.USE_CATALOG_IN_GRANTS:
|
|
650
|
+
catalog_identifier = table.args.get("catalog")
|
|
651
|
+
if not catalog_identifier:
|
|
652
|
+
catalog_name = self.get_current_catalog()
|
|
653
|
+
if not catalog_name:
|
|
654
|
+
raise SQLMeshError(
|
|
655
|
+
"Current catalog could not be determined for fetching grants. This is unexpected."
|
|
656
|
+
)
|
|
657
|
+
catalog_identifier = normalize_identifiers(
|
|
658
|
+
exp.to_identifier(catalog_name, quoted=True), dialect=self.dialect
|
|
659
|
+
)
|
|
660
|
+
catalog_name = catalog_identifier.this
|
|
661
|
+
info_schema_table.set("catalog", catalog_identifier.copy())
|
|
662
|
+
grant_conditions.insert(
|
|
663
|
+
0, exp.column("table_catalog").eq(exp.Literal.string(catalog_name))
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
exp.select("privilege_type", "grantee")
|
|
668
|
+
.from_(info_schema_table)
|
|
669
|
+
.where(exp.and_(*grant_conditions))
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig:
|
|
673
|
+
grant_expr = self._get_grant_expression(table)
|
|
674
|
+
|
|
675
|
+
results = self.fetchall(grant_expr)
|
|
676
|
+
|
|
677
|
+
grants_dict: GrantsConfig = {}
|
|
678
|
+
for privilege_raw, grantee_raw in results:
|
|
679
|
+
if privilege_raw is None or grantee_raw is None:
|
|
680
|
+
continue
|
|
681
|
+
|
|
682
|
+
privilege = str(privilege_raw)
|
|
683
|
+
grantee = str(grantee_raw)
|
|
684
|
+
if not privilege or not grantee:
|
|
685
|
+
continue
|
|
686
|
+
|
|
687
|
+
grantees = grants_dict.setdefault(privilege, [])
|
|
688
|
+
if grantee not in grantees:
|
|
689
|
+
grantees.append(grantee)
|
|
690
|
+
|
|
691
|
+
return grants_dict
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import typing as t
|
|
6
|
+
import logging
|
|
6
7
|
|
|
7
8
|
from sqlglot import exp
|
|
8
9
|
|
|
@@ -13,10 +14,10 @@ from sqlmesh.core.engine_adapter.base import (
|
|
|
13
14
|
InsertOverwriteStrategy,
|
|
14
15
|
MERGE_SOURCE_ALIAS,
|
|
15
16
|
MERGE_TARGET_ALIAS,
|
|
17
|
+
_get_data_object_cache_key,
|
|
16
18
|
)
|
|
17
19
|
from sqlmesh.core.engine_adapter.mixins import (
|
|
18
20
|
GetCurrentCatalogFromFunctionMixin,
|
|
19
|
-
InsertOverwriteWithMergeMixin,
|
|
20
21
|
PandasNativeFetchDFSupportMixin,
|
|
21
22
|
VarcharSizeWorkaroundMixin,
|
|
22
23
|
RowDiffMixin,
|
|
@@ -37,11 +38,13 @@ if t.TYPE_CHECKING:
|
|
|
37
38
|
from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
40
44
|
@set_catalog()
|
|
41
45
|
class MSSQLEngineAdapter(
|
|
42
46
|
EngineAdapterWithIndexSupport,
|
|
43
47
|
PandasNativeFetchDFSupportMixin,
|
|
44
|
-
InsertOverwriteWithMergeMixin,
|
|
45
48
|
GetCurrentCatalogFromFunctionMixin,
|
|
46
49
|
VarcharSizeWorkaroundMixin,
|
|
47
50
|
RowDiffMixin,
|
|
@@ -53,6 +56,7 @@ class MSSQLEngineAdapter(
|
|
|
53
56
|
COMMENT_CREATION_TABLE = CommentCreationTable.UNSUPPORTED
|
|
54
57
|
COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED
|
|
55
58
|
SUPPORTS_REPLACE_TABLE = False
|
|
59
|
+
MAX_IDENTIFIER_LENGTH = 128
|
|
56
60
|
SUPPORTS_QUERY_EXECUTION_TRACKING = True
|
|
57
61
|
SCHEMA_DIFFER_KWARGS = {
|
|
58
62
|
"parameterized_type_defaults": {
|
|
@@ -74,6 +78,7 @@ class MSSQLEngineAdapter(
|
|
|
74
78
|
},
|
|
75
79
|
}
|
|
76
80
|
VARIABLE_LENGTH_DATA_TYPES = {"binary", "varbinary", "char", "varchar", "nchar", "nvarchar"}
|
|
81
|
+
INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.MERGE
|
|
77
82
|
|
|
78
83
|
@property
|
|
79
84
|
def catalog_support(self) -> CatalogSupport:
|
|
@@ -145,6 +150,10 @@ class MSSQLEngineAdapter(
|
|
|
145
150
|
def table_exists(self, table_name: TableName) -> bool:
|
|
146
151
|
"""MsSql doesn't support describe so we query information_schema."""
|
|
147
152
|
table = exp.to_table(table_name)
|
|
153
|
+
data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
|
|
154
|
+
if data_object_cache_key in self._data_object_cache:
|
|
155
|
+
logger.debug("Table existence cache hit: %s", data_object_cache_key)
|
|
156
|
+
return self._data_object_cache[data_object_cache_key] is not None
|
|
148
157
|
|
|
149
158
|
sql = (
|
|
150
159
|
exp.select("1")
|
|
@@ -414,7 +423,9 @@ class MSSQLEngineAdapter(
|
|
|
414
423
|
insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None,
|
|
415
424
|
**kwargs: t.Any,
|
|
416
425
|
) -> None:
|
|
417
|
-
|
|
426
|
+
# note that this is passed as table_properties here rather than physical_properties
|
|
427
|
+
use_merge_strategy = kwargs.get("table_properties", {}).get("mssql_merge_exists")
|
|
428
|
+
if (not where or where == exp.true()) and not use_merge_strategy:
|
|
418
429
|
# this is a full table replacement, call the base strategy to do DELETE+INSERT
|
|
419
430
|
# which will result in TRUNCATE+INSERT due to how we have overridden self.delete_from()
|
|
420
431
|
return EngineAdapter._insert_overwrite_by_condition(
|
|
@@ -427,7 +438,7 @@ class MSSQLEngineAdapter(
|
|
|
427
438
|
**kwargs,
|
|
428
439
|
)
|
|
429
440
|
|
|
430
|
-
# For
|
|
441
|
+
# For conditional overwrites or when mssql_merge_exists is set use MERGE
|
|
431
442
|
return super()._insert_overwrite_by_condition(
|
|
432
443
|
table_name=table_name,
|
|
433
444
|
source_queries=source_queries,
|
|
@@ -164,11 +164,11 @@ class MySQLEngineAdapter(
|
|
|
164
164
|
exc_info=True,
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
def
|
|
167
|
+
def _create_table_like(
|
|
168
168
|
self,
|
|
169
169
|
target_table_name: TableName,
|
|
170
170
|
source_table_name: TableName,
|
|
171
|
-
exists: bool
|
|
171
|
+
exists: bool,
|
|
172
172
|
**kwargs: t.Any,
|
|
173
173
|
) -> None:
|
|
174
174
|
self.execute(
|
|
@@ -12,6 +12,7 @@ from sqlmesh.core.engine_adapter.mixins import (
|
|
|
12
12
|
PandasNativeFetchDFSupportMixin,
|
|
13
13
|
RowDiffMixin,
|
|
14
14
|
logical_merge,
|
|
15
|
+
GrantsFromInfoSchemaMixin,
|
|
15
16
|
)
|
|
16
17
|
from sqlmesh.core.engine_adapter.shared import set_catalog
|
|
17
18
|
|
|
@@ -28,14 +29,19 @@ class PostgresEngineAdapter(
|
|
|
28
29
|
PandasNativeFetchDFSupportMixin,
|
|
29
30
|
GetCurrentCatalogFromFunctionMixin,
|
|
30
31
|
RowDiffMixin,
|
|
32
|
+
GrantsFromInfoSchemaMixin,
|
|
31
33
|
):
|
|
32
34
|
DIALECT = "postgres"
|
|
35
|
+
SUPPORTS_GRANTS = True
|
|
33
36
|
SUPPORTS_INDEXES = True
|
|
34
37
|
HAS_VIEW_BINDING = True
|
|
35
38
|
CURRENT_CATALOG_EXPRESSION = exp.column("current_catalog")
|
|
36
39
|
SUPPORTS_REPLACE_TABLE = False
|
|
37
|
-
MAX_IDENTIFIER_LENGTH = 63
|
|
40
|
+
MAX_IDENTIFIER_LENGTH: t.Optional[int] = 63
|
|
38
41
|
SUPPORTS_QUERY_EXECUTION_TRACKING = True
|
|
42
|
+
GRANT_INFORMATION_SCHEMA_TABLE_NAME = "role_table_grants"
|
|
43
|
+
CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.column("current_role")
|
|
44
|
+
SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True
|
|
39
45
|
SCHEMA_DIFFER_KWARGS = {
|
|
40
46
|
"parameterized_type_defaults": {
|
|
41
47
|
# DECIMAL without precision is "up to 131072 digits before the decimal point; up to 16383 digits after the decimal point"
|
|
@@ -79,11 +85,11 @@ class PostgresEngineAdapter(
|
|
|
79
85
|
self._connection_pool.commit()
|
|
80
86
|
return df
|
|
81
87
|
|
|
82
|
-
def
|
|
88
|
+
def _create_table_like(
|
|
83
89
|
self,
|
|
84
90
|
target_table_name: TableName,
|
|
85
91
|
source_table_name: TableName,
|
|
86
|
-
exists: bool
|
|
92
|
+
exists: bool,
|
|
87
93
|
**kwargs: t.Any,
|
|
88
94
|
) -> None:
|
|
89
95
|
self.execute(
|
|
@@ -14,6 +14,7 @@ from sqlmesh.core.engine_adapter.mixins import (
|
|
|
14
14
|
VarcharSizeWorkaroundMixin,
|
|
15
15
|
RowDiffMixin,
|
|
16
16
|
logical_merge,
|
|
17
|
+
GrantsFromInfoSchemaMixin,
|
|
17
18
|
)
|
|
18
19
|
from sqlmesh.core.engine_adapter.shared import (
|
|
19
20
|
CommentCreationView,
|
|
@@ -40,12 +41,15 @@ class RedshiftEngineAdapter(
|
|
|
40
41
|
NonTransactionalTruncateMixin,
|
|
41
42
|
VarcharSizeWorkaroundMixin,
|
|
42
43
|
RowDiffMixin,
|
|
44
|
+
GrantsFromInfoSchemaMixin,
|
|
43
45
|
):
|
|
44
46
|
DIALECT = "redshift"
|
|
45
47
|
CURRENT_CATALOG_EXPRESSION = exp.func("current_database")
|
|
46
48
|
# Redshift doesn't support comments for VIEWs WITH NO SCHEMA BINDING (which we always use)
|
|
47
49
|
COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED
|
|
48
50
|
SUPPORTS_REPLACE_TABLE = False
|
|
51
|
+
SUPPORTS_GRANTS = True
|
|
52
|
+
SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True
|
|
49
53
|
|
|
50
54
|
SCHEMA_DIFFER_KWARGS = {
|
|
51
55
|
"parameterized_type_defaults": {
|
|
@@ -32,6 +32,7 @@ class RisingwaveEngineAdapter(PostgresEngineAdapter):
|
|
|
32
32
|
SUPPORTS_MATERIALIZED_VIEWS = True
|
|
33
33
|
SUPPORTS_TRANSACTIONS = False
|
|
34
34
|
MAX_IDENTIFIER_LENGTH = None
|
|
35
|
+
SUPPORTS_GRANTS = False
|
|
35
36
|
|
|
36
37
|
def columns(
|
|
37
38
|
self, table_name: TableName, include_pseudo_columns: bool = False
|
|
@@ -243,6 +243,8 @@ class InsertOverwriteStrategy(Enum):
|
|
|
243
243
|
# Issue a single INSERT query to replace a data range. The assumption is that the query engine will transparently match partition bounds
|
|
244
244
|
# and replace data rather than append to it. Trino is an example of this when `hive.insert-existing-partitions-behavior=OVERWRITE` is configured
|
|
245
245
|
INTO_IS_OVERWRITE = 4
|
|
246
|
+
# Do the INSERT OVERWRITE using merge since the engine doesn't support it natively
|
|
247
|
+
MERGE = 5
|
|
246
248
|
|
|
247
249
|
@property
|
|
248
250
|
def is_delete_insert(self) -> bool:
|
|
@@ -260,6 +262,10 @@ class InsertOverwriteStrategy(Enum):
|
|
|
260
262
|
def is_into_is_overwrite(self) -> bool:
|
|
261
263
|
return self == InsertOverwriteStrategy.INTO_IS_OVERWRITE
|
|
262
264
|
|
|
265
|
+
@property
|
|
266
|
+
def is_merge(self) -> bool:
|
|
267
|
+
return self == InsertOverwriteStrategy.MERGE
|
|
268
|
+
|
|
263
269
|
|
|
264
270
|
class SourceQuery:
|
|
265
271
|
def __init__(
|
|
@@ -15,6 +15,7 @@ from sqlmesh.core.engine_adapter.mixins import (
|
|
|
15
15
|
GetCurrentCatalogFromFunctionMixin,
|
|
16
16
|
ClusteredByMixin,
|
|
17
17
|
RowDiffMixin,
|
|
18
|
+
GrantsFromInfoSchemaMixin,
|
|
18
19
|
)
|
|
19
20
|
from sqlmesh.core.engine_adapter.shared import (
|
|
20
21
|
CatalogSupport,
|
|
@@ -34,7 +35,12 @@ if t.TYPE_CHECKING:
|
|
|
34
35
|
import pandas as pd
|
|
35
36
|
|
|
36
37
|
from sqlmesh.core._typing import SchemaName, SessionProperties, TableName
|
|
37
|
-
from sqlmesh.core.engine_adapter._typing import
|
|
38
|
+
from sqlmesh.core.engine_adapter._typing import (
|
|
39
|
+
DF,
|
|
40
|
+
Query,
|
|
41
|
+
QueryOrDF,
|
|
42
|
+
SnowparkSession,
|
|
43
|
+
)
|
|
38
44
|
from sqlmesh.core.node import IntervalUnit
|
|
39
45
|
|
|
40
46
|
|
|
@@ -46,7 +52,9 @@ if t.TYPE_CHECKING:
|
|
|
46
52
|
"drop_catalog": CatalogSupport.REQUIRES_SET_CATALOG, # needs a catalog to issue a query to information_schema.databases even though the result is global
|
|
47
53
|
}
|
|
48
54
|
)
|
|
49
|
-
class SnowflakeEngineAdapter(
|
|
55
|
+
class SnowflakeEngineAdapter(
|
|
56
|
+
GetCurrentCatalogFromFunctionMixin, ClusteredByMixin, RowDiffMixin, GrantsFromInfoSchemaMixin
|
|
57
|
+
):
|
|
50
58
|
DIALECT = "snowflake"
|
|
51
59
|
SUPPORTS_MATERIALIZED_VIEWS = True
|
|
52
60
|
SUPPORTS_MATERIALIZED_VIEW_SCHEMA = True
|
|
@@ -54,6 +62,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
54
62
|
SUPPORTS_MANAGED_MODELS = True
|
|
55
63
|
CURRENT_CATALOG_EXPRESSION = exp.func("current_database")
|
|
56
64
|
SUPPORTS_CREATE_DROP_CATALOG = True
|
|
65
|
+
SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS = True
|
|
57
66
|
SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA", "TABLE"]
|
|
58
67
|
SCHEMA_DIFFER_KWARGS = {
|
|
59
68
|
"parameterized_type_defaults": {
|
|
@@ -73,6 +82,9 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
73
82
|
MANAGED_TABLE_KIND = "DYNAMIC TABLE"
|
|
74
83
|
SNOWPARK = "snowpark"
|
|
75
84
|
SUPPORTS_QUERY_EXECUTION_TRACKING = True
|
|
85
|
+
SUPPORTS_GRANTS = True
|
|
86
|
+
CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.func("CURRENT_ROLE")
|
|
87
|
+
USE_CATALOG_IN_GRANTS = True
|
|
76
88
|
|
|
77
89
|
@contextlib.contextmanager
|
|
78
90
|
def session(self, properties: SessionProperties) -> t.Iterator[None]:
|
|
@@ -127,6 +139,23 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
127
139
|
def catalog_support(self) -> CatalogSupport:
|
|
128
140
|
return CatalogSupport.FULL_SUPPORT
|
|
129
141
|
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _grant_object_kind(table_type: DataObjectType) -> str:
|
|
144
|
+
if table_type == DataObjectType.VIEW:
|
|
145
|
+
return "VIEW"
|
|
146
|
+
if table_type == DataObjectType.MATERIALIZED_VIEW:
|
|
147
|
+
return "MATERIALIZED VIEW"
|
|
148
|
+
if table_type == DataObjectType.MANAGED_TABLE:
|
|
149
|
+
return "DYNAMIC TABLE"
|
|
150
|
+
return "TABLE"
|
|
151
|
+
|
|
152
|
+
def _get_current_schema(self) -> str:
|
|
153
|
+
"""Returns the current default schema for the connection."""
|
|
154
|
+
result = self.fetchone("SELECT CURRENT_SCHEMA()")
|
|
155
|
+
if not result or not result[0]:
|
|
156
|
+
raise SQLMeshError("Unable to determine current schema")
|
|
157
|
+
return str(result[0])
|
|
158
|
+
|
|
130
159
|
def _create_catalog(self, catalog_name: exp.Identifier) -> None:
|
|
131
160
|
props = exp.Properties(
|
|
132
161
|
expressions=[exp.SchemaCommentProperty(this=exp.Literal.string(c.SQLMESH_MANAGED))]
|
|
@@ -378,6 +407,8 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
378
407
|
elif isinstance(df, pd.DataFrame):
|
|
379
408
|
from snowflake.connector.pandas_tools import write_pandas
|
|
380
409
|
|
|
410
|
+
ordered_df = df[list(source_columns_to_types)]
|
|
411
|
+
|
|
381
412
|
# Workaround for https://github.com/snowflakedb/snowflake-connector-python/issues/1034
|
|
382
413
|
# The above issue has already been fixed upstream, but we keep the following
|
|
383
414
|
# line anyway in order to support a wider range of Snowflake versions.
|
|
@@ -388,16 +419,16 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
388
419
|
|
|
389
420
|
# See: https://stackoverflow.com/a/75627721
|
|
390
421
|
for column, kind in source_columns_to_types.items():
|
|
391
|
-
if is_datetime64_any_dtype(
|
|
422
|
+
if is_datetime64_any_dtype(ordered_df.dtypes[column]):
|
|
392
423
|
if kind.is_type("date"): # type: ignore
|
|
393
|
-
|
|
394
|
-
elif getattr(
|
|
395
|
-
|
|
424
|
+
ordered_df[column] = pd.to_datetime(ordered_df[column]).dt.date # type: ignore
|
|
425
|
+
elif getattr(ordered_df.dtypes[column], "tz", None) is not None: # type: ignore
|
|
426
|
+
ordered_df[column] = pd.to_datetime(ordered_df[column]).dt.strftime(
|
|
396
427
|
"%Y-%m-%d %H:%M:%S.%f%z"
|
|
397
428
|
) # type: ignore
|
|
398
429
|
# https://github.com/snowflakedb/snowflake-connector-python/issues/1677
|
|
399
430
|
else: # type: ignore
|
|
400
|
-
|
|
431
|
+
ordered_df[column] = pd.to_datetime(ordered_df[column]).dt.strftime(
|
|
401
432
|
"%Y-%m-%d %H:%M:%S.%f"
|
|
402
433
|
) # type: ignore
|
|
403
434
|
|
|
@@ -407,7 +438,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
407
438
|
|
|
408
439
|
write_pandas(
|
|
409
440
|
self._connection_pool.get(),
|
|
410
|
-
|
|
441
|
+
ordered_df,
|
|
411
442
|
temp_table.name,
|
|
412
443
|
schema=temp_table.db or None,
|
|
413
444
|
database=database.sql(dialect=self.dialect) if database else None,
|
|
@@ -526,16 +557,36 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
526
557
|
type=DataObjectType.from_str(row.type), # type: ignore
|
|
527
558
|
clustering_key=row.clustering_key, # type: ignore
|
|
528
559
|
)
|
|
529
|
-
for
|
|
560
|
+
# lowercase the column names for cases where Snowflake might return uppercase column names for certain catalogs
|
|
561
|
+
for row in df.rename(columns={col: col.lower() for col in df.columns}).itertuples()
|
|
530
562
|
]
|
|
531
563
|
|
|
564
|
+
def _get_grant_expression(self, table: exp.Table) -> exp.Expression:
|
|
565
|
+
# Upon execute the catalog in table expressions are properly normalized to handle the case where a user provides
|
|
566
|
+
# the default catalog in their connection config. This doesn't though update catalogs in strings like when querying
|
|
567
|
+
# the information schema. So we need to manually replace those here.
|
|
568
|
+
expression = super()._get_grant_expression(table)
|
|
569
|
+
for col_exp in expression.find_all(exp.Column):
|
|
570
|
+
if col_exp.this.name == "table_catalog":
|
|
571
|
+
and_exp = col_exp.parent
|
|
572
|
+
assert and_exp is not None, "Expected column expression to have a parent"
|
|
573
|
+
assert and_exp.expression, "Expected AND expression to have an expression"
|
|
574
|
+
normalized_catalog = self._normalize_catalog(
|
|
575
|
+
exp.table_("placeholder", db="placeholder", catalog=and_exp.expression.this)
|
|
576
|
+
)
|
|
577
|
+
and_exp.set(
|
|
578
|
+
"expression",
|
|
579
|
+
exp.Literal.string(normalized_catalog.args["catalog"].alias_or_name),
|
|
580
|
+
)
|
|
581
|
+
return expression
|
|
582
|
+
|
|
532
583
|
def set_current_catalog(self, catalog: str) -> None:
|
|
533
584
|
self.execute(exp.Use(this=exp.to_identifier(catalog)))
|
|
534
585
|
|
|
535
586
|
def set_current_schema(self, schema: str) -> None:
|
|
536
587
|
self.execute(exp.Use(kind="SCHEMA", this=to_schema(schema)))
|
|
537
588
|
|
|
538
|
-
def
|
|
589
|
+
def _normalize_catalog(self, expression: exp.Expression) -> exp.Expression:
|
|
539
590
|
# note: important to use self._default_catalog instead of the self.default_catalog property
|
|
540
591
|
# otherwise we get RecursionError: maximum recursion depth exceeded
|
|
541
592
|
# because it calls get_current_catalog(), which executes a query, which needs the default catalog, which calls get_current_catalog()... etc
|
|
@@ -568,8 +619,12 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
568
619
|
# Snowflake connection config. This is because the catalog present on the model gets normalized and quoted to match
|
|
569
620
|
# the source dialect, which isnt always compatible with Snowflake
|
|
570
621
|
expression = expression.transform(catalog_rewriter)
|
|
622
|
+
return expression
|
|
571
623
|
|
|
572
|
-
|
|
624
|
+
def _to_sql(self, expression: exp.Expression, quote: bool = True, **kwargs: t.Any) -> str:
|
|
625
|
+
return super()._to_sql(
|
|
626
|
+
expression=self._normalize_catalog(expression), quote=quote, **kwargs
|
|
627
|
+
)
|
|
573
628
|
|
|
574
629
|
def _create_column_comments(
|
|
575
630
|
self,
|
|
@@ -610,6 +665,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
610
665
|
target_table_name: TableName,
|
|
611
666
|
source_table_name: TableName,
|
|
612
667
|
replace: bool = False,
|
|
668
|
+
exists: bool = True,
|
|
613
669
|
clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None,
|
|
614
670
|
**kwargs: t.Any,
|
|
615
671
|
) -> None:
|
|
@@ -665,3 +721,18 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
665
721
|
self._connection_pool.set_attribute(self.SNOWPARK, None)
|
|
666
722
|
|
|
667
723
|
return super().close()
|
|
724
|
+
|
|
725
|
+
def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]:
|
|
726
|
+
from sqlmesh.utils.date import to_timestamp
|
|
727
|
+
|
|
728
|
+
num_tables = len(table_names)
|
|
729
|
+
|
|
730
|
+
query = "SELECT LAST_ALTERED FROM INFORMATION_SCHEMA.TABLES WHERE"
|
|
731
|
+
for i, table_name in enumerate(table_names):
|
|
732
|
+
table = exp.to_table(table_name)
|
|
733
|
+
query += f"""(TABLE_NAME = '{table.name}' AND TABLE_SCHEMA = '{table.db}' AND TABLE_CATALOG = '{table.catalog}')"""
|
|
734
|
+
if i < num_tables - 1:
|
|
735
|
+
query += " OR "
|
|
736
|
+
|
|
737
|
+
result = self.fetchall(query)
|
|
738
|
+
return [to_timestamp(row[0]) for row in result]
|
|
@@ -397,19 +397,21 @@ class SparkEngineAdapter(
|
|
|
397
397
|
def set_current_catalog(self, catalog_name: str) -> None:
|
|
398
398
|
self.connection.set_current_catalog(catalog_name)
|
|
399
399
|
|
|
400
|
-
def
|
|
400
|
+
def _get_current_schema(self) -> str:
|
|
401
401
|
if self._use_spark_session:
|
|
402
402
|
return self.spark.catalog.currentDatabase()
|
|
403
403
|
return self.fetchone(exp.select(exp.func("current_database")))[0] # type: ignore
|
|
404
404
|
|
|
405
|
-
def get_data_object(
|
|
405
|
+
def get_data_object(
|
|
406
|
+
self, target_name: TableName, safe_to_cache: bool = False
|
|
407
|
+
) -> t.Optional[DataObject]:
|
|
406
408
|
target_table = exp.to_table(target_name)
|
|
407
409
|
if isinstance(target_table.this, exp.Dot) and target_table.this.expression.name.startswith(
|
|
408
410
|
f"{self.BRANCH_PREFIX}{self.WAP_PREFIX}"
|
|
409
411
|
):
|
|
410
412
|
# Exclude the branch name
|
|
411
413
|
target_table.set("this", target_table.this.this)
|
|
412
|
-
return super().get_data_object(target_table)
|
|
414
|
+
return super().get_data_object(target_table, safe_to_cache=safe_to_cache)
|
|
413
415
|
|
|
414
416
|
def create_state_table(
|
|
415
417
|
self,
|
|
@@ -457,12 +459,14 @@ class SparkEngineAdapter(
|
|
|
457
459
|
if wap_id.startswith(f"{self.BRANCH_PREFIX}{self.WAP_PREFIX}"):
|
|
458
460
|
table_name.set("this", table_name.this.this)
|
|
459
461
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
462
|
+
do_dummy_insert = False
|
|
463
|
+
if self.wap_enabled:
|
|
464
|
+
wap_supported = (
|
|
465
|
+
kwargs.get("storage_format") or ""
|
|
466
|
+
).lower() == "iceberg" or self.wap_supported(table_name)
|
|
467
|
+
do_dummy_insert = (
|
|
468
|
+
False if not wap_supported or not exists else not self.table_exists(table_name)
|
|
469
|
+
)
|
|
466
470
|
super()._create_table(
|
|
467
471
|
table_name_or_schema,
|
|
468
472
|
expression,
|
|
@@ -535,7 +539,7 @@ class SparkEngineAdapter(
|
|
|
535
539
|
if not table.catalog:
|
|
536
540
|
table.set("catalog", self.get_current_catalog())
|
|
537
541
|
if not table.db:
|
|
538
|
-
table.set("db", self.
|
|
542
|
+
table.set("db", self._get_current_schema())
|
|
539
543
|
return table
|
|
540
544
|
|
|
541
545
|
def _build_create_comment_column_exp(
|