dbt-adapters 1.22.2__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.
- dbt/adapters/__about__.py +1 -0
- dbt/adapters/__init__.py +8 -0
- dbt/adapters/base/README.md +13 -0
- dbt/adapters/base/__init__.py +16 -0
- dbt/adapters/base/column.py +173 -0
- dbt/adapters/base/connections.py +429 -0
- dbt/adapters/base/impl.py +2036 -0
- dbt/adapters/base/meta.py +150 -0
- dbt/adapters/base/plugin.py +32 -0
- dbt/adapters/base/query_headers.py +106 -0
- dbt/adapters/base/relation.py +648 -0
- dbt/adapters/cache.py +521 -0
- dbt/adapters/capability.py +63 -0
- dbt/adapters/catalogs/__init__.py +14 -0
- dbt/adapters/catalogs/_client.py +54 -0
- dbt/adapters/catalogs/_constants.py +1 -0
- dbt/adapters/catalogs/_exceptions.py +39 -0
- dbt/adapters/catalogs/_integration.py +113 -0
- dbt/adapters/clients/__init__.py +0 -0
- dbt/adapters/clients/jinja.py +24 -0
- dbt/adapters/contracts/__init__.py +0 -0
- dbt/adapters/contracts/connection.py +229 -0
- dbt/adapters/contracts/macros.py +11 -0
- dbt/adapters/contracts/relation.py +160 -0
- dbt/adapters/events/README.md +51 -0
- dbt/adapters/events/__init__.py +0 -0
- dbt/adapters/events/adapter_types_pb2.py +2 -0
- dbt/adapters/events/base_types.py +36 -0
- dbt/adapters/events/logging.py +83 -0
- dbt/adapters/events/types.py +436 -0
- dbt/adapters/exceptions/__init__.py +40 -0
- dbt/adapters/exceptions/alias.py +24 -0
- dbt/adapters/exceptions/cache.py +68 -0
- dbt/adapters/exceptions/compilation.py +269 -0
- dbt/adapters/exceptions/connection.py +16 -0
- dbt/adapters/exceptions/database.py +51 -0
- dbt/adapters/factory.py +264 -0
- dbt/adapters/protocol.py +150 -0
- dbt/adapters/py.typed +0 -0
- dbt/adapters/record/__init__.py +2 -0
- dbt/adapters/record/base.py +291 -0
- dbt/adapters/record/cursor/cursor.py +69 -0
- dbt/adapters/record/cursor/description.py +37 -0
- dbt/adapters/record/cursor/execute.py +39 -0
- dbt/adapters/record/cursor/fetchall.py +69 -0
- dbt/adapters/record/cursor/fetchmany.py +23 -0
- dbt/adapters/record/cursor/fetchone.py +23 -0
- dbt/adapters/record/cursor/rowcount.py +23 -0
- dbt/adapters/record/handle.py +55 -0
- dbt/adapters/record/serialization.py +115 -0
- dbt/adapters/reference_keys.py +39 -0
- dbt/adapters/relation_configs/README.md +25 -0
- dbt/adapters/relation_configs/__init__.py +12 -0
- dbt/adapters/relation_configs/config_base.py +46 -0
- dbt/adapters/relation_configs/config_change.py +26 -0
- dbt/adapters/relation_configs/config_validation.py +57 -0
- dbt/adapters/sql/__init__.py +2 -0
- dbt/adapters/sql/connections.py +263 -0
- dbt/adapters/sql/impl.py +286 -0
- dbt/adapters/utils.py +69 -0
- dbt/include/__init__.py +3 -0
- dbt/include/global_project/__init__.py +4 -0
- dbt/include/global_project/dbt_project.yml +7 -0
- dbt/include/global_project/docs/overview.md +43 -0
- dbt/include/global_project/macros/adapters/apply_grants.sql +167 -0
- dbt/include/global_project/macros/adapters/columns.sql +144 -0
- dbt/include/global_project/macros/adapters/freshness.sql +32 -0
- dbt/include/global_project/macros/adapters/indexes.sql +41 -0
- dbt/include/global_project/macros/adapters/metadata.sql +105 -0
- dbt/include/global_project/macros/adapters/persist_docs.sql +33 -0
- dbt/include/global_project/macros/adapters/relation.sql +84 -0
- dbt/include/global_project/macros/adapters/schema.sql +20 -0
- dbt/include/global_project/macros/adapters/show.sql +26 -0
- dbt/include/global_project/macros/adapters/timestamps.sql +52 -0
- dbt/include/global_project/macros/adapters/validate_sql.sql +10 -0
- dbt/include/global_project/macros/etc/datetime.sql +62 -0
- dbt/include/global_project/macros/etc/statement.sql +52 -0
- dbt/include/global_project/macros/generic_test_sql/accepted_values.sql +27 -0
- dbt/include/global_project/macros/generic_test_sql/not_null.sql +9 -0
- dbt/include/global_project/macros/generic_test_sql/relationships.sql +23 -0
- dbt/include/global_project/macros/generic_test_sql/unique.sql +12 -0
- dbt/include/global_project/macros/get_custom_name/get_custom_alias.sql +36 -0
- dbt/include/global_project/macros/get_custom_name/get_custom_database.sql +32 -0
- dbt/include/global_project/macros/get_custom_name/get_custom_schema.sql +60 -0
- dbt/include/global_project/macros/materializations/configs.sql +21 -0
- dbt/include/global_project/macros/materializations/functions/aggregate.sql +65 -0
- dbt/include/global_project/macros/materializations/functions/function.sql +20 -0
- dbt/include/global_project/macros/materializations/functions/helpers.sql +20 -0
- dbt/include/global_project/macros/materializations/functions/scalar.sql +69 -0
- dbt/include/global_project/macros/materializations/hooks.sql +35 -0
- dbt/include/global_project/macros/materializations/models/clone/can_clone_table.sql +7 -0
- dbt/include/global_project/macros/materializations/models/clone/clone.sql +67 -0
- dbt/include/global_project/macros/materializations/models/clone/create_or_replace_clone.sql +7 -0
- dbt/include/global_project/macros/materializations/models/incremental/column_helpers.sql +80 -0
- dbt/include/global_project/macros/materializations/models/incremental/incremental.sql +99 -0
- dbt/include/global_project/macros/materializations/models/incremental/is_incremental.sql +13 -0
- dbt/include/global_project/macros/materializations/models/incremental/merge.sql +120 -0
- dbt/include/global_project/macros/materializations/models/incremental/on_schema_change.sql +159 -0
- dbt/include/global_project/macros/materializations/models/incremental/strategies.sql +92 -0
- dbt/include/global_project/macros/materializations/models/materialized_view.sql +121 -0
- dbt/include/global_project/macros/materializations/models/table.sql +64 -0
- dbt/include/global_project/macros/materializations/models/view.sql +72 -0
- dbt/include/global_project/macros/materializations/seeds/helpers.sql +128 -0
- dbt/include/global_project/macros/materializations/seeds/seed.sql +60 -0
- dbt/include/global_project/macros/materializations/snapshots/helpers.sql +345 -0
- dbt/include/global_project/macros/materializations/snapshots/snapshot.sql +109 -0
- dbt/include/global_project/macros/materializations/snapshots/snapshot_merge.sql +34 -0
- dbt/include/global_project/macros/materializations/snapshots/strategies.sql +184 -0
- dbt/include/global_project/macros/materializations/tests/helpers.sql +44 -0
- dbt/include/global_project/macros/materializations/tests/test.sql +66 -0
- dbt/include/global_project/macros/materializations/tests/unit.sql +40 -0
- dbt/include/global_project/macros/materializations/tests/where_subquery.sql +15 -0
- dbt/include/global_project/macros/python_model/python.sql +114 -0
- dbt/include/global_project/macros/relations/column/columns_spec_ddl.sql +89 -0
- dbt/include/global_project/macros/relations/create.sql +23 -0
- dbt/include/global_project/macros/relations/create_backup.sql +17 -0
- dbt/include/global_project/macros/relations/create_intermediate.sql +17 -0
- dbt/include/global_project/macros/relations/drop.sql +41 -0
- dbt/include/global_project/macros/relations/drop_backup.sql +14 -0
- dbt/include/global_project/macros/relations/materialized_view/alter.sql +55 -0
- dbt/include/global_project/macros/relations/materialized_view/create.sql +10 -0
- dbt/include/global_project/macros/relations/materialized_view/drop.sql +14 -0
- dbt/include/global_project/macros/relations/materialized_view/refresh.sql +9 -0
- dbt/include/global_project/macros/relations/materialized_view/rename.sql +10 -0
- dbt/include/global_project/macros/relations/materialized_view/replace.sql +10 -0
- dbt/include/global_project/macros/relations/rename.sql +35 -0
- dbt/include/global_project/macros/relations/rename_intermediate.sql +14 -0
- dbt/include/global_project/macros/relations/replace.sql +50 -0
- dbt/include/global_project/macros/relations/schema.sql +8 -0
- dbt/include/global_project/macros/relations/table/create.sql +60 -0
- dbt/include/global_project/macros/relations/table/drop.sql +14 -0
- dbt/include/global_project/macros/relations/table/rename.sql +10 -0
- dbt/include/global_project/macros/relations/table/replace.sql +10 -0
- dbt/include/global_project/macros/relations/view/create.sql +27 -0
- dbt/include/global_project/macros/relations/view/drop.sql +14 -0
- dbt/include/global_project/macros/relations/view/rename.sql +10 -0
- dbt/include/global_project/macros/relations/view/replace.sql +66 -0
- dbt/include/global_project/macros/unit_test_sql/get_fixture_sql.sql +107 -0
- dbt/include/global_project/macros/utils/any_value.sql +9 -0
- dbt/include/global_project/macros/utils/array_append.sql +8 -0
- dbt/include/global_project/macros/utils/array_concat.sql +7 -0
- dbt/include/global_project/macros/utils/array_construct.sql +12 -0
- dbt/include/global_project/macros/utils/bool_or.sql +9 -0
- dbt/include/global_project/macros/utils/cast.sql +7 -0
- dbt/include/global_project/macros/utils/cast_bool_to_text.sql +7 -0
- dbt/include/global_project/macros/utils/concat.sql +7 -0
- dbt/include/global_project/macros/utils/data_types.sql +129 -0
- dbt/include/global_project/macros/utils/date.sql +10 -0
- dbt/include/global_project/macros/utils/date_spine.sql +75 -0
- dbt/include/global_project/macros/utils/date_trunc.sql +7 -0
- dbt/include/global_project/macros/utils/dateadd.sql +14 -0
- dbt/include/global_project/macros/utils/datediff.sql +14 -0
- dbt/include/global_project/macros/utils/equals.sql +14 -0
- dbt/include/global_project/macros/utils/escape_single_quotes.sql +8 -0
- dbt/include/global_project/macros/utils/except.sql +9 -0
- dbt/include/global_project/macros/utils/generate_series.sql +53 -0
- dbt/include/global_project/macros/utils/hash.sql +7 -0
- dbt/include/global_project/macros/utils/intersect.sql +9 -0
- dbt/include/global_project/macros/utils/last_day.sql +15 -0
- dbt/include/global_project/macros/utils/length.sql +11 -0
- dbt/include/global_project/macros/utils/listagg.sql +30 -0
- dbt/include/global_project/macros/utils/literal.sql +7 -0
- dbt/include/global_project/macros/utils/position.sql +11 -0
- dbt/include/global_project/macros/utils/replace.sql +14 -0
- dbt/include/global_project/macros/utils/right.sql +12 -0
- dbt/include/global_project/macros/utils/safe_cast.sql +9 -0
- dbt/include/global_project/macros/utils/split_part.sql +26 -0
- dbt/include/global_project/tests/generic/builtin.sql +30 -0
- dbt/include/py.typed +0 -0
- dbt_adapters-1.22.2.dist-info/METADATA +124 -0
- dbt_adapters-1.22.2.dist-info/RECORD +173 -0
- dbt_adapters-1.22.2.dist-info/WHEEL +4 -0
- dbt_adapters-1.22.2.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import time
|
|
3
|
+
from concurrent.futures import as_completed, Future
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from multiprocessing.context import SpawnContext
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Callable,
|
|
12
|
+
Dict,
|
|
13
|
+
FrozenSet,
|
|
14
|
+
Iterable,
|
|
15
|
+
Iterator,
|
|
16
|
+
List,
|
|
17
|
+
Mapping,
|
|
18
|
+
Optional,
|
|
19
|
+
Set,
|
|
20
|
+
Tuple,
|
|
21
|
+
Type,
|
|
22
|
+
TypedDict,
|
|
23
|
+
Union,
|
|
24
|
+
TYPE_CHECKING,
|
|
25
|
+
)
|
|
26
|
+
import pytz
|
|
27
|
+
|
|
28
|
+
from dbt.adapters.record.base import (
|
|
29
|
+
AdapterExecuteRecord,
|
|
30
|
+
AdapterGetPartitionsMetadataRecord,
|
|
31
|
+
AdapterConvertTypeRecord,
|
|
32
|
+
AdapterStandardizeGrantsDictRecord,
|
|
33
|
+
AdapterListRelationsWithoutCachingRecord,
|
|
34
|
+
AdapterGetColumnsInRelationRecord,
|
|
35
|
+
)
|
|
36
|
+
from dbt_common.behavior_flags import Behavior, BehaviorFlag
|
|
37
|
+
from dbt_common.clients.jinja import CallableMacroGenerator
|
|
38
|
+
from dbt_common.contracts.constraints import (
|
|
39
|
+
ColumnLevelConstraint,
|
|
40
|
+
ConstraintType,
|
|
41
|
+
ModelLevelConstraint,
|
|
42
|
+
)
|
|
43
|
+
from dbt_common.contracts.metadata import CatalogTable
|
|
44
|
+
from dbt_common.events.functions import fire_event, warn_or_error
|
|
45
|
+
from dbt_common.exceptions import (
|
|
46
|
+
DbtInternalError,
|
|
47
|
+
DbtRuntimeError,
|
|
48
|
+
DbtValidationError,
|
|
49
|
+
MacroArgTypeError,
|
|
50
|
+
MacroResultError,
|
|
51
|
+
NotImplementedError,
|
|
52
|
+
UnexpectedNullError,
|
|
53
|
+
)
|
|
54
|
+
from dbt_common.record import auto_record_function, record_function, supports_replay
|
|
55
|
+
from dbt_common.utils import (
|
|
56
|
+
AttrDict,
|
|
57
|
+
cast_to_str,
|
|
58
|
+
executor,
|
|
59
|
+
filter_null_values,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
from dbt.adapters.base.column import Column as BaseColumn
|
|
63
|
+
from dbt.adapters.base.connections import (
|
|
64
|
+
AdapterResponse,
|
|
65
|
+
BaseConnectionManager,
|
|
66
|
+
Connection,
|
|
67
|
+
)
|
|
68
|
+
from dbt.adapters.base.meta import AdapterMeta, available, available_property
|
|
69
|
+
from dbt.adapters.base.relation import (
|
|
70
|
+
BaseRelation,
|
|
71
|
+
ComponentName,
|
|
72
|
+
InformationSchema,
|
|
73
|
+
SchemaSearchMap,
|
|
74
|
+
AdapterTrackingRelationInfo,
|
|
75
|
+
)
|
|
76
|
+
from dbt.adapters.cache import RelationsCache, _make_ref_key_dict
|
|
77
|
+
from dbt.adapters.capability import Capability, CapabilityDict
|
|
78
|
+
from dbt.adapters.catalogs import (
|
|
79
|
+
CatalogIntegration,
|
|
80
|
+
CatalogIntegrationClient,
|
|
81
|
+
CatalogIntegrationConfig,
|
|
82
|
+
CatalogRelation,
|
|
83
|
+
CATALOG_INTEGRATION_MODEL_CONFIG_NAME,
|
|
84
|
+
)
|
|
85
|
+
from dbt.adapters.contracts.connection import Credentials
|
|
86
|
+
from dbt.adapters.contracts.macros import MacroResolverProtocol
|
|
87
|
+
from dbt.adapters.contracts.relation import RelationConfig
|
|
88
|
+
|
|
89
|
+
from dbt.adapters.events.types import (
|
|
90
|
+
CacheMiss,
|
|
91
|
+
CatalogGenerationError,
|
|
92
|
+
CodeExecution,
|
|
93
|
+
CodeExecutionStatus,
|
|
94
|
+
CollectFreshnessReturnSignature,
|
|
95
|
+
ConstraintNotEnforced,
|
|
96
|
+
ConstraintNotSupported,
|
|
97
|
+
ListRelations,
|
|
98
|
+
)
|
|
99
|
+
from dbt.adapters.exceptions import (
|
|
100
|
+
NullRelationCacheAttemptedError,
|
|
101
|
+
NullRelationDropAttemptedError,
|
|
102
|
+
QuoteConfigTypeError,
|
|
103
|
+
RelationReturnedMultipleResultsError,
|
|
104
|
+
RenameToNoneAttemptedError,
|
|
105
|
+
SnapshotTargetNotSnapshotTableError,
|
|
106
|
+
UnexpectedNonTimestampError,
|
|
107
|
+
)
|
|
108
|
+
from dbt.adapters.protocol import AdapterConfig, MacroContextGeneratorCallable
|
|
109
|
+
from dbt.adapters.events.logging import AdapterLogger
|
|
110
|
+
|
|
111
|
+
logger = AdapterLogger(__name__)
|
|
112
|
+
if TYPE_CHECKING:
|
|
113
|
+
import agate
|
|
114
|
+
|
|
115
|
+
GET_CATALOG_MACRO_NAME = "get_catalog"
|
|
116
|
+
GET_CATALOG_RELATIONS_MACRO_NAME = "get_catalog_relations"
|
|
117
|
+
FRESHNESS_MACRO_NAME = "collect_freshness"
|
|
118
|
+
CUSTOM_SQL_FRESHNESS_MACRO_NAME = "collect_freshness_custom_sql"
|
|
119
|
+
GET_RELATION_LAST_MODIFIED_MACRO_NAME = "get_relation_last_modified"
|
|
120
|
+
DEFAULT_BASE_BEHAVIOR_FLAGS = [
|
|
121
|
+
{
|
|
122
|
+
"name": "require_batched_execution_for_custom_microbatch_strategy",
|
|
123
|
+
"default": False,
|
|
124
|
+
"docs_url": "https://docs.getdbt.com/docs/build/incremental-microbatch",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "enable_truthy_nulls_equals_macro",
|
|
128
|
+
"default": False,
|
|
129
|
+
"docs_url": "",
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ConstraintSupport(str, Enum):
|
|
135
|
+
ENFORCED = "enforced"
|
|
136
|
+
NOT_ENFORCED = "not_enforced"
|
|
137
|
+
NOT_SUPPORTED = "not_supported"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_callback_empty_table(*args, **kwargs) -> Tuple[str, "agate.Table"]:
|
|
141
|
+
# Lazy load agate_helper to avoid importing agate when it is not necessary.
|
|
142
|
+
from dbt_common.clients.agate_helper import empty_table
|
|
143
|
+
|
|
144
|
+
return "", empty_table()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _expect_row_value(key: str, row: "agate.Row"):
|
|
148
|
+
if key not in row.keys():
|
|
149
|
+
raise DbtInternalError(
|
|
150
|
+
'Got a row without "{}" column, columns: {}'.format(key, row.keys())
|
|
151
|
+
)
|
|
152
|
+
return row[key]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _catalog_filter_schemas(
|
|
156
|
+
used_schemas: FrozenSet[Tuple[str, str]]
|
|
157
|
+
) -> Callable[["agate.Row"], bool]:
|
|
158
|
+
"""Return a function that takes a row and decides if the row should be
|
|
159
|
+
included in the catalog output.
|
|
160
|
+
"""
|
|
161
|
+
schemas = frozenset(
|
|
162
|
+
(d.lower(), s.lower()) for d, s in used_schemas if d is not None and s is not None
|
|
163
|
+
)
|
|
164
|
+
if null_schemas := [d for d, s in used_schemas if d is None or s is None]:
|
|
165
|
+
logger.debug(
|
|
166
|
+
f"used_schemas contains None for either database or schema, skipping {null_schemas}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def test(row: "agate.Row") -> bool:
|
|
170
|
+
table_database = _expect_row_value("table_database", row)
|
|
171
|
+
table_schema = _expect_row_value("table_schema", row)
|
|
172
|
+
# the schema may be present but None, which is not an error and should
|
|
173
|
+
# be filtered out
|
|
174
|
+
|
|
175
|
+
if table_schema is None:
|
|
176
|
+
return False
|
|
177
|
+
if table_database is None:
|
|
178
|
+
logger.debug(f"table_database is None, skipping {table_schema}")
|
|
179
|
+
return False
|
|
180
|
+
return (table_database.lower(), table_schema.lower()) in schemas
|
|
181
|
+
|
|
182
|
+
return test
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _utc(dt: Optional[datetime], source: Optional[BaseRelation], field_name: str) -> datetime:
|
|
186
|
+
"""If dt has a timezone, return a new datetime that's in UTC. Otherwise,
|
|
187
|
+
assume the datetime is already for UTC and add the timezone.
|
|
188
|
+
"""
|
|
189
|
+
if dt is None:
|
|
190
|
+
raise UnexpectedNullError(field_name, source)
|
|
191
|
+
|
|
192
|
+
elif not hasattr(dt, "tzinfo"):
|
|
193
|
+
raise UnexpectedNonTimestampError(field_name, source, dt)
|
|
194
|
+
|
|
195
|
+
elif dt.tzinfo:
|
|
196
|
+
return dt.astimezone(pytz.UTC)
|
|
197
|
+
else:
|
|
198
|
+
return dt.replace(tzinfo=pytz.UTC)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _relation_name(rel: Optional[BaseRelation]) -> str:
|
|
202
|
+
if rel is None:
|
|
203
|
+
return "null relation"
|
|
204
|
+
else:
|
|
205
|
+
return str(rel)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def log_code_execution(code_execution_function):
|
|
209
|
+
# decorator to log code and execution time
|
|
210
|
+
if code_execution_function.__name__ != "submit_python_job":
|
|
211
|
+
raise ValueError("this should be only used to log submit_python_job now")
|
|
212
|
+
|
|
213
|
+
def execution_with_log(*args):
|
|
214
|
+
self = args[0]
|
|
215
|
+
connection_name = self.connections.get_thread_connection().name
|
|
216
|
+
fire_event(CodeExecution(conn_name=connection_name, code_content=args[2]))
|
|
217
|
+
start_time = time.time()
|
|
218
|
+
response = code_execution_function(*args)
|
|
219
|
+
fire_event(
|
|
220
|
+
CodeExecutionStatus(
|
|
221
|
+
status=response._message, elapsed=round((time.time() - start_time), 2)
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
return response
|
|
225
|
+
|
|
226
|
+
return execution_with_log
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class PythonJobHelper:
|
|
230
|
+
def __init__(self, parsed_model: Dict, credential: Credentials) -> None:
|
|
231
|
+
raise NotImplementedError("PythonJobHelper is not implemented yet")
|
|
232
|
+
|
|
233
|
+
def submit(self, compiled_code: str) -> Any:
|
|
234
|
+
raise NotImplementedError("PythonJobHelper submit function is not implemented yet")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class FreshnessResponse(TypedDict):
|
|
238
|
+
max_loaded_at: datetime
|
|
239
|
+
snapshotted_at: datetime
|
|
240
|
+
age: float # age in seconds
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class SnapshotStrategy(TypedDict):
|
|
244
|
+
unique_key: Optional[str]
|
|
245
|
+
updated_at: Optional[str]
|
|
246
|
+
row_changed: Optional[str]
|
|
247
|
+
scd_id: Optional[str]
|
|
248
|
+
hard_deletes: Optional[str]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@supports_replay
|
|
252
|
+
class BaseAdapter(metaclass=AdapterMeta):
|
|
253
|
+
"""The BaseAdapter provides an abstract base class for adapters.
|
|
254
|
+
|
|
255
|
+
Adapters must implement the following methods and macros. Some of the
|
|
256
|
+
methods can be safely overridden as a noop, where it makes sense
|
|
257
|
+
(transactions on databases that don't support them, for instance). Those
|
|
258
|
+
methods are marked with a (passable) in their docstrings. Check docstrings
|
|
259
|
+
for type information, etc.
|
|
260
|
+
|
|
261
|
+
To implement a macro, implement "${adapter_type}__${macro_name}" in the
|
|
262
|
+
adapter's internal project.
|
|
263
|
+
|
|
264
|
+
To invoke a method in an adapter macro, call it on the 'adapter' Jinja
|
|
265
|
+
object using dot syntax.
|
|
266
|
+
|
|
267
|
+
To invoke a method in model code, add the @available decorator atop a method
|
|
268
|
+
declaration. Methods are invoked as macros.
|
|
269
|
+
|
|
270
|
+
Methods:
|
|
271
|
+
- exception_handler
|
|
272
|
+
- date_function
|
|
273
|
+
- list_schemas
|
|
274
|
+
- drop_relation
|
|
275
|
+
- truncate_relation
|
|
276
|
+
- rename_relation
|
|
277
|
+
- get_columns_in_relation
|
|
278
|
+
- get_catalog_for_single_relation
|
|
279
|
+
- get_column_schema_from_query
|
|
280
|
+
- expand_column_types
|
|
281
|
+
- list_relations_without_caching
|
|
282
|
+
- is_cancelable
|
|
283
|
+
- create_schema
|
|
284
|
+
- drop_schema
|
|
285
|
+
- quote
|
|
286
|
+
- convert_text_type
|
|
287
|
+
- convert_number_type
|
|
288
|
+
- convert_boolean_type
|
|
289
|
+
- convert_datetime_type
|
|
290
|
+
- convert_date_type
|
|
291
|
+
- convert_time_type
|
|
292
|
+
- standardize_grants_dict
|
|
293
|
+
|
|
294
|
+
Macros:
|
|
295
|
+
- get_catalog
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
Relation: Type[BaseRelation] = BaseRelation
|
|
299
|
+
Column: Type[BaseColumn] = BaseColumn
|
|
300
|
+
ConnectionManager: Type[BaseConnectionManager]
|
|
301
|
+
CATALOG_INTEGRATIONS: Iterable[Type[CatalogIntegration]] = []
|
|
302
|
+
|
|
303
|
+
# A set of clobber config fields accepted by this adapter
|
|
304
|
+
# for use in materializations
|
|
305
|
+
AdapterSpecificConfigs: Type[AdapterConfig] = AdapterConfig
|
|
306
|
+
|
|
307
|
+
CONSTRAINT_SUPPORT = {
|
|
308
|
+
ConstraintType.check: ConstraintSupport.NOT_SUPPORTED,
|
|
309
|
+
ConstraintType.not_null: ConstraintSupport.ENFORCED,
|
|
310
|
+
ConstraintType.unique: ConstraintSupport.NOT_ENFORCED,
|
|
311
|
+
ConstraintType.primary_key: ConstraintSupport.NOT_ENFORCED,
|
|
312
|
+
ConstraintType.foreign_key: ConstraintSupport.ENFORCED,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
MAX_SCHEMA_METADATA_RELATIONS = 100
|
|
316
|
+
|
|
317
|
+
# This static member variable can be overridden in concrete adapter
|
|
318
|
+
# implementations to indicate adapter support for optional capabilities.
|
|
319
|
+
_capabilities = CapabilityDict({})
|
|
320
|
+
|
|
321
|
+
def __init__(self, config, mp_context: SpawnContext) -> None:
|
|
322
|
+
self.config = config
|
|
323
|
+
self.cache = RelationsCache(log_cache_events=config.log_cache_events)
|
|
324
|
+
self.connections = self.ConnectionManager(config, mp_context)
|
|
325
|
+
self._macro_resolver: Optional[MacroResolverProtocol] = None
|
|
326
|
+
self._macro_context_generator: Optional[MacroContextGeneratorCallable] = None
|
|
327
|
+
self.behavior = DEFAULT_BASE_BEHAVIOR_FLAGS # type: ignore
|
|
328
|
+
self._catalog_client = CatalogIntegrationClient(self.CATALOG_INTEGRATIONS)
|
|
329
|
+
|
|
330
|
+
def add_catalog_integration(
|
|
331
|
+
self, catalog_integration: CatalogIntegrationConfig
|
|
332
|
+
) -> CatalogIntegration:
|
|
333
|
+
return self._catalog_client.add(catalog_integration)
|
|
334
|
+
|
|
335
|
+
@available
|
|
336
|
+
def get_catalog_integration(self, name: str) -> CatalogIntegration:
|
|
337
|
+
return self._catalog_client.get(name)
|
|
338
|
+
|
|
339
|
+
@available
|
|
340
|
+
def build_catalog_relation(self, config: RelationConfig) -> Optional[CatalogRelation]:
|
|
341
|
+
if not config.config:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
# "catalog" is legacy, but we support it for backward compatibility
|
|
345
|
+
if catalog_name := config.config.get(
|
|
346
|
+
CATALOG_INTEGRATION_MODEL_CONFIG_NAME
|
|
347
|
+
) or config.config.get("catalog"):
|
|
348
|
+
catalog = self.get_catalog_integration(catalog_name)
|
|
349
|
+
return catalog.build_relation(config)
|
|
350
|
+
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
###
|
|
354
|
+
# Methods to set / access a macro resolver
|
|
355
|
+
###
|
|
356
|
+
def set_macro_resolver(self, macro_resolver: MacroResolverProtocol) -> None:
|
|
357
|
+
self._macro_resolver = macro_resolver
|
|
358
|
+
|
|
359
|
+
def get_macro_resolver(self) -> Optional[MacroResolverProtocol]:
|
|
360
|
+
return self._macro_resolver
|
|
361
|
+
|
|
362
|
+
def clear_macro_resolver(self) -> None:
|
|
363
|
+
if self._macro_resolver is not None:
|
|
364
|
+
self._macro_resolver = None
|
|
365
|
+
|
|
366
|
+
def set_macro_context_generator(
|
|
367
|
+
self,
|
|
368
|
+
macro_context_generator: MacroContextGeneratorCallable,
|
|
369
|
+
) -> None:
|
|
370
|
+
self._macro_context_generator = macro_context_generator
|
|
371
|
+
|
|
372
|
+
@available_property
|
|
373
|
+
def behavior(self) -> Behavior:
|
|
374
|
+
return self._behavior
|
|
375
|
+
|
|
376
|
+
@behavior.setter # type: ignore
|
|
377
|
+
def behavior(self, flags: List[BehaviorFlag]) -> None:
|
|
378
|
+
flags.extend(self._behavior_flags)
|
|
379
|
+
|
|
380
|
+
# we don't always get project flags, for example, the project file is not loaded during `dbt debug`
|
|
381
|
+
# in that case, load the default values for behavior flags to avoid compilation errors
|
|
382
|
+
# this mimics not loading a project file, or not specifying flags in a project file
|
|
383
|
+
user_overrides = getattr(self.config, "flags", {})
|
|
384
|
+
|
|
385
|
+
self._behavior = Behavior(flags, user_overrides)
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def _behavior_flags(self) -> List[BehaviorFlag]:
|
|
389
|
+
"""
|
|
390
|
+
This method should be overwritten by adapter maintainers to provide platform-specific flags
|
|
391
|
+
|
|
392
|
+
The BaseAdapter should NOT include any global flags here as those should be defined via DEFAULT_BASE_BEHAVIOR_FLAGS
|
|
393
|
+
"""
|
|
394
|
+
return []
|
|
395
|
+
|
|
396
|
+
###
|
|
397
|
+
# Methods that pass through to the connection manager
|
|
398
|
+
###
|
|
399
|
+
def acquire_connection(self, name=None) -> Connection:
|
|
400
|
+
return self.connections.set_connection_name(name)
|
|
401
|
+
|
|
402
|
+
def release_connection(self) -> None:
|
|
403
|
+
self.connections.release()
|
|
404
|
+
|
|
405
|
+
def cleanup_connections(self) -> None:
|
|
406
|
+
self.connections.cleanup_all()
|
|
407
|
+
|
|
408
|
+
def clear_transaction(self) -> None:
|
|
409
|
+
self.connections.clear_transaction()
|
|
410
|
+
|
|
411
|
+
def commit_if_has_connection(self) -> None:
|
|
412
|
+
self.connections.commit_if_has_connection()
|
|
413
|
+
|
|
414
|
+
def debug_query(self) -> None:
|
|
415
|
+
self.execute("select 1 as id")
|
|
416
|
+
|
|
417
|
+
def nice_connection_name(self) -> str:
|
|
418
|
+
conn = self.connections.get_if_exists()
|
|
419
|
+
if conn is None or conn.name is None:
|
|
420
|
+
return "<None>"
|
|
421
|
+
return conn.name
|
|
422
|
+
|
|
423
|
+
@contextmanager
|
|
424
|
+
def connection_named(
|
|
425
|
+
self, name: str, query_header_context: Any = None, should_release_connection=True
|
|
426
|
+
) -> Iterator[None]:
|
|
427
|
+
try:
|
|
428
|
+
if self.connections.query_header is not None:
|
|
429
|
+
self.connections.query_header.set(name, query_header_context)
|
|
430
|
+
self.acquire_connection(name)
|
|
431
|
+
yield
|
|
432
|
+
finally:
|
|
433
|
+
if should_release_connection:
|
|
434
|
+
self.release_connection()
|
|
435
|
+
|
|
436
|
+
if self.connections.query_header is not None:
|
|
437
|
+
self.connections.query_header.reset()
|
|
438
|
+
|
|
439
|
+
@available.parse(_parse_callback_empty_table)
|
|
440
|
+
@record_function(
|
|
441
|
+
AdapterExecuteRecord, method=True, index_on_thread_id=True, id_field_name="thread_id"
|
|
442
|
+
)
|
|
443
|
+
def execute(
|
|
444
|
+
self,
|
|
445
|
+
sql: str,
|
|
446
|
+
auto_begin: bool = False,
|
|
447
|
+
fetch: bool = False,
|
|
448
|
+
limit: Optional[int] = None,
|
|
449
|
+
) -> Tuple[AdapterResponse, "agate.Table"]:
|
|
450
|
+
"""Execute the given SQL. This is a thin wrapper around
|
|
451
|
+
ConnectionManager.execute.
|
|
452
|
+
|
|
453
|
+
:param str sql: The sql to execute.
|
|
454
|
+
:param bool auto_begin: If set, and dbt is not currently inside a
|
|
455
|
+
transaction, automatically begin one.
|
|
456
|
+
:param bool fetch: If set, fetch results.
|
|
457
|
+
:param Optional[int] limit: If set, only fetch n number of rows
|
|
458
|
+
:return: A tuple of the query status and results (empty if fetch=False).
|
|
459
|
+
:rtype: Tuple[AdapterResponse, "agate.Table"]
|
|
460
|
+
"""
|
|
461
|
+
return self.connections.execute(sql=sql, auto_begin=auto_begin, fetch=fetch, limit=limit)
|
|
462
|
+
|
|
463
|
+
def validate_sql(self, sql: str) -> AdapterResponse:
|
|
464
|
+
"""Submit the given SQL to the engine for validation, but not execution.
|
|
465
|
+
|
|
466
|
+
This should throw an appropriate exception if the input SQL is invalid, although
|
|
467
|
+
in practice that will generally be handled by delegating to an existing method
|
|
468
|
+
for execution and allowing the error handler to take care of the rest.
|
|
469
|
+
|
|
470
|
+
:param str sql: The sql to validate
|
|
471
|
+
"""
|
|
472
|
+
raise NotImplementedError("`validate_sql` is not implemented for this adapter!")
|
|
473
|
+
|
|
474
|
+
@auto_record_function("AdapterGetColumnSchemaFromQuery", group="Available")
|
|
475
|
+
@available.parse(lambda *a, **k: [])
|
|
476
|
+
def get_column_schema_from_query(self, sql: str) -> List[BaseColumn]:
|
|
477
|
+
"""Get a list of the Columns with names and data types from the given sql."""
|
|
478
|
+
_, cursor = self.connections.add_select_query(sql)
|
|
479
|
+
columns = [
|
|
480
|
+
self.Column.create(
|
|
481
|
+
column_name, self.connections.data_type_code_to_name(column_type_code)
|
|
482
|
+
)
|
|
483
|
+
# https://peps.python.org/pep-0249/#description
|
|
484
|
+
for column_name, column_type_code, *_ in cursor.description
|
|
485
|
+
]
|
|
486
|
+
return columns
|
|
487
|
+
|
|
488
|
+
@record_function(
|
|
489
|
+
AdapterGetPartitionsMetadataRecord,
|
|
490
|
+
method=True,
|
|
491
|
+
index_on_thread_id=True,
|
|
492
|
+
id_field_name="thread_id",
|
|
493
|
+
)
|
|
494
|
+
@available.parse(_parse_callback_empty_table)
|
|
495
|
+
def get_partitions_metadata(self, table: str) -> Tuple["agate.Table"]:
|
|
496
|
+
"""
|
|
497
|
+
TODO: Can we move this to dbt-bigquery?
|
|
498
|
+
Obtain partitions metadata for a BigQuery partitioned table.
|
|
499
|
+
|
|
500
|
+
:param str table: a partitioned table id, in standard SQL format.
|
|
501
|
+
:return: a partition metadata tuple, as described in
|
|
502
|
+
https://cloud.google.com/bigquery/docs/creating-partitioned-tables#getting_partition_metadata_using_meta_tables.
|
|
503
|
+
:rtype: "agate.Table"
|
|
504
|
+
"""
|
|
505
|
+
if hasattr(self.connections, "get_partitions_metadata"):
|
|
506
|
+
return self.connections.get_partitions_metadata(table=table)
|
|
507
|
+
else:
|
|
508
|
+
raise NotImplementedError(
|
|
509
|
+
"`get_partitions_metadata` is not implemented for this adapter!"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
###
|
|
513
|
+
# Methods that should never be overridden
|
|
514
|
+
###
|
|
515
|
+
@classmethod
|
|
516
|
+
def type(cls) -> str:
|
|
517
|
+
"""Get the type of this adapter. Types must be class-unique and
|
|
518
|
+
consistent.
|
|
519
|
+
|
|
520
|
+
:return: The type name
|
|
521
|
+
:rtype: str
|
|
522
|
+
"""
|
|
523
|
+
return cls.ConnectionManager.TYPE
|
|
524
|
+
|
|
525
|
+
# Caching methods
|
|
526
|
+
###
|
|
527
|
+
def _schema_is_cached(self, database: Optional[str], schema: str) -> bool:
|
|
528
|
+
"""Check if the schema is cached, and by default logs if it is not."""
|
|
529
|
+
|
|
530
|
+
if (database, schema) not in self.cache:
|
|
531
|
+
fire_event(
|
|
532
|
+
CacheMiss(
|
|
533
|
+
conn_name=self.nice_connection_name(),
|
|
534
|
+
database=cast_to_str(database),
|
|
535
|
+
schema=schema,
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
return False
|
|
539
|
+
else:
|
|
540
|
+
return True
|
|
541
|
+
|
|
542
|
+
def _get_cache_schemas(self, relation_configs: Iterable[RelationConfig]) -> Set[BaseRelation]:
|
|
543
|
+
"""Get the set of schema relations that the cache logic needs to
|
|
544
|
+
populate.
|
|
545
|
+
"""
|
|
546
|
+
return {
|
|
547
|
+
self.Relation.create_from(
|
|
548
|
+
quoting=self.config, relation_config=relation_config
|
|
549
|
+
).without_identifier()
|
|
550
|
+
for relation_config in relation_configs
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
def _get_catalog_schemas(self, relation_configs: Iterable[RelationConfig]) -> SchemaSearchMap:
|
|
554
|
+
"""Get a mapping of each node's "information_schema" relations to a
|
|
555
|
+
set of all schemas expected in that information_schema.
|
|
556
|
+
|
|
557
|
+
There may be keys that are technically duplicates on the database side,
|
|
558
|
+
for example all of '"foo", 'foo', '"FOO"' and 'FOO' could coexist as
|
|
559
|
+
databases, and values could overlap as appropriate. All values are
|
|
560
|
+
lowercase strings.
|
|
561
|
+
"""
|
|
562
|
+
info_schema_name_map = SchemaSearchMap()
|
|
563
|
+
relations = self._get_catalog_relations(relation_configs)
|
|
564
|
+
for relation in relations:
|
|
565
|
+
info_schema_name_map.add(relation)
|
|
566
|
+
# result is a map whose keys are information_schema Relations without
|
|
567
|
+
# identifiers that have appropriate database prefixes, and whose values
|
|
568
|
+
# are sets of lowercase schema names that are valid members of those
|
|
569
|
+
# databases
|
|
570
|
+
return info_schema_name_map
|
|
571
|
+
|
|
572
|
+
def _get_catalog_relations_by_info_schema(
|
|
573
|
+
self, relations
|
|
574
|
+
) -> Dict[InformationSchema, List[BaseRelation]]:
|
|
575
|
+
relations_by_info_schema: Dict[InformationSchema, List[BaseRelation]] = dict()
|
|
576
|
+
for relation in relations:
|
|
577
|
+
info_schema = relation.information_schema_only()
|
|
578
|
+
if info_schema not in relations_by_info_schema:
|
|
579
|
+
relations_by_info_schema[info_schema] = []
|
|
580
|
+
relations_by_info_schema[info_schema].append(relation)
|
|
581
|
+
|
|
582
|
+
return relations_by_info_schema
|
|
583
|
+
|
|
584
|
+
def _get_catalog_relations(
|
|
585
|
+
self, relation_configs: Iterable[RelationConfig]
|
|
586
|
+
) -> List[BaseRelation]:
|
|
587
|
+
relations = [
|
|
588
|
+
self.Relation.create_from(quoting=self.config, relation_config=relation_config)
|
|
589
|
+
for relation_config in relation_configs
|
|
590
|
+
]
|
|
591
|
+
return relations
|
|
592
|
+
|
|
593
|
+
def _relations_cache_for_schemas(
|
|
594
|
+
self,
|
|
595
|
+
relation_configs: Iterable[RelationConfig],
|
|
596
|
+
cache_schemas: Optional[Set[BaseRelation]] = None,
|
|
597
|
+
) -> None:
|
|
598
|
+
"""Populate the relations cache for the given schemas. Returns an
|
|
599
|
+
iterable of the schemas populated, as strings.
|
|
600
|
+
"""
|
|
601
|
+
if not cache_schemas:
|
|
602
|
+
cache_schemas = self._get_cache_schemas(relation_configs)
|
|
603
|
+
with executor(self.config) as tpe:
|
|
604
|
+
futures: List[Future[List[BaseRelation]]] = []
|
|
605
|
+
for cache_schema in cache_schemas:
|
|
606
|
+
fut = tpe.submit_connected(
|
|
607
|
+
self,
|
|
608
|
+
f"list_{cache_schema.database}_{cache_schema.schema}",
|
|
609
|
+
self.list_relations_without_caching,
|
|
610
|
+
cache_schema,
|
|
611
|
+
)
|
|
612
|
+
futures.append(fut)
|
|
613
|
+
|
|
614
|
+
for future in as_completed(futures):
|
|
615
|
+
# if we can't read the relations we need to just raise anyway,
|
|
616
|
+
# so just call future.result() and let that raise on failure
|
|
617
|
+
for relation in future.result():
|
|
618
|
+
self.cache.add(relation)
|
|
619
|
+
|
|
620
|
+
# it's possible that there were no relations in some schemas. We want
|
|
621
|
+
# to insert the schemas we query into the cache's `.schemas` attribute
|
|
622
|
+
# so we can check it later
|
|
623
|
+
cache_update: Set[Tuple[Optional[str], str]] = set()
|
|
624
|
+
for relation in cache_schemas:
|
|
625
|
+
if relation.schema:
|
|
626
|
+
cache_update.add((relation.database, relation.schema))
|
|
627
|
+
self.cache.update_schemas(cache_update)
|
|
628
|
+
|
|
629
|
+
def set_relations_cache(
|
|
630
|
+
self,
|
|
631
|
+
relation_configs: Iterable[RelationConfig],
|
|
632
|
+
clear: bool = False,
|
|
633
|
+
required_schemas: Optional[Set[BaseRelation]] = None,
|
|
634
|
+
) -> None:
|
|
635
|
+
"""Run a query that gets a populated cache of the relations in the
|
|
636
|
+
database and set the cache on this adapter.
|
|
637
|
+
"""
|
|
638
|
+
with self.cache.lock:
|
|
639
|
+
if clear:
|
|
640
|
+
self.cache.clear()
|
|
641
|
+
self._relations_cache_for_schemas(relation_configs, required_schemas)
|
|
642
|
+
|
|
643
|
+
@auto_record_function("AdapterCacheAdded", group="Available")
|
|
644
|
+
@available
|
|
645
|
+
def cache_added(self, relation: Optional[BaseRelation]) -> str:
|
|
646
|
+
"""Cache a new relation in dbt. It will show up in `list relations`."""
|
|
647
|
+
if relation is None:
|
|
648
|
+
name = self.nice_connection_name()
|
|
649
|
+
raise NullRelationCacheAttemptedError(name)
|
|
650
|
+
self.cache.add(relation)
|
|
651
|
+
# so jinja doesn't render things
|
|
652
|
+
return ""
|
|
653
|
+
|
|
654
|
+
@auto_record_function("AdapterCacheDropped", group="Available")
|
|
655
|
+
@available
|
|
656
|
+
def cache_dropped(self, relation: Optional[BaseRelation]) -> str:
|
|
657
|
+
"""Drop a relation in dbt. It will no longer show up in
|
|
658
|
+
`list relations`, and any bound views will be dropped from the cache
|
|
659
|
+
"""
|
|
660
|
+
if relation is None:
|
|
661
|
+
name = self.nice_connection_name()
|
|
662
|
+
raise NullRelationDropAttemptedError(name)
|
|
663
|
+
self.cache.drop(relation)
|
|
664
|
+
return ""
|
|
665
|
+
|
|
666
|
+
@auto_record_function("AdapterCacheRenamed", group="Available")
|
|
667
|
+
@available
|
|
668
|
+
def cache_renamed(
|
|
669
|
+
self,
|
|
670
|
+
from_relation: Optional[BaseRelation],
|
|
671
|
+
to_relation: Optional[BaseRelation],
|
|
672
|
+
) -> str:
|
|
673
|
+
"""Rename a relation in dbt. It will show up with a new name in
|
|
674
|
+
`list_relations`, but bound views will remain bound.
|
|
675
|
+
"""
|
|
676
|
+
if from_relation is None or to_relation is None:
|
|
677
|
+
name = self.nice_connection_name()
|
|
678
|
+
src_name = _relation_name(from_relation)
|
|
679
|
+
dst_name = _relation_name(to_relation)
|
|
680
|
+
raise RenameToNoneAttemptedError(src_name, dst_name, name)
|
|
681
|
+
|
|
682
|
+
self.cache.rename(from_relation, to_relation)
|
|
683
|
+
return ""
|
|
684
|
+
|
|
685
|
+
###
|
|
686
|
+
# Abstract methods for database-specific values, attributes, and types
|
|
687
|
+
###
|
|
688
|
+
@classmethod
|
|
689
|
+
@abc.abstractmethod
|
|
690
|
+
def date_function(cls) -> str:
|
|
691
|
+
"""Get the date function used by this adapter's database."""
|
|
692
|
+
raise NotImplementedError("`date_function` is not implemented for this adapter!")
|
|
693
|
+
|
|
694
|
+
@classmethod
|
|
695
|
+
@abc.abstractmethod
|
|
696
|
+
def is_cancelable(cls) -> bool:
|
|
697
|
+
raise NotImplementedError("`is_cancelable` is not implemented for this adapter!")
|
|
698
|
+
|
|
699
|
+
###
|
|
700
|
+
# Abstract methods about schemas
|
|
701
|
+
###
|
|
702
|
+
@abc.abstractmethod
|
|
703
|
+
def list_schemas(self, database: str) -> List[str]:
|
|
704
|
+
"""Get a list of existing schemas in database"""
|
|
705
|
+
raise NotImplementedError("`list_schemas` is not implemented for this adapter!")
|
|
706
|
+
|
|
707
|
+
@auto_record_function("AdapterCheckSchemaExists", group="Available")
|
|
708
|
+
@available.parse(lambda *a, **k: False)
|
|
709
|
+
def check_schema_exists(self, database: str, schema: str) -> bool:
|
|
710
|
+
"""Check if a schema exists.
|
|
711
|
+
|
|
712
|
+
The default implementation of this is potentially unnecessarily slow,
|
|
713
|
+
and adapters should implement it if there is an optimized path (and
|
|
714
|
+
there probably is)
|
|
715
|
+
"""
|
|
716
|
+
search = (s.lower() for s in self.list_schemas(database=database))
|
|
717
|
+
return schema.lower() in search
|
|
718
|
+
|
|
719
|
+
###
|
|
720
|
+
# Abstract methods about relations
|
|
721
|
+
###
|
|
722
|
+
@auto_record_function("AdapterDropRelation", group="Available")
|
|
723
|
+
@abc.abstractmethod
|
|
724
|
+
@available.parse_none
|
|
725
|
+
def drop_relation(self, relation: BaseRelation) -> None:
|
|
726
|
+
"""Drop the given relation.
|
|
727
|
+
|
|
728
|
+
*Implementors must call self.cache.drop() to preserve cache state!*
|
|
729
|
+
"""
|
|
730
|
+
raise NotImplementedError("`drop_relation` is not implemented for this adapter!")
|
|
731
|
+
|
|
732
|
+
@auto_record_function("AdapterTruncateRelation", group="Available")
|
|
733
|
+
@abc.abstractmethod
|
|
734
|
+
@available.parse_none
|
|
735
|
+
def truncate_relation(self, relation: BaseRelation) -> None:
|
|
736
|
+
"""Truncate the given relation."""
|
|
737
|
+
raise NotImplementedError("`truncate_relation` is not implemented for this adapter!")
|
|
738
|
+
|
|
739
|
+
@auto_record_function("AdapterRenameRelation", group="Available")
|
|
740
|
+
@abc.abstractmethod
|
|
741
|
+
@available.parse_none
|
|
742
|
+
def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None:
|
|
743
|
+
"""Rename the relation from from_relation to to_relation.
|
|
744
|
+
|
|
745
|
+
Implementors must call self.cache.rename() to preserve cache state.
|
|
746
|
+
"""
|
|
747
|
+
raise NotImplementedError("`rename_relation` is not implemented for this adapter!")
|
|
748
|
+
|
|
749
|
+
@record_function(
|
|
750
|
+
AdapterGetColumnsInRelationRecord,
|
|
751
|
+
method=True,
|
|
752
|
+
index_on_thread_id=True,
|
|
753
|
+
id_field_name="thread_id",
|
|
754
|
+
)
|
|
755
|
+
@abc.abstractmethod
|
|
756
|
+
@available.parse_list
|
|
757
|
+
def get_columns_in_relation(self, relation: BaseRelation) -> List[BaseColumn]:
|
|
758
|
+
"""Get a list of the columns in the given Relation."""
|
|
759
|
+
raise NotImplementedError("`get_columns_in_relation` is not implemented for this adapter!")
|
|
760
|
+
|
|
761
|
+
def get_catalog_for_single_relation(self, relation: BaseRelation) -> Optional[CatalogTable]:
|
|
762
|
+
"""Get catalog information including table-level and column-level metadata for a single relation."""
|
|
763
|
+
raise NotImplementedError(
|
|
764
|
+
"`get_catalog_for_single_relation` is not implemented for this adapter!"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
@auto_record_function("AdapterGetColumnsInTable", group="Available")
|
|
768
|
+
@available.deprecated("get_columns_in_relation", lambda *a, **k: [])
|
|
769
|
+
def get_columns_in_table(self, schema: str, identifier: str) -> List[BaseColumn]:
|
|
770
|
+
"""DEPRECATED: Get a list of the columns in the given table."""
|
|
771
|
+
relation = self.Relation.create(
|
|
772
|
+
database=self.config.credentials.database,
|
|
773
|
+
schema=schema,
|
|
774
|
+
identifier=identifier,
|
|
775
|
+
quote_policy=self.config.quoting,
|
|
776
|
+
)
|
|
777
|
+
return self.get_columns_in_relation(relation)
|
|
778
|
+
|
|
779
|
+
@abc.abstractmethod
|
|
780
|
+
def expand_column_types(self, goal: BaseRelation, current: BaseRelation) -> None:
|
|
781
|
+
"""Expand the current table's types to match the goal table. (passable)
|
|
782
|
+
|
|
783
|
+
:param self.Relation goal: A relation that currently exists in the
|
|
784
|
+
database with columns of the desired types.
|
|
785
|
+
:param self.Relation current: A relation that currently exists in the
|
|
786
|
+
database with columns of unspecified types.
|
|
787
|
+
"""
|
|
788
|
+
raise NotImplementedError(
|
|
789
|
+
"`expand_target_column_types` is not implemented for this adapter!"
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
@record_function(
|
|
793
|
+
AdapterListRelationsWithoutCachingRecord,
|
|
794
|
+
method=True,
|
|
795
|
+
index_on_thread_id=True,
|
|
796
|
+
id_field_name="thread_id",
|
|
797
|
+
)
|
|
798
|
+
@abc.abstractmethod
|
|
799
|
+
def list_relations_without_caching(self, schema_relation: BaseRelation) -> List[BaseRelation]:
|
|
800
|
+
"""List relations in the given schema, bypassing the cache.
|
|
801
|
+
|
|
802
|
+
This is used as the underlying behavior to fill the cache.
|
|
803
|
+
|
|
804
|
+
:param schema_relation: A relation containing the database and schema
|
|
805
|
+
as appropraite for the underlying data warehouse
|
|
806
|
+
:return: The relations in schema
|
|
807
|
+
:rtype: List[self.Relation]
|
|
808
|
+
"""
|
|
809
|
+
raise NotImplementedError(
|
|
810
|
+
"`list_relations_without_caching` is not implemented for this adapter!"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
###
|
|
814
|
+
# Methods about grants
|
|
815
|
+
###
|
|
816
|
+
@record_function(
|
|
817
|
+
AdapterStandardizeGrantsDictRecord,
|
|
818
|
+
method=True,
|
|
819
|
+
index_on_thread_id=True,
|
|
820
|
+
id_field_name="thread_id",
|
|
821
|
+
)
|
|
822
|
+
@available
|
|
823
|
+
def standardize_grants_dict(self, grants_table: "agate.Table") -> dict:
|
|
824
|
+
"""Translate the result of `show grants` (or equivalent) to match the
|
|
825
|
+
grants which a user would configure in their project.
|
|
826
|
+
|
|
827
|
+
Ideally, the SQL to show grants should also be filtering:
|
|
828
|
+
filter OUT any grants TO the current user/role (e.g. OWNERSHIP).
|
|
829
|
+
If that's not possible in SQL, it can be done in this method instead.
|
|
830
|
+
|
|
831
|
+
:param grants_table: An agate table containing the query result of
|
|
832
|
+
the SQL returned by get_show_grant_sql
|
|
833
|
+
:return: A standardized dictionary matching the `grants` config
|
|
834
|
+
:rtype: dict
|
|
835
|
+
"""
|
|
836
|
+
|
|
837
|
+
grants_dict: Dict[str, List[str]] = {}
|
|
838
|
+
for row in grants_table:
|
|
839
|
+
grantee = row["grantee"]
|
|
840
|
+
privilege = row["privilege_type"]
|
|
841
|
+
if privilege in grants_dict.keys():
|
|
842
|
+
grants_dict[privilege].append(grantee)
|
|
843
|
+
else:
|
|
844
|
+
grants_dict.update({privilege: [grantee]})
|
|
845
|
+
return grants_dict
|
|
846
|
+
|
|
847
|
+
###
|
|
848
|
+
# Provided methods about relations
|
|
849
|
+
###
|
|
850
|
+
@auto_record_function("AdapterGetMissingColumns", group="Available")
|
|
851
|
+
@available.parse_list
|
|
852
|
+
def get_missing_columns(
|
|
853
|
+
self, from_relation: BaseRelation, to_relation: BaseRelation
|
|
854
|
+
) -> List[BaseColumn]:
|
|
855
|
+
"""Returns a list of Columns in from_relation that are missing from
|
|
856
|
+
to_relation.
|
|
857
|
+
"""
|
|
858
|
+
if not isinstance(from_relation, self.Relation):
|
|
859
|
+
raise MacroArgTypeError(
|
|
860
|
+
method_name="get_missing_columns",
|
|
861
|
+
arg_name="from_relation",
|
|
862
|
+
got_value=from_relation,
|
|
863
|
+
expected_type=self.Relation,
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
if not isinstance(to_relation, self.Relation):
|
|
867
|
+
raise MacroArgTypeError(
|
|
868
|
+
method_name="get_missing_columns",
|
|
869
|
+
arg_name="to_relation",
|
|
870
|
+
got_value=to_relation,
|
|
871
|
+
expected_type=self.Relation,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
from_columns = {col.name: col for col in self.get_columns_in_relation(from_relation)}
|
|
875
|
+
|
|
876
|
+
to_columns = {col.name: col for col in self.get_columns_in_relation(to_relation)}
|
|
877
|
+
|
|
878
|
+
missing_columns = set(from_columns.keys()) - set(to_columns.keys())
|
|
879
|
+
|
|
880
|
+
return [col for (col_name, col) in from_columns.items() if col_name in missing_columns]
|
|
881
|
+
|
|
882
|
+
@auto_record_function("AdapterValidSnapshotTarget", group="Available")
|
|
883
|
+
@available.parse_none
|
|
884
|
+
def valid_snapshot_target(
|
|
885
|
+
self, relation: BaseRelation, column_names: Optional[Dict[str, str]] = None
|
|
886
|
+
) -> None:
|
|
887
|
+
"""Ensure that the target relation is valid, by making sure it has the
|
|
888
|
+
expected columns.
|
|
889
|
+
|
|
890
|
+
:param Relation relation: The relation to check
|
|
891
|
+
:raises InvalidMacroArgType: If the columns are
|
|
892
|
+
incorrect.
|
|
893
|
+
"""
|
|
894
|
+
if not isinstance(relation, self.Relation):
|
|
895
|
+
raise MacroArgTypeError(
|
|
896
|
+
method_name="valid_snapshot_target",
|
|
897
|
+
arg_name="relation",
|
|
898
|
+
got_value=relation,
|
|
899
|
+
expected_type=self.Relation,
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
columns = self.get_columns_in_relation(relation)
|
|
903
|
+
names = set(c.name.lower() for c in columns)
|
|
904
|
+
missing = []
|
|
905
|
+
# Note: we're not checking dbt_updated_at or dbt_is_deleted here because they
|
|
906
|
+
# aren't always present.
|
|
907
|
+
for column in ("dbt_scd_id", "dbt_valid_from", "dbt_valid_to"):
|
|
908
|
+
desired = column_names[column] if column_names else column
|
|
909
|
+
if desired and desired.lower() not in names:
|
|
910
|
+
missing.append(desired)
|
|
911
|
+
|
|
912
|
+
if missing:
|
|
913
|
+
raise SnapshotTargetNotSnapshotTableError(missing)
|
|
914
|
+
|
|
915
|
+
@auto_record_function("AdapterAssertValidSnapshotTargetGivenStrategy", group="Available")
|
|
916
|
+
@available.parse_none
|
|
917
|
+
def assert_valid_snapshot_target_given_strategy(
|
|
918
|
+
self, relation: BaseRelation, column_names: Dict[str, str], strategy: SnapshotStrategy
|
|
919
|
+
) -> None:
|
|
920
|
+
|
|
921
|
+
# Assert everything we can with the legacy function.
|
|
922
|
+
self.valid_snapshot_target(relation, column_names)
|
|
923
|
+
|
|
924
|
+
# Now do strategy-specific checks.
|
|
925
|
+
# TODO: Make these checks more comprehensive.
|
|
926
|
+
if strategy.get("hard_deletes", None) == "new_record":
|
|
927
|
+
columns = self.get_columns_in_relation(relation)
|
|
928
|
+
names = set(c.name.lower() for c in columns)
|
|
929
|
+
missing = []
|
|
930
|
+
|
|
931
|
+
for column in ("dbt_is_deleted",):
|
|
932
|
+
desired = column_names[column] if column_names else column
|
|
933
|
+
if desired not in names:
|
|
934
|
+
missing.append(desired)
|
|
935
|
+
|
|
936
|
+
if missing:
|
|
937
|
+
raise SnapshotTargetNotSnapshotTableError(missing)
|
|
938
|
+
|
|
939
|
+
@auto_record_function("AdapterExpandTargetColumnTypes", group="Available")
|
|
940
|
+
@available.parse_none
|
|
941
|
+
def expand_target_column_types(
|
|
942
|
+
self, from_relation: BaseRelation, to_relation: BaseRelation
|
|
943
|
+
) -> None:
|
|
944
|
+
|
|
945
|
+
if not isinstance(from_relation, self.Relation):
|
|
946
|
+
raise MacroArgTypeError(
|
|
947
|
+
method_name="expand_target_column_types",
|
|
948
|
+
arg_name="from_relation",
|
|
949
|
+
got_value=from_relation,
|
|
950
|
+
expected_type=self.Relation,
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
if not isinstance(to_relation, self.Relation):
|
|
954
|
+
raise MacroArgTypeError(
|
|
955
|
+
method_name="expand_target_column_types",
|
|
956
|
+
arg_name="to_relation",
|
|
957
|
+
got_value=to_relation,
|
|
958
|
+
expected_type=self.Relation,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
self.expand_column_types(from_relation, to_relation)
|
|
962
|
+
|
|
963
|
+
def list_relations(self, database: Optional[str], schema: str) -> List[BaseRelation]:
|
|
964
|
+
if self._schema_is_cached(database, schema):
|
|
965
|
+
return self.cache.get_relations(database, schema)
|
|
966
|
+
|
|
967
|
+
schema_relation = self.Relation.create(
|
|
968
|
+
database=database,
|
|
969
|
+
schema=schema,
|
|
970
|
+
identifier="",
|
|
971
|
+
quote_policy=self.config.quoting,
|
|
972
|
+
).without_identifier()
|
|
973
|
+
|
|
974
|
+
# we can't build the relations cache because we don't have a
|
|
975
|
+
# manifest so we can't run any operations.
|
|
976
|
+
relations = self.list_relations_without_caching(schema_relation)
|
|
977
|
+
|
|
978
|
+
# if the cache is already populated, add this schema in
|
|
979
|
+
# otherwise, skip updating the cache and just ignore
|
|
980
|
+
if self.cache:
|
|
981
|
+
for relation in relations:
|
|
982
|
+
self.cache.add(relation)
|
|
983
|
+
if not relations:
|
|
984
|
+
# it's possible that there were no relations in some schemas. We want
|
|
985
|
+
# to insert the schemas we query into the cache's `.schemas` attribute
|
|
986
|
+
# so we can check it later
|
|
987
|
+
self.cache.update_schemas([(database, schema)])
|
|
988
|
+
|
|
989
|
+
fire_event(
|
|
990
|
+
ListRelations(
|
|
991
|
+
database=cast_to_str(database),
|
|
992
|
+
schema=schema,
|
|
993
|
+
relations=[_make_ref_key_dict(x) for x in relations],
|
|
994
|
+
)
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
return relations
|
|
998
|
+
|
|
999
|
+
def _make_match_kwargs(self, database: str, schema: str, identifier: str) -> Dict[str, str]:
|
|
1000
|
+
quoting = self.config.quoting
|
|
1001
|
+
if identifier is not None and quoting["identifier"] is False:
|
|
1002
|
+
identifier = identifier.lower()
|
|
1003
|
+
|
|
1004
|
+
if schema is not None and quoting["schema"] is False:
|
|
1005
|
+
schema = schema.lower()
|
|
1006
|
+
|
|
1007
|
+
if database is not None and quoting["database"] is False:
|
|
1008
|
+
database = database.lower()
|
|
1009
|
+
|
|
1010
|
+
return filter_null_values(
|
|
1011
|
+
{
|
|
1012
|
+
"database": database,
|
|
1013
|
+
"identifier": identifier,
|
|
1014
|
+
"schema": schema,
|
|
1015
|
+
}
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
def _make_match(
|
|
1019
|
+
self,
|
|
1020
|
+
relations_list: List[BaseRelation],
|
|
1021
|
+
database: str,
|
|
1022
|
+
schema: str,
|
|
1023
|
+
identifier: str,
|
|
1024
|
+
) -> List[BaseRelation]:
|
|
1025
|
+
matches = []
|
|
1026
|
+
|
|
1027
|
+
search = self._make_match_kwargs(database, schema, identifier)
|
|
1028
|
+
|
|
1029
|
+
for relation in relations_list:
|
|
1030
|
+
if relation.matches(**search):
|
|
1031
|
+
matches.append(relation)
|
|
1032
|
+
|
|
1033
|
+
return matches
|
|
1034
|
+
|
|
1035
|
+
@auto_record_function("AdapterGetRelation", group="Available")
|
|
1036
|
+
@available.parse_none
|
|
1037
|
+
def get_relation(self, database: str, schema: str, identifier: str) -> Optional[BaseRelation]:
|
|
1038
|
+
|
|
1039
|
+
relations_list = self.list_relations(database, schema)
|
|
1040
|
+
|
|
1041
|
+
matches = self._make_match(relations_list, database, schema, identifier)
|
|
1042
|
+
|
|
1043
|
+
if len(matches) > 1:
|
|
1044
|
+
kwargs = {
|
|
1045
|
+
"identifier": identifier,
|
|
1046
|
+
"schema": schema,
|
|
1047
|
+
"database": database,
|
|
1048
|
+
}
|
|
1049
|
+
raise RelationReturnedMultipleResultsError(kwargs, matches)
|
|
1050
|
+
|
|
1051
|
+
elif matches:
|
|
1052
|
+
return matches[0]
|
|
1053
|
+
|
|
1054
|
+
return None
|
|
1055
|
+
|
|
1056
|
+
@auto_record_function("AdapterAlreadyExists", group="Available")
|
|
1057
|
+
@available.deprecated("get_relation", lambda *a, **k: False)
|
|
1058
|
+
def already_exists(self, schema: str, name: str) -> bool:
|
|
1059
|
+
"""DEPRECATED: Return if a model already exists in the database"""
|
|
1060
|
+
|
|
1061
|
+
database = self.config.credentials.database
|
|
1062
|
+
relation = self.get_relation(database, schema, name)
|
|
1063
|
+
return relation is not None
|
|
1064
|
+
|
|
1065
|
+
###
|
|
1066
|
+
# ODBC FUNCTIONS -- these should not need to change for every adapter,
|
|
1067
|
+
# although some adapters may override them
|
|
1068
|
+
###
|
|
1069
|
+
@auto_record_function("AdapterCreateSchema", group="Available")
|
|
1070
|
+
@abc.abstractmethod
|
|
1071
|
+
@available.parse_none
|
|
1072
|
+
def create_schema(self, relation: BaseRelation):
|
|
1073
|
+
"""Create the given schema if it does not exist."""
|
|
1074
|
+
raise NotImplementedError("`create_schema` is not implemented for this adapter!")
|
|
1075
|
+
|
|
1076
|
+
@auto_record_function("AdapterDropSchema", group="Available")
|
|
1077
|
+
@abc.abstractmethod
|
|
1078
|
+
@available.parse_none
|
|
1079
|
+
def drop_schema(self, relation: BaseRelation):
|
|
1080
|
+
"""Drop the given schema (and everything in it) if it exists."""
|
|
1081
|
+
raise NotImplementedError("`drop_schema` is not implemented for this adapter!")
|
|
1082
|
+
|
|
1083
|
+
@available
|
|
1084
|
+
@classmethod
|
|
1085
|
+
@auto_record_function("AdapterQuote", group="Available")
|
|
1086
|
+
@abc.abstractmethod
|
|
1087
|
+
def quote(cls, identifier: str) -> str:
|
|
1088
|
+
"""Quote the given identifier, as appropriate for the database."""
|
|
1089
|
+
raise NotImplementedError("`quote` is not implemented for this adapter!")
|
|
1090
|
+
|
|
1091
|
+
@auto_record_function("AdapterQuoteAsConfigured", group="Available")
|
|
1092
|
+
@available
|
|
1093
|
+
def quote_as_configured(self, identifier: str, quote_key: str) -> str:
|
|
1094
|
+
"""Quote or do not quote the given identifer as configured in the
|
|
1095
|
+
project config for the quote key.
|
|
1096
|
+
|
|
1097
|
+
The quote key should be one of 'database' (on bigquery, 'profile'),
|
|
1098
|
+
'identifier', or 'schema', or it will be treated as if you set `True`.
|
|
1099
|
+
"""
|
|
1100
|
+
|
|
1101
|
+
try:
|
|
1102
|
+
key = ComponentName(quote_key)
|
|
1103
|
+
except ValueError:
|
|
1104
|
+
return identifier
|
|
1105
|
+
|
|
1106
|
+
default = self.Relation.get_default_quote_policy().get_part(key)
|
|
1107
|
+
if self.config.quoting.get(key, default):
|
|
1108
|
+
return self.quote(identifier)
|
|
1109
|
+
else:
|
|
1110
|
+
return identifier
|
|
1111
|
+
|
|
1112
|
+
@auto_record_function("AdapterQuoteSeedColumn", group="Available")
|
|
1113
|
+
@available
|
|
1114
|
+
def quote_seed_column(self, column: str, quote_config: Optional[bool]) -> str:
|
|
1115
|
+
|
|
1116
|
+
quote_columns: bool = True
|
|
1117
|
+
if isinstance(quote_config, bool):
|
|
1118
|
+
quote_columns = quote_config
|
|
1119
|
+
elif quote_config is None:
|
|
1120
|
+
pass
|
|
1121
|
+
else:
|
|
1122
|
+
raise QuoteConfigTypeError(quote_config)
|
|
1123
|
+
|
|
1124
|
+
if quote_columns:
|
|
1125
|
+
return self.quote(column)
|
|
1126
|
+
else:
|
|
1127
|
+
return column
|
|
1128
|
+
|
|
1129
|
+
###
|
|
1130
|
+
# Conversions: These must be implemented by concrete implementations, for
|
|
1131
|
+
# converting agate types into their sql equivalents.
|
|
1132
|
+
###
|
|
1133
|
+
@classmethod
|
|
1134
|
+
@abc.abstractmethod
|
|
1135
|
+
def convert_text_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1136
|
+
"""Return the type in the database that best maps to the agate.Text
|
|
1137
|
+
type for the given agate table and column index.
|
|
1138
|
+
|
|
1139
|
+
:param agate_table: The table
|
|
1140
|
+
:param col_idx: The index into the agate table for the column.
|
|
1141
|
+
:return: The name of the type in the database
|
|
1142
|
+
"""
|
|
1143
|
+
raise NotImplementedError("`convert_text_type` is not implemented for this adapter!")
|
|
1144
|
+
|
|
1145
|
+
@classmethod
|
|
1146
|
+
@abc.abstractmethod
|
|
1147
|
+
def convert_number_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1148
|
+
"""Return the type in the database that best maps to the agate.Number
|
|
1149
|
+
type for the given agate table and column index.
|
|
1150
|
+
|
|
1151
|
+
:param agate_table: The table
|
|
1152
|
+
:param col_idx: The index into the agate table for the column.
|
|
1153
|
+
:return: The name of the type in the database
|
|
1154
|
+
"""
|
|
1155
|
+
raise NotImplementedError("`convert_number_type` is not implemented for this adapter!")
|
|
1156
|
+
|
|
1157
|
+
@classmethod
|
|
1158
|
+
def convert_integer_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1159
|
+
"""Return the type in the database that best maps to the agate.Number
|
|
1160
|
+
type for the given agate table and column index.
|
|
1161
|
+
|
|
1162
|
+
:param agate_table: The table
|
|
1163
|
+
:param col_idx: The index into the agate table for the column.
|
|
1164
|
+
:return: The name of the type in the database
|
|
1165
|
+
"""
|
|
1166
|
+
return "integer"
|
|
1167
|
+
|
|
1168
|
+
@classmethod
|
|
1169
|
+
@abc.abstractmethod
|
|
1170
|
+
def convert_boolean_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1171
|
+
"""Return the type in the database that best maps to the agate.Boolean
|
|
1172
|
+
type for the given agate table and column index.
|
|
1173
|
+
|
|
1174
|
+
:param agate_table: The table
|
|
1175
|
+
:param col_idx: The index into the agate table for the column.
|
|
1176
|
+
:return: The name of the type in the database
|
|
1177
|
+
"""
|
|
1178
|
+
raise NotImplementedError("`convert_boolean_type` is not implemented for this adapter!")
|
|
1179
|
+
|
|
1180
|
+
@classmethod
|
|
1181
|
+
@abc.abstractmethod
|
|
1182
|
+
def convert_datetime_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1183
|
+
"""Return the type in the database that best maps to the agate.DateTime
|
|
1184
|
+
type for the given agate table and column index.
|
|
1185
|
+
|
|
1186
|
+
:param agate_table: The table
|
|
1187
|
+
:param col_idx: The index into the agate table for the column.
|
|
1188
|
+
:return: The name of the type in the database
|
|
1189
|
+
"""
|
|
1190
|
+
raise NotImplementedError("`convert_datetime_type` is not implemented for this adapter!")
|
|
1191
|
+
|
|
1192
|
+
@classmethod
|
|
1193
|
+
@abc.abstractmethod
|
|
1194
|
+
def convert_date_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1195
|
+
"""Return the type in the database that best maps to the agate.Date
|
|
1196
|
+
type for the given agate table and column index.
|
|
1197
|
+
|
|
1198
|
+
:param agate_table: The table
|
|
1199
|
+
:param col_idx: The index into the agate table for the column.
|
|
1200
|
+
:return: The name of the type in the database
|
|
1201
|
+
"""
|
|
1202
|
+
raise NotImplementedError("`convert_date_type` is not implemented for this adapter!")
|
|
1203
|
+
|
|
1204
|
+
@classmethod
|
|
1205
|
+
@abc.abstractmethod
|
|
1206
|
+
def convert_time_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
|
|
1207
|
+
"""Return the type in the database that best maps to the
|
|
1208
|
+
agate.TimeDelta type for the given agate table and column index.
|
|
1209
|
+
|
|
1210
|
+
:param agate_table: The table
|
|
1211
|
+
:param col_idx: The index into the agate table for the column.
|
|
1212
|
+
:return: The name of the type in the database
|
|
1213
|
+
"""
|
|
1214
|
+
raise NotImplementedError("`convert_time_type` is not implemented for this adapter!")
|
|
1215
|
+
|
|
1216
|
+
@available
|
|
1217
|
+
@classmethod
|
|
1218
|
+
@record_function(
|
|
1219
|
+
AdapterConvertTypeRecord, method=True, index_on_thread_id=True, id_field_name="thread_id"
|
|
1220
|
+
)
|
|
1221
|
+
def convert_type(cls, agate_table: "agate.Table", col_idx: int) -> Optional[str]:
|
|
1222
|
+
|
|
1223
|
+
return cls.convert_agate_type(agate_table, col_idx)
|
|
1224
|
+
|
|
1225
|
+
@classmethod
|
|
1226
|
+
def convert_agate_type(cls, agate_table: "agate.Table", col_idx: int) -> Optional[str]:
|
|
1227
|
+
import agate
|
|
1228
|
+
from dbt_common.clients.agate_helper import Integer
|
|
1229
|
+
|
|
1230
|
+
agate_type: Type = agate_table.column_types[col_idx]
|
|
1231
|
+
conversions: List[Tuple[Type, Callable[..., str]]] = [
|
|
1232
|
+
(Integer, cls.convert_integer_type),
|
|
1233
|
+
(agate.Text, cls.convert_text_type),
|
|
1234
|
+
(agate.Number, cls.convert_number_type),
|
|
1235
|
+
(agate.Boolean, cls.convert_boolean_type),
|
|
1236
|
+
(agate.DateTime, cls.convert_datetime_type),
|
|
1237
|
+
(agate.Date, cls.convert_date_type),
|
|
1238
|
+
(agate.TimeDelta, cls.convert_time_type),
|
|
1239
|
+
]
|
|
1240
|
+
for agate_cls, func in conversions:
|
|
1241
|
+
if isinstance(agate_type, agate_cls):
|
|
1242
|
+
return func(agate_table, col_idx)
|
|
1243
|
+
|
|
1244
|
+
return None
|
|
1245
|
+
|
|
1246
|
+
###
|
|
1247
|
+
# Operations involving the manifest
|
|
1248
|
+
###
|
|
1249
|
+
def execute_macro(
|
|
1250
|
+
self,
|
|
1251
|
+
macro_name: str,
|
|
1252
|
+
macro_resolver: Optional[MacroResolverProtocol] = None,
|
|
1253
|
+
project: Optional[str] = None,
|
|
1254
|
+
context_override: Optional[Dict[str, Any]] = None,
|
|
1255
|
+
kwargs: Optional[Dict[str, Any]] = None,
|
|
1256
|
+
needs_conn: bool = False,
|
|
1257
|
+
) -> AttrDict:
|
|
1258
|
+
"""Look macro_name up in the manifest and execute its results.
|
|
1259
|
+
|
|
1260
|
+
:param macro_name: The name of the macro to execute.
|
|
1261
|
+
:param manifest: The manifest to use for generating the base macro
|
|
1262
|
+
execution context. If none is provided, use the internal manifest.
|
|
1263
|
+
:param project: The name of the project to search in, or None for the
|
|
1264
|
+
first match.
|
|
1265
|
+
:param context_override: An optional dict to update() the macro
|
|
1266
|
+
execution context.
|
|
1267
|
+
:param kwargs: An optional dict of keyword args used to pass to the
|
|
1268
|
+
macro.
|
|
1269
|
+
: param needs_conn: A boolean that indicates whether the specified macro
|
|
1270
|
+
requires an open connection to execute. If needs_conn is True, a
|
|
1271
|
+
connection is expected and opened if necessary. Otherwise (and by default),
|
|
1272
|
+
no connection is expected prior to executing the macro.
|
|
1273
|
+
"""
|
|
1274
|
+
|
|
1275
|
+
if kwargs is None:
|
|
1276
|
+
kwargs = {}
|
|
1277
|
+
if context_override is None:
|
|
1278
|
+
context_override = {}
|
|
1279
|
+
|
|
1280
|
+
resolver = macro_resolver or self._macro_resolver
|
|
1281
|
+
if resolver is None:
|
|
1282
|
+
raise DbtInternalError("Macro resolver was None when calling execute_macro!")
|
|
1283
|
+
|
|
1284
|
+
if self._macro_context_generator is None:
|
|
1285
|
+
raise DbtInternalError("Macro context generator was None when calling execute_macro!")
|
|
1286
|
+
|
|
1287
|
+
macro = resolver.find_macro_by_name(macro_name, self.config.project_name, project)
|
|
1288
|
+
if macro is None:
|
|
1289
|
+
if project is None:
|
|
1290
|
+
package_name = "any package"
|
|
1291
|
+
else:
|
|
1292
|
+
package_name = 'the "{}" package'.format(project)
|
|
1293
|
+
|
|
1294
|
+
raise DbtRuntimeError(
|
|
1295
|
+
'dbt could not find a macro with the name "{}" in {}'.format(
|
|
1296
|
+
macro_name, package_name
|
|
1297
|
+
)
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
macro_context = self._macro_context_generator(macro, self.config, resolver, project)
|
|
1301
|
+
macro_context.update(context_override)
|
|
1302
|
+
|
|
1303
|
+
macro_function = CallableMacroGenerator(macro, macro_context)
|
|
1304
|
+
|
|
1305
|
+
if needs_conn:
|
|
1306
|
+
connection = self.connections.get_thread_connection()
|
|
1307
|
+
self.connections.open(connection)
|
|
1308
|
+
|
|
1309
|
+
with self.connections.exception_handler(f"macro {macro_name}"):
|
|
1310
|
+
result = macro_function(**kwargs)
|
|
1311
|
+
return result
|
|
1312
|
+
|
|
1313
|
+
@classmethod
|
|
1314
|
+
def _catalog_filter_table(
|
|
1315
|
+
cls, table: "agate.Table", used_schemas: FrozenSet[Tuple[str, str]]
|
|
1316
|
+
) -> "agate.Table":
|
|
1317
|
+
"""Filter the table as appropriate for catalog entries. Subclasses can
|
|
1318
|
+
override this to change filtering rules on a per-adapter basis.
|
|
1319
|
+
"""
|
|
1320
|
+
from dbt_common.clients.agate_helper import table_from_rows
|
|
1321
|
+
|
|
1322
|
+
# force database + schema to be strings
|
|
1323
|
+
table = table_from_rows(
|
|
1324
|
+
table.rows,
|
|
1325
|
+
table.column_names,
|
|
1326
|
+
text_only_columns=[
|
|
1327
|
+
"table_database",
|
|
1328
|
+
"table_schema",
|
|
1329
|
+
"table_name",
|
|
1330
|
+
"table_type",
|
|
1331
|
+
"table_comment",
|
|
1332
|
+
"table_owner",
|
|
1333
|
+
"column_name",
|
|
1334
|
+
"column_type",
|
|
1335
|
+
"column_comment",
|
|
1336
|
+
],
|
|
1337
|
+
)
|
|
1338
|
+
return table.where(_catalog_filter_schemas(used_schemas))
|
|
1339
|
+
|
|
1340
|
+
def _get_one_catalog(
|
|
1341
|
+
self,
|
|
1342
|
+
information_schema: InformationSchema,
|
|
1343
|
+
schemas: Set[str],
|
|
1344
|
+
used_schemas: FrozenSet[Tuple[str, str]],
|
|
1345
|
+
) -> "agate.Table":
|
|
1346
|
+
kwargs = {"information_schema": information_schema, "schemas": schemas}
|
|
1347
|
+
table = self.execute_macro(GET_CATALOG_MACRO_NAME, kwargs=kwargs)
|
|
1348
|
+
|
|
1349
|
+
results = self._catalog_filter_table(table, used_schemas)
|
|
1350
|
+
return results
|
|
1351
|
+
|
|
1352
|
+
def _get_one_catalog_by_relations(
|
|
1353
|
+
self,
|
|
1354
|
+
information_schema: InformationSchema,
|
|
1355
|
+
relations: List[BaseRelation],
|
|
1356
|
+
used_schemas: FrozenSet[Tuple[str, str]],
|
|
1357
|
+
) -> "agate.Table":
|
|
1358
|
+
kwargs = {
|
|
1359
|
+
"information_schema": information_schema,
|
|
1360
|
+
"relations": relations,
|
|
1361
|
+
}
|
|
1362
|
+
table = self.execute_macro(GET_CATALOG_RELATIONS_MACRO_NAME, kwargs=kwargs)
|
|
1363
|
+
|
|
1364
|
+
results = self._catalog_filter_table(table, used_schemas)
|
|
1365
|
+
return results
|
|
1366
|
+
|
|
1367
|
+
def get_filtered_catalog(
|
|
1368
|
+
self,
|
|
1369
|
+
relation_configs: Iterable[RelationConfig],
|
|
1370
|
+
used_schemas: FrozenSet[Tuple[str, str]],
|
|
1371
|
+
relations: Optional[Set[BaseRelation]] = None,
|
|
1372
|
+
):
|
|
1373
|
+
catalogs: "agate.Table"
|
|
1374
|
+
if (
|
|
1375
|
+
relations is None
|
|
1376
|
+
or len(relations) > self.MAX_SCHEMA_METADATA_RELATIONS
|
|
1377
|
+
or not self.supports(Capability.SchemaMetadataByRelations)
|
|
1378
|
+
):
|
|
1379
|
+
# Do it the traditional way. We get the full catalog.
|
|
1380
|
+
catalogs, exceptions = self.get_catalog(relation_configs, used_schemas)
|
|
1381
|
+
else:
|
|
1382
|
+
# Do it the new way. We try to save time by selecting information
|
|
1383
|
+
# only for the exact set of relations we are interested in.
|
|
1384
|
+
catalogs, exceptions = self.get_catalog_by_relations(used_schemas, relations)
|
|
1385
|
+
|
|
1386
|
+
if relations and catalogs:
|
|
1387
|
+
relation_map = {
|
|
1388
|
+
(
|
|
1389
|
+
r.database.casefold() if r.database else None,
|
|
1390
|
+
r.schema.casefold() if r.schema else None,
|
|
1391
|
+
r.identifier.casefold() if r.identifier else None,
|
|
1392
|
+
)
|
|
1393
|
+
for r in relations
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
def in_map(row: "agate.Row"):
|
|
1397
|
+
d = _expect_row_value("table_database", row)
|
|
1398
|
+
s = _expect_row_value("table_schema", row)
|
|
1399
|
+
i = _expect_row_value("table_name", row)
|
|
1400
|
+
d = d.casefold() if d is not None else None
|
|
1401
|
+
s = s.casefold() if s is not None else None
|
|
1402
|
+
i = i.casefold() if i is not None else None
|
|
1403
|
+
return (d, s, i) in relation_map
|
|
1404
|
+
|
|
1405
|
+
catalogs = catalogs.where(in_map)
|
|
1406
|
+
|
|
1407
|
+
return catalogs, exceptions
|
|
1408
|
+
|
|
1409
|
+
def row_matches_relation(self, row: "agate.Row", relations: Set[BaseRelation]):
|
|
1410
|
+
pass
|
|
1411
|
+
|
|
1412
|
+
def get_catalog(
|
|
1413
|
+
self,
|
|
1414
|
+
relation_configs: Iterable[RelationConfig],
|
|
1415
|
+
used_schemas: FrozenSet[Tuple[str, str]],
|
|
1416
|
+
) -> Tuple["agate.Table", List[Exception]]:
|
|
1417
|
+
with executor(self.config) as tpe:
|
|
1418
|
+
futures: List[Future["agate.Table"]] = []
|
|
1419
|
+
schema_map: SchemaSearchMap = self._get_catalog_schemas(relation_configs)
|
|
1420
|
+
for info, schemas in schema_map.items():
|
|
1421
|
+
if len(schemas) == 0:
|
|
1422
|
+
continue
|
|
1423
|
+
name = ".".join([str(info.database), "information_schema"])
|
|
1424
|
+
fut = tpe.submit_connected(
|
|
1425
|
+
self, name, self._get_one_catalog, info, schemas, used_schemas
|
|
1426
|
+
)
|
|
1427
|
+
futures.append(fut)
|
|
1428
|
+
|
|
1429
|
+
catalogs, exceptions = catch_as_completed(futures)
|
|
1430
|
+
return catalogs, exceptions
|
|
1431
|
+
|
|
1432
|
+
def get_catalog_by_relations(
|
|
1433
|
+
self, used_schemas: FrozenSet[Tuple[str, str]], relations: Set[BaseRelation]
|
|
1434
|
+
) -> Tuple["agate.Table", List[Exception]]:
|
|
1435
|
+
with executor(self.config) as tpe:
|
|
1436
|
+
futures: List[Future["agate.Table"]] = []
|
|
1437
|
+
relations_by_schema = self._get_catalog_relations_by_info_schema(relations)
|
|
1438
|
+
for info_schema in relations_by_schema:
|
|
1439
|
+
name = ".".join([str(info_schema.database), "information_schema"])
|
|
1440
|
+
relations = set(relations_by_schema[info_schema])
|
|
1441
|
+
fut = tpe.submit_connected(
|
|
1442
|
+
self,
|
|
1443
|
+
name,
|
|
1444
|
+
self._get_one_catalog_by_relations,
|
|
1445
|
+
info_schema,
|
|
1446
|
+
relations,
|
|
1447
|
+
used_schemas,
|
|
1448
|
+
)
|
|
1449
|
+
futures.append(fut)
|
|
1450
|
+
|
|
1451
|
+
catalogs, exceptions = catch_as_completed(futures)
|
|
1452
|
+
return catalogs, exceptions
|
|
1453
|
+
|
|
1454
|
+
def cancel_open_connections(self):
|
|
1455
|
+
"""Cancel all open connections."""
|
|
1456
|
+
return self.connections.cancel_open()
|
|
1457
|
+
|
|
1458
|
+
def _process_freshness_execution(
|
|
1459
|
+
self,
|
|
1460
|
+
macro_name: str,
|
|
1461
|
+
kwargs: Dict[str, Any],
|
|
1462
|
+
macro_resolver: Optional[MacroResolverProtocol] = None,
|
|
1463
|
+
) -> Tuple[Optional[AdapterResponse], FreshnessResponse]:
|
|
1464
|
+
"""Execute and process a freshness macro to generate a FreshnessResponse"""
|
|
1465
|
+
import agate
|
|
1466
|
+
|
|
1467
|
+
result = self.execute_macro(macro_name, kwargs=kwargs, macro_resolver=macro_resolver)
|
|
1468
|
+
|
|
1469
|
+
if isinstance(result, agate.Table):
|
|
1470
|
+
warn_or_error(CollectFreshnessReturnSignature())
|
|
1471
|
+
table = result
|
|
1472
|
+
adapter_response = None
|
|
1473
|
+
else:
|
|
1474
|
+
adapter_response, table = result.response, result.table
|
|
1475
|
+
|
|
1476
|
+
# Process the results table
|
|
1477
|
+
if len(table) != 1 or len(table[0]) != 2:
|
|
1478
|
+
raise MacroResultError(macro_name, table)
|
|
1479
|
+
|
|
1480
|
+
freshness_response = self._create_freshness_response(table[0][0], table[0][1])
|
|
1481
|
+
return adapter_response, freshness_response
|
|
1482
|
+
|
|
1483
|
+
def calculate_freshness(
|
|
1484
|
+
self,
|
|
1485
|
+
source: BaseRelation,
|
|
1486
|
+
loaded_at_field: str,
|
|
1487
|
+
filter: Optional[str],
|
|
1488
|
+
macro_resolver: Optional[MacroResolverProtocol] = None,
|
|
1489
|
+
) -> Tuple[Optional[AdapterResponse], FreshnessResponse]:
|
|
1490
|
+
"""Calculate the freshness of sources in dbt, and return it"""
|
|
1491
|
+
kwargs = {
|
|
1492
|
+
"source": source,
|
|
1493
|
+
"loaded_at_field": loaded_at_field,
|
|
1494
|
+
"filter": filter,
|
|
1495
|
+
}
|
|
1496
|
+
return self._process_freshness_execution(FRESHNESS_MACRO_NAME, kwargs, macro_resolver)
|
|
1497
|
+
|
|
1498
|
+
def calculate_freshness_from_custom_sql(
|
|
1499
|
+
self,
|
|
1500
|
+
source: BaseRelation,
|
|
1501
|
+
sql: str,
|
|
1502
|
+
macro_resolver: Optional[MacroResolverProtocol] = None,
|
|
1503
|
+
) -> Tuple[Optional[AdapterResponse], FreshnessResponse]:
|
|
1504
|
+
kwargs = {
|
|
1505
|
+
"source": source,
|
|
1506
|
+
"loaded_at_query": sql,
|
|
1507
|
+
}
|
|
1508
|
+
return self._process_freshness_execution(
|
|
1509
|
+
CUSTOM_SQL_FRESHNESS_MACRO_NAME, kwargs, macro_resolver
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
def calculate_freshness_from_metadata_batch(
|
|
1513
|
+
self,
|
|
1514
|
+
sources: List[BaseRelation],
|
|
1515
|
+
macro_resolver: Optional[MacroResolverProtocol] = None,
|
|
1516
|
+
) -> Tuple[List[Optional[AdapterResponse]], Dict[BaseRelation, FreshnessResponse]]:
|
|
1517
|
+
"""
|
|
1518
|
+
Given a list of sources (BaseRelations), calculate the metadata-based freshness in batch.
|
|
1519
|
+
This method should _not_ execute a warehouse query per source, but rather batch up
|
|
1520
|
+
the sources into as few requests as possible to minimize the number of roundtrips required
|
|
1521
|
+
to compute metadata-based freshness for each input source.
|
|
1522
|
+
|
|
1523
|
+
:param sources: The list of sources to calculate metadata-based freshness for
|
|
1524
|
+
:param macro_resolver: An optional macro_resolver to use for get_relation_last_modified
|
|
1525
|
+
:return: a tuple where:
|
|
1526
|
+
* the first element is a list of optional AdapterResponses indicating the response
|
|
1527
|
+
for each request the method made to compute the freshness for the provided sources.
|
|
1528
|
+
* the second element is a dictionary mapping an input source BaseRelation to a FreshnessResponse,
|
|
1529
|
+
if it was possible to calculate a FreshnessResponse for the source.
|
|
1530
|
+
"""
|
|
1531
|
+
# Track schema, identifiers of sources for lookup from batch query
|
|
1532
|
+
schema_identifier_to_source = {
|
|
1533
|
+
(
|
|
1534
|
+
source.path.get_lowered_part(ComponentName.Schema), # type: ignore
|
|
1535
|
+
source.path.get_lowered_part(ComponentName.Identifier), # type: ignore
|
|
1536
|
+
): source
|
|
1537
|
+
for source in sources
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
# Group metadata sources by information schema -- one query per information schema will be necessary
|
|
1541
|
+
sources_by_info_schema: Dict[InformationSchema, List[BaseRelation]] = (
|
|
1542
|
+
self._get_catalog_relations_by_info_schema(sources)
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
freshness_responses: Dict[BaseRelation, FreshnessResponse] = {}
|
|
1546
|
+
adapter_responses: List[Optional[AdapterResponse]] = []
|
|
1547
|
+
for (
|
|
1548
|
+
information_schema,
|
|
1549
|
+
sources_for_information_schema,
|
|
1550
|
+
) in sources_by_info_schema.items():
|
|
1551
|
+
result = self.execute_macro(
|
|
1552
|
+
GET_RELATION_LAST_MODIFIED_MACRO_NAME,
|
|
1553
|
+
kwargs={
|
|
1554
|
+
"information_schema": information_schema,
|
|
1555
|
+
"relations": sources_for_information_schema,
|
|
1556
|
+
},
|
|
1557
|
+
macro_resolver=macro_resolver,
|
|
1558
|
+
needs_conn=True,
|
|
1559
|
+
)
|
|
1560
|
+
adapter_response, table = result.response, result.table
|
|
1561
|
+
adapter_responses.append(adapter_response)
|
|
1562
|
+
|
|
1563
|
+
for row in table:
|
|
1564
|
+
raw_relation, freshness_response = self._parse_freshness_row(row, table)
|
|
1565
|
+
source_relation_for_result = schema_identifier_to_source[raw_relation]
|
|
1566
|
+
freshness_responses[source_relation_for_result] = freshness_response
|
|
1567
|
+
|
|
1568
|
+
return adapter_responses, freshness_responses
|
|
1569
|
+
|
|
1570
|
+
def calculate_freshness_from_metadata(
|
|
1571
|
+
self,
|
|
1572
|
+
source: BaseRelation,
|
|
1573
|
+
macro_resolver: Optional[MacroResolverProtocol] = None,
|
|
1574
|
+
) -> Tuple[Optional[AdapterResponse], FreshnessResponse]:
|
|
1575
|
+
adapter_responses, freshness_responses = self.calculate_freshness_from_metadata_batch(
|
|
1576
|
+
sources=[source],
|
|
1577
|
+
macro_resolver=macro_resolver,
|
|
1578
|
+
)
|
|
1579
|
+
adapter_response = adapter_responses[0] if adapter_responses else None
|
|
1580
|
+
return adapter_response, freshness_responses[source]
|
|
1581
|
+
|
|
1582
|
+
def _create_freshness_response(
|
|
1583
|
+
self, last_modified: Optional[datetime], snapshotted_at: Optional[datetime]
|
|
1584
|
+
) -> FreshnessResponse:
|
|
1585
|
+
if last_modified is None:
|
|
1586
|
+
# Interpret missing value as "infinitely long ago"
|
|
1587
|
+
max_loaded_at = datetime(1, 1, 1, 0, 0, 0, tzinfo=pytz.UTC)
|
|
1588
|
+
else:
|
|
1589
|
+
max_loaded_at = _utc(last_modified, None, "last_modified")
|
|
1590
|
+
|
|
1591
|
+
snapshotted_at = _utc(snapshotted_at, None, "snapshotted_at")
|
|
1592
|
+
age = (snapshotted_at - max_loaded_at).total_seconds()
|
|
1593
|
+
freshness: FreshnessResponse = {
|
|
1594
|
+
"max_loaded_at": max_loaded_at,
|
|
1595
|
+
"snapshotted_at": snapshotted_at,
|
|
1596
|
+
"age": age,
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
return freshness
|
|
1600
|
+
|
|
1601
|
+
def _parse_freshness_row(
|
|
1602
|
+
self, row: "agate.Row", table: "agate.Table"
|
|
1603
|
+
) -> Tuple[Any, FreshnessResponse]:
|
|
1604
|
+
from dbt_common.clients.agate_helper import get_column_value_uncased
|
|
1605
|
+
|
|
1606
|
+
try:
|
|
1607
|
+
last_modified_val = get_column_value_uncased("last_modified", row)
|
|
1608
|
+
snapshotted_at_val = get_column_value_uncased("snapshotted_at", row)
|
|
1609
|
+
identifier = get_column_value_uncased("identifier", row)
|
|
1610
|
+
schema = get_column_value_uncased("schema", row)
|
|
1611
|
+
except Exception:
|
|
1612
|
+
raise MacroResultError(GET_RELATION_LAST_MODIFIED_MACRO_NAME, table)
|
|
1613
|
+
|
|
1614
|
+
freshness_response = self._create_freshness_response(last_modified_val, snapshotted_at_val)
|
|
1615
|
+
raw_relation = schema.lower().strip(), identifier.lower().strip()
|
|
1616
|
+
return raw_relation, freshness_response
|
|
1617
|
+
|
|
1618
|
+
def pre_model_hook(self, config: Mapping[str, Any]) -> Any:
|
|
1619
|
+
"""A hook for running some operation before the model materialization
|
|
1620
|
+
runs. The hook can assume it has a connection available.
|
|
1621
|
+
|
|
1622
|
+
The only parameter is a configuration dictionary (the same one
|
|
1623
|
+
available in the materialization context). It should be considered
|
|
1624
|
+
read-only.
|
|
1625
|
+
|
|
1626
|
+
The pre-model hook may return anything as a context, which will be
|
|
1627
|
+
passed to the post-model hook.
|
|
1628
|
+
"""
|
|
1629
|
+
pass
|
|
1630
|
+
|
|
1631
|
+
def post_model_hook(self, config: Mapping[str, Any], context: Any) -> None:
|
|
1632
|
+
"""A hook for running some operation after the model materialization
|
|
1633
|
+
runs. The hook can assume it has a connection available.
|
|
1634
|
+
|
|
1635
|
+
The first parameter is a configuration dictionary (the same one
|
|
1636
|
+
available in the materialization context). It should be considered
|
|
1637
|
+
read-only.
|
|
1638
|
+
|
|
1639
|
+
The second parameter is the value returned by pre_mdoel_hook.
|
|
1640
|
+
"""
|
|
1641
|
+
pass
|
|
1642
|
+
|
|
1643
|
+
# Methods used in adapter tests
|
|
1644
|
+
def update_column_sql(
|
|
1645
|
+
self,
|
|
1646
|
+
dst_name: str,
|
|
1647
|
+
dst_column: str,
|
|
1648
|
+
clause: str,
|
|
1649
|
+
where_clause: Optional[str] = None,
|
|
1650
|
+
) -> str:
|
|
1651
|
+
clause = f"update {dst_name} set {dst_column} = {clause}"
|
|
1652
|
+
if where_clause is not None:
|
|
1653
|
+
clause += f" where {where_clause}"
|
|
1654
|
+
return clause
|
|
1655
|
+
|
|
1656
|
+
def timestamp_add_sql(self, add_to: str, number: int = 1, interval: str = "hour") -> str:
|
|
1657
|
+
# for backwards compatibility, we're compelled to set some sort of
|
|
1658
|
+
# default. A lot of searching has lead me to believe that the
|
|
1659
|
+
# '+ interval' syntax used in postgres/redshift is relatively common
|
|
1660
|
+
# and might even be the SQL standard's intention.
|
|
1661
|
+
return f"{add_to} + interval '{number} {interval}'"
|
|
1662
|
+
|
|
1663
|
+
def string_add_sql(
|
|
1664
|
+
self,
|
|
1665
|
+
add_to: str,
|
|
1666
|
+
value: str,
|
|
1667
|
+
location="append",
|
|
1668
|
+
) -> str:
|
|
1669
|
+
if location == "append":
|
|
1670
|
+
return f"{add_to} || '{value}'"
|
|
1671
|
+
elif location == "prepend":
|
|
1672
|
+
return f"'{value}' || {add_to}"
|
|
1673
|
+
else:
|
|
1674
|
+
raise DbtRuntimeError(f'Got an unexpected location value of "{location}"')
|
|
1675
|
+
|
|
1676
|
+
def get_rows_different_sql(
|
|
1677
|
+
self,
|
|
1678
|
+
relation_a: BaseRelation,
|
|
1679
|
+
relation_b: BaseRelation,
|
|
1680
|
+
column_names: Optional[List[str]] = None,
|
|
1681
|
+
except_operator: str = "EXCEPT",
|
|
1682
|
+
) -> str:
|
|
1683
|
+
"""Generate SQL for a query that returns a single row with a two
|
|
1684
|
+
columns: the number of rows that are different between the two
|
|
1685
|
+
relations and the number of mismatched rows.
|
|
1686
|
+
"""
|
|
1687
|
+
# This method only really exists for test reasons.
|
|
1688
|
+
names: List[str]
|
|
1689
|
+
if column_names is None:
|
|
1690
|
+
columns = self.get_columns_in_relation(relation_a)
|
|
1691
|
+
names = sorted((self.quote(c.name) for c in columns))
|
|
1692
|
+
else:
|
|
1693
|
+
names = sorted((self.quote(n) for n in column_names))
|
|
1694
|
+
columns_csv = ", ".join(names)
|
|
1695
|
+
|
|
1696
|
+
sql = COLUMNS_EQUAL_SQL.format(
|
|
1697
|
+
columns=columns_csv,
|
|
1698
|
+
relation_a=str(relation_a),
|
|
1699
|
+
relation_b=str(relation_b),
|
|
1700
|
+
except_op=except_operator,
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
return sql
|
|
1704
|
+
|
|
1705
|
+
@property
|
|
1706
|
+
def python_submission_helpers(self) -> Dict[str, Type[PythonJobHelper]]:
|
|
1707
|
+
raise NotImplementedError("python_submission_helpers is not specified")
|
|
1708
|
+
|
|
1709
|
+
@property
|
|
1710
|
+
def default_python_submission_method(self) -> str:
|
|
1711
|
+
raise NotImplementedError("default_python_submission_method is not specified")
|
|
1712
|
+
|
|
1713
|
+
@log_code_execution
|
|
1714
|
+
def submit_python_job(self, parsed_model: dict, compiled_code: str) -> AdapterResponse:
|
|
1715
|
+
submission_method = parsed_model["config"].get(
|
|
1716
|
+
"submission_method", self.default_python_submission_method
|
|
1717
|
+
)
|
|
1718
|
+
if submission_method not in self.python_submission_helpers:
|
|
1719
|
+
raise NotImplementedError(
|
|
1720
|
+
"Submission method {} is not supported for current adapter".format(
|
|
1721
|
+
submission_method
|
|
1722
|
+
)
|
|
1723
|
+
)
|
|
1724
|
+
job_helper = self.python_submission_helpers[submission_method](
|
|
1725
|
+
parsed_model, self.connections.profile.credentials
|
|
1726
|
+
)
|
|
1727
|
+
submission_result = job_helper.submit(compiled_code)
|
|
1728
|
+
# process submission result to generate adapter response
|
|
1729
|
+
return self.generate_python_submission_response(submission_result)
|
|
1730
|
+
|
|
1731
|
+
def generate_python_submission_response(self, submission_result: Any) -> AdapterResponse:
|
|
1732
|
+
raise NotImplementedError(
|
|
1733
|
+
"Your adapter need to implement generate_python_submission_response"
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
def valid_incremental_strategies(self):
|
|
1737
|
+
"""The set of standard builtin strategies which this adapter supports out-of-the-box.
|
|
1738
|
+
Not used to validate custom strategies defined by end users.
|
|
1739
|
+
"""
|
|
1740
|
+
return ["append"]
|
|
1741
|
+
|
|
1742
|
+
def builtin_incremental_strategies(self):
|
|
1743
|
+
"""
|
|
1744
|
+
List of possible builtin strategies for adapters
|
|
1745
|
+
|
|
1746
|
+
Microbatch is added by _default_. It is only not added when the behavior flag
|
|
1747
|
+
`require_batched_execution_for_custom_microbatch_strategy` is True.
|
|
1748
|
+
"""
|
|
1749
|
+
builtin_strategies = ["append", "delete+insert", "merge", "insert_overwrite"]
|
|
1750
|
+
if not self.behavior.require_batched_execution_for_custom_microbatch_strategy.no_warn:
|
|
1751
|
+
builtin_strategies.append("microbatch")
|
|
1752
|
+
|
|
1753
|
+
return builtin_strategies
|
|
1754
|
+
|
|
1755
|
+
@available.parse_none
|
|
1756
|
+
def get_incremental_strategy_macro(self, model_context, strategy: str):
|
|
1757
|
+
"""Gets the macro for the given incremental strategy.
|
|
1758
|
+
|
|
1759
|
+
Additionally some validations are done:
|
|
1760
|
+
1. Assert that if the given strategy is a "builtin" strategy, then it must
|
|
1761
|
+
also be defined as a "valid" strategy for the associated adapter
|
|
1762
|
+
2. Assert that the incremental strategy exists in the model context
|
|
1763
|
+
|
|
1764
|
+
Notably, something be defined by the adapter as "valid" without it being
|
|
1765
|
+
a "builtin", and nothing will break (and that is desirable).
|
|
1766
|
+
"""
|
|
1767
|
+
|
|
1768
|
+
# Construct macro_name from strategy name
|
|
1769
|
+
if strategy is None:
|
|
1770
|
+
strategy = "default"
|
|
1771
|
+
|
|
1772
|
+
# validate strategies for this adapter
|
|
1773
|
+
valid_strategies = self.valid_incremental_strategies()
|
|
1774
|
+
valid_strategies.append("default")
|
|
1775
|
+
builtin_strategies = self.builtin_incremental_strategies()
|
|
1776
|
+
if strategy in builtin_strategies and strategy not in valid_strategies:
|
|
1777
|
+
raise DbtRuntimeError(
|
|
1778
|
+
f"The incremental strategy '{strategy}' is not valid for this adapter"
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
strategy = strategy.replace("+", "_")
|
|
1782
|
+
macro_name = f"get_incremental_{strategy}_sql"
|
|
1783
|
+
# The model_context should have callable objects for all macros
|
|
1784
|
+
if macro_name not in model_context:
|
|
1785
|
+
raise DbtRuntimeError(
|
|
1786
|
+
'dbt could not find an incremental strategy macro with the name "{}" in {}'.format(
|
|
1787
|
+
macro_name, self.config.project_name
|
|
1788
|
+
)
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
# This returns a callable macro
|
|
1792
|
+
return model_context[macro_name]
|
|
1793
|
+
|
|
1794
|
+
@classmethod
|
|
1795
|
+
def _parse_column_constraint(cls, raw_constraint: Dict[str, Any]) -> ColumnLevelConstraint:
|
|
1796
|
+
try:
|
|
1797
|
+
ColumnLevelConstraint.validate(raw_constraint)
|
|
1798
|
+
return ColumnLevelConstraint.from_dict(raw_constraint)
|
|
1799
|
+
except Exception:
|
|
1800
|
+
raise DbtValidationError(f"Could not parse constraint: {raw_constraint}")
|
|
1801
|
+
|
|
1802
|
+
@classmethod
|
|
1803
|
+
def render_column_constraint(cls, constraint: ColumnLevelConstraint) -> Optional[str]:
|
|
1804
|
+
"""Render the given constraint as DDL text. Should be overriden by adapters which need custom constraint
|
|
1805
|
+
rendering."""
|
|
1806
|
+
constraint_expression = constraint.expression or ""
|
|
1807
|
+
|
|
1808
|
+
rendered_column_constraint = None
|
|
1809
|
+
if constraint.type == ConstraintType.check and constraint_expression:
|
|
1810
|
+
rendered_column_constraint = f"check ({constraint_expression})"
|
|
1811
|
+
elif constraint.type == ConstraintType.not_null:
|
|
1812
|
+
rendered_column_constraint = f"not null {constraint_expression}"
|
|
1813
|
+
elif constraint.type == ConstraintType.unique:
|
|
1814
|
+
rendered_column_constraint = f"unique {constraint_expression}"
|
|
1815
|
+
elif constraint.type == ConstraintType.primary_key:
|
|
1816
|
+
rendered_column_constraint = f"primary key {constraint_expression}"
|
|
1817
|
+
elif constraint.type == ConstraintType.foreign_key:
|
|
1818
|
+
if constraint.to and constraint.to_columns:
|
|
1819
|
+
rendered_column_constraint = (
|
|
1820
|
+
f"references {constraint.to} ({', '.join(constraint.to_columns)})"
|
|
1821
|
+
)
|
|
1822
|
+
elif constraint_expression:
|
|
1823
|
+
rendered_column_constraint = f"references {constraint_expression}"
|
|
1824
|
+
elif constraint.type == ConstraintType.custom and constraint_expression:
|
|
1825
|
+
rendered_column_constraint = constraint_expression
|
|
1826
|
+
|
|
1827
|
+
if rendered_column_constraint:
|
|
1828
|
+
rendered_column_constraint = rendered_column_constraint.strip()
|
|
1829
|
+
|
|
1830
|
+
return rendered_column_constraint
|
|
1831
|
+
|
|
1832
|
+
@available
|
|
1833
|
+
@classmethod
|
|
1834
|
+
@auto_record_function("AdapterRenderRawColumnConstraints", group="Available")
|
|
1835
|
+
def render_raw_columns_constraints(cls, raw_columns: Dict[str, Dict[str, Any]]) -> List[str]:
|
|
1836
|
+
|
|
1837
|
+
rendered_column_constraints = []
|
|
1838
|
+
|
|
1839
|
+
for v in raw_columns.values():
|
|
1840
|
+
col_name = cls.quote(v["name"]) if v.get("quote") else v["name"]
|
|
1841
|
+
rendered_column_constraint = [f"{col_name} {v['data_type']}"]
|
|
1842
|
+
for con in v.get("constraints", None):
|
|
1843
|
+
constraint = cls._parse_column_constraint(con)
|
|
1844
|
+
c = cls.process_parsed_constraint(constraint, cls.render_column_constraint)
|
|
1845
|
+
if c is not None:
|
|
1846
|
+
rendered_column_constraint.append(c)
|
|
1847
|
+
rendered_column_constraints.append(" ".join(rendered_column_constraint))
|
|
1848
|
+
|
|
1849
|
+
return rendered_column_constraints
|
|
1850
|
+
|
|
1851
|
+
@classmethod
|
|
1852
|
+
def process_parsed_constraint(
|
|
1853
|
+
cls,
|
|
1854
|
+
parsed_constraint: Union[ColumnLevelConstraint, ModelLevelConstraint],
|
|
1855
|
+
render_func,
|
|
1856
|
+
) -> Optional[str]:
|
|
1857
|
+
# skip checking enforcement if this is a 'custom' constraint
|
|
1858
|
+
if parsed_constraint.type == ConstraintType.custom:
|
|
1859
|
+
return render_func(parsed_constraint)
|
|
1860
|
+
if (
|
|
1861
|
+
parsed_constraint.warn_unsupported
|
|
1862
|
+
and cls.CONSTRAINT_SUPPORT[parsed_constraint.type] == ConstraintSupport.NOT_SUPPORTED
|
|
1863
|
+
):
|
|
1864
|
+
warn_or_error(
|
|
1865
|
+
ConstraintNotSupported(constraint=parsed_constraint.type.value, adapter=cls.type())
|
|
1866
|
+
)
|
|
1867
|
+
if (
|
|
1868
|
+
parsed_constraint.warn_unenforced
|
|
1869
|
+
and cls.CONSTRAINT_SUPPORT[parsed_constraint.type] == ConstraintSupport.NOT_ENFORCED
|
|
1870
|
+
):
|
|
1871
|
+
warn_or_error(
|
|
1872
|
+
ConstraintNotEnforced(constraint=parsed_constraint.type.value, adapter=cls.type())
|
|
1873
|
+
)
|
|
1874
|
+
if cls.CONSTRAINT_SUPPORT[parsed_constraint.type] != ConstraintSupport.NOT_SUPPORTED:
|
|
1875
|
+
return render_func(parsed_constraint)
|
|
1876
|
+
|
|
1877
|
+
return None
|
|
1878
|
+
|
|
1879
|
+
@classmethod
|
|
1880
|
+
def _parse_model_constraint(cls, raw_constraint: Dict[str, Any]) -> ModelLevelConstraint:
|
|
1881
|
+
try:
|
|
1882
|
+
ModelLevelConstraint.validate(raw_constraint)
|
|
1883
|
+
c = ModelLevelConstraint.from_dict(raw_constraint)
|
|
1884
|
+
return c
|
|
1885
|
+
except Exception:
|
|
1886
|
+
raise DbtValidationError(f"Could not parse constraint: {raw_constraint}")
|
|
1887
|
+
|
|
1888
|
+
@available
|
|
1889
|
+
@classmethod
|
|
1890
|
+
@auto_record_function("AdapterRenderRawModelConstraints", group="Available")
|
|
1891
|
+
def render_raw_model_constraints(cls, raw_constraints: List[Dict[str, Any]]) -> List[str]:
|
|
1892
|
+
|
|
1893
|
+
return [c for c in map(cls.render_raw_model_constraint, raw_constraints) if c is not None]
|
|
1894
|
+
|
|
1895
|
+
@classmethod
|
|
1896
|
+
def render_raw_model_constraint(cls, raw_constraint: Dict[str, Any]) -> Optional[str]:
|
|
1897
|
+
constraint = cls._parse_model_constraint(raw_constraint)
|
|
1898
|
+
return cls.process_parsed_constraint(constraint, cls.render_model_constraint)
|
|
1899
|
+
|
|
1900
|
+
@classmethod
|
|
1901
|
+
def render_model_constraint(cls, constraint: ModelLevelConstraint) -> Optional[str]:
|
|
1902
|
+
"""Render the given constraint as DDL text. Should be overriden by adapters which need custom constraint
|
|
1903
|
+
rendering."""
|
|
1904
|
+
constraint_prefix = f"constraint {constraint.name} " if constraint.name else ""
|
|
1905
|
+
column_list = ", ".join(constraint.columns)
|
|
1906
|
+
rendered_model_constraint = None
|
|
1907
|
+
|
|
1908
|
+
if constraint.type == ConstraintType.check and constraint.expression:
|
|
1909
|
+
rendered_model_constraint = f"{constraint_prefix}check ({constraint.expression})"
|
|
1910
|
+
elif constraint.type == ConstraintType.unique:
|
|
1911
|
+
constraint_expression = f" {constraint.expression}" if constraint.expression else ""
|
|
1912
|
+
rendered_model_constraint = (
|
|
1913
|
+
f"{constraint_prefix}unique{constraint_expression} ({column_list})"
|
|
1914
|
+
)
|
|
1915
|
+
elif constraint.type == ConstraintType.primary_key:
|
|
1916
|
+
constraint_expression = f" {constraint.expression}" if constraint.expression else ""
|
|
1917
|
+
rendered_model_constraint = (
|
|
1918
|
+
f"{constraint_prefix}primary key{constraint_expression} ({column_list})"
|
|
1919
|
+
)
|
|
1920
|
+
elif constraint.type == ConstraintType.foreign_key:
|
|
1921
|
+
if constraint.to and constraint.to_columns:
|
|
1922
|
+
rendered_model_constraint = f"{constraint_prefix}foreign key ({column_list}) references {constraint.to} ({', '.join(constraint.to_columns)})"
|
|
1923
|
+
elif constraint.expression:
|
|
1924
|
+
rendered_model_constraint = f"{constraint_prefix}foreign key ({column_list}) references {constraint.expression}"
|
|
1925
|
+
elif constraint.type == ConstraintType.custom and constraint.expression:
|
|
1926
|
+
rendered_model_constraint = f"{constraint_prefix}{constraint.expression}"
|
|
1927
|
+
|
|
1928
|
+
return rendered_model_constraint
|
|
1929
|
+
|
|
1930
|
+
@classmethod
|
|
1931
|
+
def capabilities(cls) -> CapabilityDict:
|
|
1932
|
+
return cls._capabilities
|
|
1933
|
+
|
|
1934
|
+
@classmethod
|
|
1935
|
+
def supports(cls, capability: Capability) -> bool:
|
|
1936
|
+
return bool(cls.capabilities()[capability])
|
|
1937
|
+
|
|
1938
|
+
@classmethod
|
|
1939
|
+
def get_adapter_run_info(cls, config: RelationConfig) -> AdapterTrackingRelationInfo:
|
|
1940
|
+
adapter_class_name, *_ = cls.__name__.split("Adapter")
|
|
1941
|
+
adapter_name = adapter_class_name.lower()
|
|
1942
|
+
|
|
1943
|
+
if adapter_name == "base":
|
|
1944
|
+
adapter_version = ""
|
|
1945
|
+
else:
|
|
1946
|
+
adapter_version = import_module(f"dbt.adapters.{adapter_name}.__version__").version
|
|
1947
|
+
|
|
1948
|
+
return AdapterTrackingRelationInfo(
|
|
1949
|
+
adapter_name=adapter_name,
|
|
1950
|
+
base_adapter_version=import_module("dbt.adapters.__about__").version,
|
|
1951
|
+
adapter_version=adapter_version,
|
|
1952
|
+
model_adapter_details=cls._get_adapter_specific_run_info(config),
|
|
1953
|
+
)
|
|
1954
|
+
|
|
1955
|
+
@classmethod
|
|
1956
|
+
def _get_adapter_specific_run_info(cls, config) -> Dict[str, Any]:
|
|
1957
|
+
"""
|
|
1958
|
+
Adapter maintainers should overwrite this method to return any run metadata that should be captured during a run.
|
|
1959
|
+
"""
|
|
1960
|
+
return {}
|
|
1961
|
+
|
|
1962
|
+
@available.parse_none
|
|
1963
|
+
@classmethod
|
|
1964
|
+
def get_hard_deletes_behavior(cls, config: Dict[str, str]) -> str:
|
|
1965
|
+
"""Check the hard_deletes config enum, and the legacy invalidate_hard_deletes
|
|
1966
|
+
config flag in order to determine which behavior should be used for deleted
|
|
1967
|
+
records in a snapshot. The default is to ignore them."""
|
|
1968
|
+
invalidate_hard_deletes = config.get("invalidate_hard_deletes", None)
|
|
1969
|
+
hard_deletes = config.get("hard_deletes", None)
|
|
1970
|
+
|
|
1971
|
+
if invalidate_hard_deletes is not None and hard_deletes is not None:
|
|
1972
|
+
raise DbtValidationError(
|
|
1973
|
+
"You cannot set both the invalidate_hard_deletes and hard_deletes config properties on the same snapshot."
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
if invalidate_hard_deletes or hard_deletes == "invalidate":
|
|
1977
|
+
return "invalidate"
|
|
1978
|
+
elif hard_deletes == "new_record":
|
|
1979
|
+
return "new_record"
|
|
1980
|
+
elif hard_deletes is None or hard_deletes == "ignore":
|
|
1981
|
+
return "ignore"
|
|
1982
|
+
|
|
1983
|
+
raise DbtValidationError("Invalid setting for property hard_deletes.")
|
|
1984
|
+
|
|
1985
|
+
|
|
1986
|
+
COLUMNS_EQUAL_SQL = """
|
|
1987
|
+
with diff_count as (
|
|
1988
|
+
SELECT
|
|
1989
|
+
1 as id,
|
|
1990
|
+
COUNT(*) as num_missing FROM (
|
|
1991
|
+
(SELECT {columns} FROM {relation_a} {except_op}
|
|
1992
|
+
SELECT {columns} FROM {relation_b})
|
|
1993
|
+
UNION ALL
|
|
1994
|
+
(SELECT {columns} FROM {relation_b} {except_op}
|
|
1995
|
+
SELECT {columns} FROM {relation_a})
|
|
1996
|
+
) as a
|
|
1997
|
+
), table_a as (
|
|
1998
|
+
SELECT COUNT(*) as num_rows FROM {relation_a}
|
|
1999
|
+
), table_b as (
|
|
2000
|
+
SELECT COUNT(*) as num_rows FROM {relation_b}
|
|
2001
|
+
), row_count_diff as (
|
|
2002
|
+
select
|
|
2003
|
+
1 as id,
|
|
2004
|
+
table_a.num_rows - table_b.num_rows as difference
|
|
2005
|
+
from table_a, table_b
|
|
2006
|
+
)
|
|
2007
|
+
select
|
|
2008
|
+
row_count_diff.difference as row_count_difference,
|
|
2009
|
+
diff_count.num_missing as num_mismatched
|
|
2010
|
+
from row_count_diff
|
|
2011
|
+
join diff_count using (id)
|
|
2012
|
+
""".strip()
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def catch_as_completed(
|
|
2016
|
+
futures, # typing: List[Future["agate.Table"]]
|
|
2017
|
+
) -> Tuple["agate.Table", List[Exception]]:
|
|
2018
|
+
from dbt_common.clients.agate_helper import merge_tables
|
|
2019
|
+
|
|
2020
|
+
# catalogs: "agate.Table" =".Table(rows=[])
|
|
2021
|
+
tables: List["agate.Table"] = []
|
|
2022
|
+
exceptions: List[Exception] = []
|
|
2023
|
+
|
|
2024
|
+
for future in as_completed(futures):
|
|
2025
|
+
exc = future.exception()
|
|
2026
|
+
# we want to re-raise on ctrl+c and BaseException
|
|
2027
|
+
if exc is None:
|
|
2028
|
+
catalog = future.result()
|
|
2029
|
+
tables.append(catalog)
|
|
2030
|
+
elif isinstance(exc, KeyboardInterrupt) or not isinstance(exc, Exception):
|
|
2031
|
+
raise exc
|
|
2032
|
+
else:
|
|
2033
|
+
warn_or_error(CatalogGenerationError(exc=str(exc)))
|
|
2034
|
+
# exc is not None, derives from Exception, and isn't ctrl+c
|
|
2035
|
+
exceptions.append(exc)
|
|
2036
|
+
return merge_tables(tables), exceptions
|