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.
Files changed (41) hide show
  1. dbt/__init__.py +3 -0
  2. dbt/adapters/confluent/__init__.py +16 -0
  3. dbt/adapters/confluent/__version__.py +1 -0
  4. dbt/adapters/confluent/column.py +14 -0
  5. dbt/adapters/confluent/connections.py +360 -0
  6. dbt/adapters/confluent/impl.py +313 -0
  7. dbt/adapters/confluent/utils.py +78 -0
  8. dbt/include/confluent/__init__.py +3 -0
  9. dbt/include/confluent/dbt_project.yml +7 -0
  10. dbt/include/confluent/macros/adapters/columns.sql +19 -0
  11. dbt/include/confluent/macros/adapters/metadata.sql +95 -0
  12. dbt/include/confluent/macros/adapters/relation.sql +8 -0
  13. dbt/include/confluent/macros/adapters/schema.sql +14 -0
  14. dbt/include/confluent/macros/etc/statement.sql +37 -0
  15. dbt/include/confluent/macros/get_custom_name/get_custom_schema.sql +9 -0
  16. dbt/include/confluent/macros/materializations/models/helpers.sql +15 -0
  17. dbt/include/confluent/macros/materializations/models/incremental/incremental.sql +6 -0
  18. dbt/include/confluent/macros/materializations/models/materialized_view.sql +26 -0
  19. dbt/include/confluent/macros/materializations/models/streaming_source.sql +47 -0
  20. dbt/include/confluent/macros/materializations/models/streaming_table.sql +52 -0
  21. dbt/include/confluent/macros/materializations/models/table.sql +24 -0
  22. dbt/include/confluent/macros/materializations/models/view.sql +24 -0
  23. dbt/include/confluent/macros/materializations/seeds/helpers.sql +49 -0
  24. dbt/include/confluent/macros/materializations/tests/helpers.sql +22 -0
  25. dbt/include/confluent/macros/materializations/tests/test.sql +70 -0
  26. dbt/include/confluent/macros/materializations/tests/unit.sql +85 -0
  27. dbt/include/confluent/macros/relations/drop.sql +13 -0
  28. dbt/include/confluent/macros/relations/materialized_view/alter.sql +5 -0
  29. dbt/include/confluent/macros/relations/materialized_view/create.sql +7 -0
  30. dbt/include/confluent/macros/relations/materialized_view/drop.sql +6 -0
  31. dbt/include/confluent/macros/relations/materialized_view/refresh.sql +5 -0
  32. dbt/include/confluent/macros/relations/table/create.sql +20 -0
  33. dbt/include/confluent/macros/relations/table/drop.sql +5 -0
  34. dbt/include/confluent/macros/relations/views/drop.sql +4 -0
  35. dbt/include/confluent/macros/timestamps.sql +12 -0
  36. dbt/include/confluent/profile_template.yml +38 -0
  37. dbt_confluent-0.1.0.dist-info/METADATA +192 -0
  38. dbt_confluent-0.1.0.dist-info/RECORD +41 -0
  39. dbt_confluent-0.1.0.dist-info/WHEEL +4 -0
  40. dbt_confluent-0.1.0.dist-info/entry_points.txt +2 -0
  41. dbt_confluent-0.1.0.dist-info/licenses/LICENSE +177 -0
dbt/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pkgutil import extend_path
2
+
3
+ __path__ = extend_path(__path__, __name__)
@@ -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