dbt-confluent 0.1.0__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/__init__.py +3 -0
- dbt/adapters/confluent/__init__.py +16 -0
- dbt/adapters/confluent/__version__.py +1 -0
- dbt/adapters/confluent/column.py +14 -0
- dbt/adapters/confluent/connections.py +360 -0
- dbt/adapters/confluent/impl.py +313 -0
- dbt/adapters/confluent/utils.py +78 -0
- dbt/include/confluent/__init__.py +3 -0
- dbt/include/confluent/dbt_project.yml +7 -0
- dbt/include/confluent/macros/adapters/columns.sql +19 -0
- dbt/include/confluent/macros/adapters/metadata.sql +95 -0
- dbt/include/confluent/macros/adapters/relation.sql +8 -0
- dbt/include/confluent/macros/adapters/schema.sql +14 -0
- dbt/include/confluent/macros/etc/statement.sql +37 -0
- dbt/include/confluent/macros/get_custom_name/get_custom_schema.sql +9 -0
- dbt/include/confluent/macros/materializations/models/helpers.sql +15 -0
- dbt/include/confluent/macros/materializations/models/incremental/incremental.sql +6 -0
- dbt/include/confluent/macros/materializations/models/materialized_view.sql +26 -0
- dbt/include/confluent/macros/materializations/models/streaming_source.sql +47 -0
- dbt/include/confluent/macros/materializations/models/streaming_table.sql +52 -0
- dbt/include/confluent/macros/materializations/models/table.sql +24 -0
- dbt/include/confluent/macros/materializations/models/view.sql +24 -0
- dbt/include/confluent/macros/materializations/seeds/helpers.sql +49 -0
- dbt/include/confluent/macros/materializations/tests/helpers.sql +22 -0
- dbt/include/confluent/macros/materializations/tests/test.sql +70 -0
- dbt/include/confluent/macros/materializations/tests/unit.sql +85 -0
- dbt/include/confluent/macros/relations/drop.sql +13 -0
- dbt/include/confluent/macros/relations/materialized_view/alter.sql +5 -0
- dbt/include/confluent/macros/relations/materialized_view/create.sql +7 -0
- dbt/include/confluent/macros/relations/materialized_view/drop.sql +6 -0
- dbt/include/confluent/macros/relations/materialized_view/refresh.sql +5 -0
- dbt/include/confluent/macros/relations/table/create.sql +20 -0
- dbt/include/confluent/macros/relations/table/drop.sql +5 -0
- dbt/include/confluent/macros/relations/views/drop.sql +4 -0
- dbt/include/confluent/macros/timestamps.sql +12 -0
- dbt/include/confluent/profile_template.yml +38 -0
- dbt_confluent-0.1.0.dist-info/METADATA +192 -0
- dbt_confluent-0.1.0.dist-info/RECORD +41 -0
- dbt_confluent-0.1.0.dist-info/WHEEL +4 -0
- dbt_confluent-0.1.0.dist-info/entry_points.txt +2 -0
- dbt_confluent-0.1.0.dist-info/licenses/LICENSE +177 -0
dbt/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dbt.adapters.base import AdapterPlugin
|
|
2
|
+
from dbt.adapters.confluent.column import ConfluentColumn
|
|
3
|
+
from dbt.adapters.confluent.connections import (
|
|
4
|
+
ConfluentConnectionManager,
|
|
5
|
+
ConfluentCredentials,
|
|
6
|
+
)
|
|
7
|
+
from dbt.adapters.confluent.impl import ConfluentAdapter
|
|
8
|
+
from dbt.include import confluent
|
|
9
|
+
|
|
10
|
+
__all__ = ["Plugin", "ConfluentConnectionManager", "ConfluentColumn"]
|
|
11
|
+
|
|
12
|
+
Plugin = AdapterPlugin(
|
|
13
|
+
adapter=ConfluentAdapter,
|
|
14
|
+
credentials=ConfluentCredentials,
|
|
15
|
+
include_path=confluent.PACKAGE_PATH,
|
|
16
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.1.0"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dbt.adapters.base.column import Column
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConfluentColumn(Column):
|
|
5
|
+
"""
|
|
6
|
+
Custom Column class for Confluent adapter.
|
|
7
|
+
|
|
8
|
+
Overrides the quoted property to use backticks instead of double quotes,
|
|
9
|
+
as Confluent Cloud SQL requires backticks for identifiers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def quoted(self) -> str:
|
|
14
|
+
return f"`{self.column}`"
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import confluent_sql
|
|
10
|
+
from confluent_sql import Cursor
|
|
11
|
+
from confluent_sql.exceptions import ComputePoolExhaustedError
|
|
12
|
+
from confluent_sql.execution_mode import ExecutionMode
|
|
13
|
+
from dbt_common.events.contextvars import get_node_info
|
|
14
|
+
from dbt_common.events.functions import fire_event
|
|
15
|
+
from dbt_common.exceptions import (
|
|
16
|
+
ConnectionError,
|
|
17
|
+
DbtDatabaseError,
|
|
18
|
+
DbtRuntimeError,
|
|
19
|
+
)
|
|
20
|
+
from dbt_common.utils import cast_to_str
|
|
21
|
+
|
|
22
|
+
from dbt.adapters.contracts.connection import (
|
|
23
|
+
AdapterResponse,
|
|
24
|
+
Connection,
|
|
25
|
+
ConnectionState,
|
|
26
|
+
Credentials,
|
|
27
|
+
)
|
|
28
|
+
from dbt.adapters.events.types import (
|
|
29
|
+
AdapterEventDebug,
|
|
30
|
+
AdapterEventWarning,
|
|
31
|
+
ConnectionUsed,
|
|
32
|
+
SQLQuery,
|
|
33
|
+
SQLQueryStatus,
|
|
34
|
+
)
|
|
35
|
+
from dbt.adapters.sql import SQLConnectionManager
|
|
36
|
+
|
|
37
|
+
from .__version__ import version
|
|
38
|
+
from .utils import fetch_from_cursor
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
import agate
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ConfluentCredentials(Credentials):
|
|
48
|
+
"""
|
|
49
|
+
Defines database specific credentials that get added to
|
|
50
|
+
profiles.yml to connect to new adapter
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Add credentials members here, like:
|
|
54
|
+
cloud_provider: str
|
|
55
|
+
cloud_region: str
|
|
56
|
+
compute_pool_id: str
|
|
57
|
+
organization_id: str
|
|
58
|
+
flink_api_key: str
|
|
59
|
+
flink_api_secret: str
|
|
60
|
+
execution_mode: ExecutionMode = ExecutionMode.STREAMING_QUERY
|
|
61
|
+
statement_name_prefix: str = "dbt-confluent-"
|
|
62
|
+
statement_label: str = "dbt-confluent"
|
|
63
|
+
|
|
64
|
+
_ALIASES = {"environment_id": "database", "dbname": "schema"}
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def type(self):
|
|
68
|
+
"""Return name of adapter."""
|
|
69
|
+
return "confluent"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def unique_field(self):
|
|
73
|
+
"""
|
|
74
|
+
Hashed and included in anonymous telemetry to track adapter adoption.
|
|
75
|
+
Pick a field that can uniquely identify one team/organization building with this adapter
|
|
76
|
+
"""
|
|
77
|
+
return f"{self.cloud_provider}-{self.cloud_region}-{self.organization_id}"
|
|
78
|
+
|
|
79
|
+
def _connection_keys(self):
|
|
80
|
+
"""
|
|
81
|
+
List of keys to display in the `dbt debug` output.
|
|
82
|
+
"""
|
|
83
|
+
return ("organization_id", "database", "schema")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ConfluentConnectionManager(SQLConnectionManager):
|
|
87
|
+
TYPE = "confluent"
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def get_result_from_cursor(cls, cursor: Cursor, limit: int | None) -> "agate.Table":
|
|
91
|
+
from dbt_common.clients.agate_helper import table_from_data_flat
|
|
92
|
+
|
|
93
|
+
data: Iterable[Any] = []
|
|
94
|
+
column_names: list[str] = []
|
|
95
|
+
|
|
96
|
+
if cursor.description is not None:
|
|
97
|
+
column_names = [col[0] for col in cursor.description]
|
|
98
|
+
rows = fetch_from_cursor(cursor, limit)
|
|
99
|
+
data = cls.process_results(column_names, rows)
|
|
100
|
+
|
|
101
|
+
return table_from_data_flat(data, column_names)
|
|
102
|
+
|
|
103
|
+
def execute(
|
|
104
|
+
self,
|
|
105
|
+
sql: str,
|
|
106
|
+
auto_begin: bool = False,
|
|
107
|
+
fetch: bool = False,
|
|
108
|
+
limit: int | None = None,
|
|
109
|
+
execution_mode: str | None = None,
|
|
110
|
+
) -> tuple[AdapterResponse, "agate.Table"]:
|
|
111
|
+
"""This is customized so we can pass execution_mode down the chain."""
|
|
112
|
+
from dbt_common.clients.agate_helper import empty_table
|
|
113
|
+
|
|
114
|
+
sql = self._add_query_comment(sql)
|
|
115
|
+
_, cursor = self.add_query(sql, auto_begin, execution_mode=execution_mode)
|
|
116
|
+
response = self.get_response(cursor)
|
|
117
|
+
if fetch:
|
|
118
|
+
table = self.get_result_from_cursor(cursor, limit)
|
|
119
|
+
else:
|
|
120
|
+
cursor.close()
|
|
121
|
+
table = empty_table()
|
|
122
|
+
return response, table
|
|
123
|
+
|
|
124
|
+
def add_query(
|
|
125
|
+
self,
|
|
126
|
+
sql: str,
|
|
127
|
+
auto_begin: bool = True,
|
|
128
|
+
bindings: Any | None = None,
|
|
129
|
+
abridge_sql_log: bool = False,
|
|
130
|
+
retryable_exceptions: tuple[type[Exception], ...] = (ComputePoolExhaustedError,),
|
|
131
|
+
retry_limit: int = 5,
|
|
132
|
+
execution_mode: str | None = None,
|
|
133
|
+
) -> tuple[Connection, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Copied from upstream (in SqlConnectionManager) with handling of cursor's
|
|
136
|
+
execution_mode. ExecutionMode can be specified at the project level in credentials,
|
|
137
|
+
or as a node info in config blocks.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def _execute_query_with_retry(
|
|
141
|
+
cursor: confluent_sql.Cursor,
|
|
142
|
+
sql: str,
|
|
143
|
+
bindings: Any | None,
|
|
144
|
+
retryable_exceptions: tuple[type[Exception], ...],
|
|
145
|
+
retry_limit: int,
|
|
146
|
+
attempt: int,
|
|
147
|
+
statement_name: str | None = None,
|
|
148
|
+
statement_label: str | None = None,
|
|
149
|
+
):
|
|
150
|
+
"""
|
|
151
|
+
A success sees the try exit cleanly and avoid any recursive
|
|
152
|
+
retries. Failure begins a sleep and retry routine.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
cursor.execute(
|
|
156
|
+
sql, bindings, statement_name=statement_name, statement_label=statement_label
|
|
157
|
+
)
|
|
158
|
+
except retryable_exceptions as e:
|
|
159
|
+
# Cease retries and fail when limit is hit.
|
|
160
|
+
if attempt >= retry_limit:
|
|
161
|
+
raise e
|
|
162
|
+
|
|
163
|
+
backoff = min(attempt * 3, 15)
|
|
164
|
+
retries_left = retry_limit - attempt
|
|
165
|
+
|
|
166
|
+
if isinstance(e, ComputePoolExhaustedError):
|
|
167
|
+
fire_event(
|
|
168
|
+
AdapterEventWarning(
|
|
169
|
+
base_msg=f"Compute pool exhausted. {retries_left} retries left. "
|
|
170
|
+
f"Retrying in {backoff} seconds."
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
fire_event(
|
|
175
|
+
AdapterEventDebug(
|
|
176
|
+
base_msg=f"Got a retryable error {type(e)}. {retries_left} retries left. "
|
|
177
|
+
f"Retrying in {backoff} seconds.\nError:\n{e}"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
time.sleep(backoff)
|
|
181
|
+
|
|
182
|
+
# Generate a new statement name for the retry since the
|
|
183
|
+
# previous one may have been deleted by ComputePoolExhaustedError.
|
|
184
|
+
prefix = connection.credentials.statement_name_prefix
|
|
185
|
+
retry_statement_name = f"{prefix}{uuid.uuid4()}" if statement_name else None
|
|
186
|
+
return _execute_query_with_retry(
|
|
187
|
+
cursor=cursor,
|
|
188
|
+
sql=sql,
|
|
189
|
+
bindings=bindings,
|
|
190
|
+
retryable_exceptions=retryable_exceptions,
|
|
191
|
+
retry_limit=retry_limit,
|
|
192
|
+
attempt=attempt + 1,
|
|
193
|
+
statement_name=retry_statement_name,
|
|
194
|
+
statement_label=statement_label,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
connection = self.get_thread_connection()
|
|
198
|
+
if auto_begin and connection.transaction_open is False:
|
|
199
|
+
self.begin()
|
|
200
|
+
fire_event(
|
|
201
|
+
ConnectionUsed(
|
|
202
|
+
conn_type=self.TYPE,
|
|
203
|
+
conn_name=cast_to_str(connection.name),
|
|
204
|
+
node_info=get_node_info(),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
with self.exception_handler(sql):
|
|
209
|
+
if abridge_sql_log:
|
|
210
|
+
log_sql = f"{sql[:512]}..."
|
|
211
|
+
else:
|
|
212
|
+
log_sql = sql
|
|
213
|
+
|
|
214
|
+
fire_event(
|
|
215
|
+
SQLQuery(
|
|
216
|
+
conn_name=cast_to_str(connection.name),
|
|
217
|
+
sql=log_sql,
|
|
218
|
+
node_info=get_node_info(),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
pre = time.perf_counter()
|
|
223
|
+
|
|
224
|
+
if execution_mode:
|
|
225
|
+
resolved_mode = ExecutionMode(execution_mode)
|
|
226
|
+
else:
|
|
227
|
+
resolved_mode = ExecutionMode(connection.credentials.execution_mode)
|
|
228
|
+
|
|
229
|
+
prefix = connection.credentials.statement_name_prefix
|
|
230
|
+
statement_name = f"{prefix}{uuid.uuid4()}"
|
|
231
|
+
label = connection.credentials.statement_label
|
|
232
|
+
cursor = connection.handle.cursor(mode=resolved_mode)
|
|
233
|
+
_execute_query_with_retry(
|
|
234
|
+
cursor=cursor,
|
|
235
|
+
sql=sql,
|
|
236
|
+
bindings=bindings,
|
|
237
|
+
retryable_exceptions=retryable_exceptions,
|
|
238
|
+
retry_limit=retry_limit,
|
|
239
|
+
attempt=1,
|
|
240
|
+
statement_name=statement_name,
|
|
241
|
+
statement_label=label,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
result = self.get_response(cursor)
|
|
245
|
+
|
|
246
|
+
fire_event(
|
|
247
|
+
SQLQueryStatus(
|
|
248
|
+
status=str(result),
|
|
249
|
+
elapsed=time.perf_counter() - pre,
|
|
250
|
+
node_info=get_node_info(),
|
|
251
|
+
query_id=result.query_id,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return connection, cursor
|
|
256
|
+
|
|
257
|
+
@contextmanager
|
|
258
|
+
def exception_handler(self, sql: str):
|
|
259
|
+
"""
|
|
260
|
+
Returns a context manager, that will handle exceptions raised
|
|
261
|
+
from queries, catch, log, and raise dbt exceptions it knows how to handle.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
yield
|
|
265
|
+
except confluent_sql.Error as e:
|
|
266
|
+
# TODO: Use logger, or fire a dbt event? Or both?
|
|
267
|
+
msg = f"confluent_sql error for '{sql}': {e}"
|
|
268
|
+
logger.debug(msg)
|
|
269
|
+
raise DbtDatabaseError(msg) from e
|
|
270
|
+
except Exception as e:
|
|
271
|
+
msg = f"Error running SQL '{sql}': {e}"
|
|
272
|
+
logger.debug(msg)
|
|
273
|
+
raise DbtRuntimeError(msg) from e
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def open(cls, connection):
|
|
277
|
+
"""
|
|
278
|
+
Receives a connection object and a Credentials object
|
|
279
|
+
and moves it to the "open" state.
|
|
280
|
+
"""
|
|
281
|
+
if connection.state is ConnectionState.OPEN:
|
|
282
|
+
# TODO: Use logger, or fire a dbt event? Or both?
|
|
283
|
+
logger.debug("Connection is already open, skipping open.")
|
|
284
|
+
return connection
|
|
285
|
+
|
|
286
|
+
credentials = connection.credentials
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
# This is hardcoded here as we don't want this to be customizable
|
|
290
|
+
user_agent = f"Confluent-dbt/v{version}"
|
|
291
|
+
|
|
292
|
+
handle = confluent_sql.connect(
|
|
293
|
+
flink_api_key=credentials.flink_api_key,
|
|
294
|
+
flink_api_secret=credentials.flink_api_secret,
|
|
295
|
+
environment=credentials.database,
|
|
296
|
+
compute_pool_id=credentials.compute_pool_id,
|
|
297
|
+
organization_id=credentials.organization_id,
|
|
298
|
+
cloud_provider=credentials.cloud_provider,
|
|
299
|
+
cloud_region=credentials.cloud_region,
|
|
300
|
+
dbname=credentials.schema,
|
|
301
|
+
http_user_agent=user_agent,
|
|
302
|
+
)
|
|
303
|
+
connection.state = "open"
|
|
304
|
+
connection.handle = handle
|
|
305
|
+
return connection
|
|
306
|
+
except Exception as e:
|
|
307
|
+
connection.state = "fail"
|
|
308
|
+
connection.handle = None
|
|
309
|
+
raise ConnectionError("confluent_sql connection error") from e
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def get_response(cls, cursor):
|
|
313
|
+
"""
|
|
314
|
+
Gets a cursor object and returns adapter-specific information
|
|
315
|
+
about the last executed command generally a AdapterResponse object
|
|
316
|
+
that has items such as code, rows_affected,etc. can also just be a string ex. "OK"
|
|
317
|
+
if your cursor does not offer rich metadata.
|
|
318
|
+
"""
|
|
319
|
+
assert cursor.statement is not None, "Cursor has no active statement"
|
|
320
|
+
return AdapterResponse(f"{cursor._statement.phase}")
|
|
321
|
+
|
|
322
|
+
def cancel(self, connection):
|
|
323
|
+
"""
|
|
324
|
+
Gets a connection object and attempts to cancel any ongoing queries.
|
|
325
|
+
"""
|
|
326
|
+
connection.handle.close()
|
|
327
|
+
|
|
328
|
+
def commit(self):
|
|
329
|
+
# Confluent cloud SQL does not support transactions, so commit is a noop here.
|
|
330
|
+
# TODO: Should we raise an exception if a non supported feature is used instead?
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
def begin(self):
|
|
334
|
+
# Confluent cloud SQL does not support transactions, so begin is a noop here.
|
|
335
|
+
# TODO: Should we raise an exception if a non supported feature is used instead?
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def data_type_code_to_name(cls, type_code: int | str) -> str:
|
|
340
|
+
"""
|
|
341
|
+
Get the string representation of the data type from the type code.
|
|
342
|
+
|
|
343
|
+
Flink SQL returns type names like:
|
|
344
|
+
- ARRAY<STRING> → ARRAY
|
|
345
|
+
- MAP<INT, STRING> → MAP
|
|
346
|
+
- DECIMAL(10, 2) → DECIMAL
|
|
347
|
+
- ROW<field1 INT, field2 STRING> → ROW
|
|
348
|
+
|
|
349
|
+
This method extracts the base type name by removing type parameters.
|
|
350
|
+
"""
|
|
351
|
+
if isinstance(type_code, int):
|
|
352
|
+
# Confluent SQL library returns string type names, not numeric codes
|
|
353
|
+
# If we somehow get a numeric code, convert it to string
|
|
354
|
+
type_code = str(type_code)
|
|
355
|
+
|
|
356
|
+
# Remove generic type parameters (e.g., ARRAY<STRING> → ARRAY)
|
|
357
|
+
# and precision/scale parameters (e.g., DECIMAL(10,2) → DECIMAL)
|
|
358
|
+
base_type = type_code.split("(")[0].split("<")[0].strip().upper()
|
|
359
|
+
|
|
360
|
+
return base_type
|