dbt-ydb 0.0.1__tar.gz

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_ydb-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.3
2
+ Name: dbt-ydb
3
+ Version: 0.0.1
4
+ Summary: DBT adapter for YDB
5
+ Author: Yandex LLC
6
+ Author-email: ydb@yandex-team.ru
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: dbt-adapters (>=1.14.8,<2.0.0)
14
+ Requires-Dist: dbt-common (>=1.23.0,<2.0.0)
15
+ Requires-Dist: dbt-core (>=1.8.0,<2.0.0)
16
+ Requires-Dist: ydb-dbapi (>=0.1.12,<0.2.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ <p align="center">
20
+ <img src="https://raw.githubusercontent.com/dbt-labs/dbt/ec7dee39f793aa4f7dd3dae37282cc87664813e4/etc/dbt-logo-full.svg" alt="dbt logo" width="500"/>
21
+ </p>
22
+
23
+ # dbt-ydb
24
+
25
+ **dbt-ydb** is a plugin for [dbt](https://www.getdbt.com/) that provides support for working with [YDB](https://ydb.tech).
26
+
27
+ ## Installation
28
+
29
+ To install plugin, execute the following command:
30
+
31
+ ```bash
32
+ pip install dbt-ydb
33
+ ```
34
+
35
+ ## Supported features
36
+
37
+ - [x] Table materialization
38
+ - [x] View materialization
39
+ - [x] Seeds
40
+ - [x] Docs generate
41
+ - [x] Tests
42
+ - [ ] TBD
@@ -0,0 +1,24 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/dbt-labs/dbt/ec7dee39f793aa4f7dd3dae37282cc87664813e4/etc/dbt-logo-full.svg" alt="dbt logo" width="500"/>
3
+ </p>
4
+
5
+ # dbt-ydb
6
+
7
+ **dbt-ydb** is a plugin for [dbt](https://www.getdbt.com/) that provides support for working with [YDB](https://ydb.tech).
8
+
9
+ ## Installation
10
+
11
+ To install plugin, execute the following command:
12
+
13
+ ```bash
14
+ pip install dbt-ydb
15
+ ```
16
+
17
+ ## Supported features
18
+
19
+ - [x] Table materialization
20
+ - [x] View materialization
21
+ - [x] Seeds
22
+ - [x] Docs generate
23
+ - [x] Tests
24
+ - [ ] TBD
@@ -0,0 +1,11 @@
1
+ from dbt.adapters.ydb.connections import YDBConnectionManager # noqa
2
+ from dbt.adapters.ydb.connections import YDBCredentials
3
+ from dbt.adapters.ydb.impl import YDBAdapter
4
+
5
+ from dbt.adapters.base import AdapterPlugin
6
+ from dbt.include import ydb
7
+
8
+
9
+ Plugin = AdapterPlugin(
10
+ adapter=YDBAdapter, credentials=YDBCredentials, include_path=ydb.PACKAGE_PATH
11
+ )
@@ -0,0 +1 @@
1
+ version = "0.0.1"
@@ -0,0 +1,136 @@
1
+ import re
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, List, Literal, TypeVar
4
+
5
+ from dbt.adapters.base.column import Column
6
+ from dbt_common.exceptions import DbtRuntimeError
7
+
8
+ Self = TypeVar('Self', bound='YDBColumn')
9
+
10
+
11
+ @dataclass
12
+ class YDBColumn(Column):
13
+ TYPE_LABELS = {
14
+ 'STRING': 'String',
15
+ 'TIMESTAMP': 'DateTime',
16
+ 'FLOAT': 'Float32',
17
+ 'INTEGER': 'Int32',
18
+ }
19
+ is_nullable: bool = False
20
+ is_low_cardinality: bool = False
21
+ _low_card_regex = re.compile(r'^LowCardinality\((.*)\)$')
22
+ _nullable_regex = re.compile(r'^(.*)\?$')
23
+ _fix_size_regex = re.compile(r'FixedString\((.*?)\)')
24
+ _decimal_regex = re.compile(r'Decimal\((\d+), (\d+)\)')
25
+
26
+ def __init__(self, column: str, dtype: str) -> None:
27
+ char_size = None
28
+ numeric_precision = None
29
+ numeric_scale = None
30
+
31
+ dtype = self._inner_dtype(dtype)
32
+
33
+ if dtype.lower().startswith('fixedstring'):
34
+ match_sized = self._fix_size_regex.search(dtype)
35
+ if match_sized:
36
+ char_size = int(match_sized.group(1))
37
+
38
+ if dtype.lower().startswith('decimal'):
39
+ match_dec = self._decimal_regex.search(dtype)
40
+ numeric_precision = 0
41
+ numeric_scale = 0
42
+ if match_dec:
43
+ numeric_precision = int(match_dec.group(1))
44
+ numeric_scale = int(match_dec.group(2))
45
+
46
+ super().__init__(column, dtype, char_size, numeric_precision, numeric_scale)
47
+
48
+ def __repr__(self) -> str:
49
+ return f'<YDBColumn {self.name} ({self.data_type}, is nullable: {self.is_nullable})>'
50
+
51
+ @property
52
+ def data_type(self) -> str:
53
+ if self.is_string():
54
+ data_t = self.string_type(self.string_size())
55
+ elif self.is_numeric():
56
+ data_t = self.numeric_type(self.dtype, self.numeric_precision, self.numeric_scale)
57
+ else:
58
+ data_t = self.dtype
59
+
60
+ if self.is_nullable or self.is_low_cardinality:
61
+ data_t = self.nested_type(data_t, self.is_low_cardinality, self.is_nullable)
62
+
63
+ return data_t
64
+
65
+ def is_string(self) -> bool:
66
+ return self.dtype.lower() in [
67
+ 'string',
68
+ 'fixedstring',
69
+ 'longblob',
70
+ 'longtext',
71
+ 'tinytext',
72
+ 'text',
73
+ 'varchar',
74
+ 'mediumblob',
75
+ 'blob',
76
+ 'tinyblob',
77
+ 'char',
78
+ 'mediumtext',
79
+ ] or self.dtype.lower().startswith('fixedstring')
80
+
81
+ def is_integer(self) -> bool:
82
+ return self.dtype.lower().startswith('int') or self.dtype.lower().startswith('uint')
83
+
84
+ def is_numeric(self) -> bool:
85
+ return self.dtype.lower().startswith('decimal')
86
+
87
+ def is_float(self) -> bool:
88
+ return self.dtype.lower().startswith('float')
89
+
90
+ def string_size(self) -> int:
91
+ if not self.is_string():
92
+ raise DbtRuntimeError('Called string_size() on non-string field!')
93
+
94
+ if not self.dtype.lower().startswith('fixedstring') or self.char_size is None:
95
+ return 256
96
+ else:
97
+ return int(self.char_size)
98
+
99
+ @classmethod
100
+ def string_type(cls, size: int) -> str:
101
+ return 'String'
102
+
103
+ @classmethod
104
+ def numeric_type(cls, dtype: str, precision: Any, scale: Any) -> str:
105
+ return f'Decimal({precision}, {scale})'
106
+
107
+ @classmethod
108
+ def nested_type(cls, dtype: str, is_low_cardinality: bool, is_nullable: bool) -> str:
109
+ template = "{}"
110
+ if is_low_cardinality:
111
+ template = template.format("LowCardinality({})")
112
+ if is_nullable:
113
+ template = template.format("Nullable({})")
114
+ return template.format(dtype)
115
+
116
+ def literal(self, value):
117
+ return f'to{self.dtype}({value})'
118
+
119
+ def can_expand_to(self, other_column: 'Column') -> bool:
120
+ if not self.is_string() or not other_column.is_string():
121
+ return False
122
+
123
+ return other_column.string_size() > self.string_size()
124
+
125
+ def _inner_dtype(self, dtype) -> str:
126
+ inner_dtype = dtype.strip()
127
+
128
+ if low_card_match := self._low_card_regex.search(inner_dtype):
129
+ self.is_low_cardinality = True
130
+ inner_dtype = low_card_match.group(1)
131
+
132
+ if null_match := self._nullable_regex.search(inner_dtype):
133
+ self.is_nullable = True
134
+ inner_dtype = null_match.group(1)
135
+
136
+ return inner_dtype
@@ -0,0 +1,179 @@
1
+ from contextlib import contextmanager
2
+ from dataclasses import dataclass
3
+ import dbt.exceptions
4
+ from dbt.adapters.contracts.connection import AdapterResponse
5
+ from dbt.adapters.contracts.connection import Credentials
6
+ from dbt.adapters.contracts.connection import Connection
7
+ import time
8
+ from dbt.adapters.sql import SQLConnectionManager
9
+
10
+ from dbt.adapters.events.logging import AdapterLogger
11
+
12
+ import ydb_dbapi as dbapi
13
+
14
+ from typing import Any, Optional, Tuple, TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ import agate
18
+
19
+
20
+ logger = AdapterLogger("YDB")
21
+
22
+
23
+ @dataclass
24
+ class YDBCredentials(Credentials):
25
+ """
26
+ Defines database specific credentials that get added to
27
+ profiles.yml to connect to new adapter
28
+ """
29
+
30
+ # Add credentials members here, like:
31
+ host: str = "localhost"
32
+ port: int = 2136
33
+ username: Optional[str] = None
34
+ password: Optional[str] = None
35
+
36
+ schema: str = ""
37
+
38
+ _ALIASES = {"dbname": "database", "pass": "password", "user": "username"}
39
+
40
+ @property
41
+ def type(self):
42
+ """Return name of adapter."""
43
+ return "ydb"
44
+
45
+ @property
46
+ def unique_field(self):
47
+ """
48
+ Hashed and included in anonymous telemetry to track adapter adoption.
49
+ Pick a field that can uniquely identify one team/organization building with this adapter
50
+ """
51
+ return self.host
52
+
53
+ def _connection_keys(self):
54
+ """
55
+ List of keys to display in the `dbt debug` output.
56
+ """
57
+ return ("host", "port", "username", "user", "database", "schema")
58
+
59
+ def _get_ydb_credentials(self):
60
+ import ydb
61
+
62
+ if self.username is not None:
63
+ return ydb.StaticCredentials.from_user_password(
64
+ self.username, self.password
65
+ )
66
+
67
+ return ydb.AnonymousCredentials()
68
+
69
+
70
+ class YDBConnectionManager(SQLConnectionManager):
71
+ TYPE = "ydb"
72
+
73
+ @contextmanager
74
+ def exception_handler(self, sql: str):
75
+ """
76
+ Returns a context manager, that will handle exceptions raised
77
+ from queries, catch, log, and raise dbt exceptions it knows how to handle.
78
+ """
79
+ ## Example ##
80
+ try:
81
+ yield
82
+ except dbapi.DatabaseError as exc:
83
+ self.release()
84
+
85
+ logger.debug("YDB error: {}".format(str(exc)))
86
+ raise dbt.exceptions.DbtRuntimeError(str(exc))
87
+ except Exception as exc:
88
+ logger.debug("Error running SQL: {}".format(sql))
89
+ logger.debug("Rolling back transaction.")
90
+ self.release()
91
+ raise dbt.exceptions.DbtRuntimeError(str(exc))
92
+
93
+ @classmethod
94
+ def open(cls, connection):
95
+ """
96
+ Receives a connection object and a Credentials object
97
+ and moves it to the "open" state.
98
+ """
99
+ ## Example ##
100
+ if connection.state == "open":
101
+ logger.debug("Connection is already open, skipping open.")
102
+ return connection
103
+
104
+ credentials = connection.credentials
105
+
106
+ try:
107
+ handle = dbapi.connect(
108
+ host=credentials.host,
109
+ port=credentials.port,
110
+ database=credentials.database,
111
+ credentials=credentials._get_ydb_credentials(),
112
+ )
113
+ logger.debug("Connect success")
114
+ connection.state = "open"
115
+ connection.handle = handle
116
+ except dbapi.Error as exc:
117
+ logger.debug("YDB error: {}".format(str(exc)))
118
+ raise dbt.exceptions.DatabaseException(str(exc))
119
+
120
+ return connection
121
+
122
+ @classmethod
123
+ def get_response(cls, _):
124
+ return "OK"
125
+
126
+ def cancel(self, connection):
127
+ """
128
+ Gets a connection object and attempts to cancel any ongoing queries.
129
+ """
130
+ connection_name = connection.name
131
+ logger.debug("Cancelling query '{}'", connection_name)
132
+ connection.handle.close()
133
+ logger.debug("Cancel query '{}'", connection_name)
134
+
135
+ def begin(self):
136
+ pass
137
+
138
+ def commit(self):
139
+ pass
140
+
141
+ def execute(
142
+ self,
143
+ sql: str,
144
+ auto_begin: bool = False,
145
+ fetch: bool = False,
146
+ limit: Optional[int] = None,
147
+ ) -> Tuple[AdapterResponse, "agate.Table"]:
148
+ from dbt_common.clients.agate_helper import empty_table
149
+
150
+ sql = self._add_query_comment(sql)
151
+ _, cursor = self.add_query(sql, auto_begin)
152
+ response = self.get_response(cursor)
153
+ if fetch:
154
+ table = self.get_result_from_cursor(cursor, limit)
155
+ else:
156
+ table = empty_table()
157
+ return AdapterResponse(response), table
158
+
159
+ def add_query(
160
+ self,
161
+ sql: str,
162
+ auto_begin: bool = True,
163
+ bindings: Optional[Any] = None,
164
+ abridge_sql_log: bool = False,
165
+ ) -> Tuple[Connection, Any]:
166
+ sql = self._add_query_comment(sql)
167
+ conn = self.get_thread_connection()
168
+ dbapi_connection = conn.handle
169
+ with self.exception_handler(sql):
170
+ logger.debug(f"On {conn.name}: {sql}...")
171
+ pre = time.time()
172
+ cursor = dbapi_connection.cursor()
173
+ logger.info(f"Try to execute SQL: \n{sql}")
174
+ cursor.execute_scheme(sql) # TODO: split
175
+ status = self.get_response(cursor)
176
+ logger.info(
177
+ f"SQL status: {status} in {(time.time() - pre):0.2f} seconds"
178
+ )
179
+ return conn, cursor
@@ -0,0 +1,333 @@
1
+ from dbt.adapters.sql import SQLAdapter
2
+ from dbt.adapters.base.relation import BaseRelation
3
+ from dbt.adapters.base.impl import ConstraintSupport, ConstraintType
4
+ from dbt.adapters.cache import _make_ref_key_dict
5
+ from dbt.adapters.events.types import ColTypeChange, SchemaCreation, SchemaDrop
6
+ import dbt_common.clients
7
+ from dbt_common.exceptions import DbtRuntimeError, CompilationError
8
+ from dbt_common.events.functions import fire_event
9
+ from dbt.adapters.base.relation import InformationSchema
10
+
11
+ from dbt.adapters.base.meta import AdapterMeta, available
12
+ from dbt.adapters.events.logging import AdapterLogger
13
+ from dbt_common.clients.agate_helper import DEFAULT_TYPE_TESTER
14
+ # from dbt.adapters.base.column import Column
15
+
16
+ from dbt.adapters.ydb import YDBConnectionManager
17
+ from dbt.adapters.ydb.column import YDBColumn
18
+ from dbt.adapters.ydb.relation import YDBRelation
19
+
20
+ from typing import Any, Dict, FrozenSet, Iterable, List, TYPE_CHECKING, Optional, Set, Tuple
21
+
22
+ import traceback
23
+
24
+ if TYPE_CHECKING:
25
+ import agate
26
+
27
+ logger = AdapterLogger("YDB")
28
+
29
+ class YDBAdapter(SQLAdapter):
30
+ """
31
+ Controls actual implmentation of adapter, and ability to override certain methods.
32
+ """
33
+
34
+ ConnectionManager = YDBConnectionManager
35
+ Column = YDBColumn
36
+ Relation = YDBRelation
37
+
38
+ CONSTRAINT_SUPPORT = {
39
+ ConstraintType.check: ConstraintSupport.NOT_SUPPORTED,
40
+ ConstraintType.not_null: ConstraintSupport.NOT_SUPPORTED,
41
+ ConstraintType.unique: ConstraintSupport.NOT_SUPPORTED,
42
+ ConstraintType.primary_key: ConstraintSupport.ENFORCED,
43
+ ConstraintType.foreign_key: ConstraintSupport.NOT_SUPPORTED,
44
+ }
45
+
46
+ @classmethod
47
+ def quote(self, identifier):
48
+ return '`{}`'.format(identifier)
49
+
50
+ @classmethod
51
+ def date_function(cls):
52
+ """
53
+ Returns canonical date func
54
+ """
55
+ return "CurrentUtcDate()"
56
+
57
+ @classmethod
58
+ def convert_text_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
59
+ return 'Utf8'
60
+
61
+ @classmethod
62
+ def convert_number_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
63
+ import agate
64
+
65
+ decimals = agate_table.aggregate(agate.MaxPrecision(col_idx))
66
+ return 'Double' if decimals else 'Int64'
67
+
68
+ @classmethod
69
+ def convert_datetime_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
70
+ return 'DateTime'
71
+
72
+ @classmethod
73
+ def convert_date_type(cls, agate_table: "agate.Table", col_idx: int) -> str:
74
+ return 'Date'
75
+
76
+ def list_relations_without_caching(
77
+ self,
78
+ schema_relation: BaseRelation,
79
+ ) -> List[BaseRelation]:
80
+ if not self.check_schema_exists(schema_relation.database, schema_relation.schema):
81
+ logger.info(f"Unable to list relations in schema {schema_relation.schema}: schema does not exist")
82
+ return []
83
+
84
+ connection = self.connections.get_thread_connection()
85
+ dbapi_connection = connection.handle
86
+
87
+ current_prefix = dbapi_connection.table_path_prefix
88
+ dbapi_connection.table_path_prefix = schema_relation.schema
89
+
90
+ relations = []
91
+
92
+ for relation_type, relation_list in [
93
+ ("table", dbapi_connection.get_table_names()),
94
+ ("view", dbapi_connection.get_view_names()),
95
+ ]:
96
+ for relation in relation_list:
97
+ r = self.Relation.create(
98
+ database=schema_relation.database,
99
+ schema=schema_relation.schema,
100
+ identifier=relation,
101
+ type=relation_type,
102
+ )
103
+
104
+ relations.append(r)
105
+
106
+ dbapi_connection.table_path_prefix = current_prefix
107
+
108
+ return relations
109
+
110
+ def get_columns_in_relation(self, relation):
111
+ connection = self.connections.get_thread_connection()
112
+ dbapi_connection = connection.handle
113
+
114
+ logger.info(f"Try to get columns from {relation}")
115
+
116
+ rel_str = relation.render().replace('`', '')
117
+
118
+ if not dbapi_connection.check_exists(rel_str):
119
+ logger.info(f"Relation {rel_str} does not exist, unable to get columns")
120
+ return []
121
+
122
+ with dbapi_connection.cursor() as cur:
123
+ cur.execute(f"SELECT * FROM {relation} LIMIT 0")
124
+ return [YDBColumn(col[0], col[1]) for col in cur.description]
125
+
126
+ def get_rows_different_sql(
127
+ self,
128
+ relation_a: YDBRelation,
129
+ relation_b: YDBRelation,
130
+ column_names: Optional[List[str]] = None,
131
+ except_operator: Optional[str] = None,
132
+ ) -> str:
133
+ names: List[str]
134
+ if column_names is None:
135
+ columns = self.get_columns_in_relation(relation_a)
136
+ names = sorted((self.quote(c.name) for c in columns))
137
+ else:
138
+ names = sorted((self.quote(n) for n in column_names))
139
+
140
+ alias_a = 'ta'
141
+ alias_b = 'tb'
142
+ columns_csv_a = ', '.join([f'{alias_a}.{name}' for name in names])
143
+ columns_csv_b = ', '.join([f'{alias_b}.{name}' for name in names])
144
+ join_condition = ' AND '.join([f'{alias_a}.{name} = {alias_b}.{name}' for name in names])
145
+ first_column = names[0]
146
+
147
+ sql = COLUMNS_EQUAL_SQL.format(
148
+ alias_a=alias_a,
149
+ alias_b=alias_b,
150
+ first_column=first_column,
151
+ columns_a=columns_csv_a,
152
+ columns_b=columns_csv_b,
153
+ join_condition=join_condition,
154
+ relation_a=str(relation_a),
155
+ relation_b=str(relation_b),
156
+ )
157
+
158
+ return sql
159
+
160
+ def check_schema_exists(self, database: str, schema: str) -> bool:
161
+ connection = self.connections.get_thread_connection()
162
+ dbapi_connection = connection.handle
163
+
164
+ return dbapi_connection.check_exists(schema)
165
+
166
+ def create_schema(self, relation: BaseRelation) -> None:
167
+ relation = relation.without_identifier()
168
+ fire_event(SchemaCreation(relation=_make_ref_key_dict(relation)))
169
+
170
+ connection = self.connections.get_thread_connection()
171
+ dbapi_connection = connection.handle
172
+
173
+ if not relation.schema:
174
+ return
175
+
176
+ directory = f"{relation.database}/{relation.schema}"
177
+
178
+ dbapi_connection._driver.scheme_client.make_directory(directory)
179
+
180
+ def drop_schema(self, relation):
181
+ logger.info("DROP SCHEMA")
182
+ # traceback.print_stack()
183
+
184
+ if not self.check_schema_exists(relation.database, relation.schema):
185
+ logger.info("Unable to drop schema: does not exist")
186
+ return
187
+ relation = relation.without_identifier()
188
+ fire_event(SchemaDrop(relation=_make_ref_key_dict(relation)))
189
+
190
+ for inner_relation in self.list_relations_without_caching(relation):
191
+ logger.info(f"Drop child relation before drop schema: {inner_relation}")
192
+ self.drop_relation(inner_relation)
193
+
194
+ connection = self.connections.get_thread_connection()
195
+ dbapi_connection = connection.handle
196
+
197
+ if not relation.schema:
198
+ return
199
+
200
+ directory = f"{relation.database}/{relation.schema}"
201
+ dbapi_connection._driver.scheme_client.remove_directory(directory)
202
+
203
+ # we can update the cache here
204
+ self.cache.drop_schema(relation.database, relation.schema)
205
+
206
+ logger.debug("DROP SCHEMA SUCCESS")
207
+
208
+ @available
209
+ def prepare_insert_values_from_csv(self, table):
210
+ import agate
211
+ csv_funcs = [c.csvify for c in table._column_types]
212
+
213
+ out = []
214
+ def prepare_value(i, val):
215
+ ctype = table._column_types[i]
216
+ if isinstance(ctype, agate.data_types.DateTime):
217
+ return f'DateTime("{val}Z")'
218
+ if isinstance(ctype, agate.data_types.Date):
219
+ return f'Date("{val}")'
220
+ if isinstance(ctype, dbt_common.clients.agate_helper.Number):
221
+ return f"{val}"
222
+ return f'"{val}"'
223
+
224
+ for row in table.rows:
225
+ out.append(f"({', '.join(prepare_value(i, csv_funcs[i](d)) for i, d in enumerate(row))})")
226
+
227
+ return ",".join(out)
228
+
229
+ def rename_relation(self, from_relation: YDBRelation, to_relation: YDBRelation):
230
+ logger.debug(f"Try to rename relation from {from_relation}[{str(from_relation.type)}] to {to_relation}[{str(to_relation.type)}]")
231
+ self.drop_relation(to_relation)
232
+ if from_relation.type == "table":
233
+ import ydb
234
+ connection = self.connections.get_thread_connection()
235
+ dbapi_connection = connection.handle
236
+
237
+ from_path = f"{from_relation.database}/{from_relation.render()[1:-1]}".strip()
238
+ to_path = f"{to_relation.database}/{to_relation.render()[1:-1]}".strip()
239
+
240
+ rename_item = ydb.RenameItem(
241
+ from_path,
242
+ to_path,
243
+ )
244
+
245
+ dbapi_connection._driver.table_client.rename_tables([rename_item])
246
+ else:
247
+ raise DbtRuntimeError("View rename does not supported in YDB")
248
+ self.cache_renamed(from_relation, to_relation)
249
+
250
+ def get_catalog_for_single_relation(self, relation):
251
+ columns: List[Dict[str, Any]] = []
252
+ logger.debug("Getting table schema for relation {}", str(relation))
253
+ columns.extend(self.parse_columns_from_relation(relation))
254
+
255
+ import agate
256
+
257
+ return agate.Table.from_object(columns, column_types=DEFAULT_TYPE_TESTER)
258
+
259
+ def _get_one_catalog(
260
+ self,
261
+ information_schema: InformationSchema,
262
+ schemas: Set[str],
263
+ used_schemas: FrozenSet[Tuple[str, str]],
264
+ ) -> "agate.Table":
265
+ if len(schemas) != 1:
266
+ raise CompilationError(
267
+ f"Expected only one schema in ydb _get_one_catalog, found " f"{schemas}"
268
+ )
269
+
270
+ database = information_schema.database
271
+ schema = list(schemas)[0]
272
+
273
+ columns: List[Dict[str, Any]] = []
274
+ relations = self.list_relations(database, schema)
275
+ for relation in relations:
276
+ logger.debug("Getting table schema for relation {}", str(relation))
277
+ columns.extend(self.parse_columns_from_relation(relation))
278
+
279
+ import agate
280
+
281
+ return agate.Table.from_object(columns, column_types=DEFAULT_TYPE_TESTER)
282
+
283
+ def parse_columns_from_relation(self, relation: BaseRelation) -> List[Dict[str, Any]]:
284
+ owner = ""
285
+ ydb_columns = self.get_columns_in_relation(relation)
286
+ columns = []
287
+ for col_num, col in enumerate(ydb_columns):
288
+ column = {
289
+ "table_database": relation.database,
290
+ "table_schema": relation.schema,
291
+ "table_name": relation.identifier,
292
+ "table_type": relation.type,
293
+ "column_index": col_num,
294
+ "table_owner": owner,
295
+ "column_name": col.name,
296
+ "column_type": col.dtype
297
+ }
298
+ columns.append(column)
299
+ return columns
300
+
301
+
302
+
303
+ COLUMNS_EQUAL_SQL = '''
304
+ $rel_a = SELECT COUNT(*) as num_rows FROM {relation_a};
305
+ $rel_b = SELECT COUNT(*) as num_rows FROM {relation_b};
306
+ SELECT
307
+ row_count_diff.difference as row_count_difference,
308
+ diff_count.num_missing as num_mismatched
309
+ FROM (
310
+ SELECT
311
+ 1 as id,
312
+ $rel_a - $rel_b as difference
313
+ ) as row_count_diff
314
+ INNER JOIN (
315
+ SELECT
316
+ 1 as id,
317
+ COUNT(*) as num_missing FROM (
318
+ SELECT
319
+ {columns_a}
320
+ FROM {relation_a} as {alias_a}
321
+ LEFT OUTER JOIN {relation_b} as {alias_b}
322
+ ON {join_condition}
323
+ WHERE {alias_b}.{first_column} IS NULL
324
+ UNION ALL
325
+ SELECT
326
+ {columns_b}
327
+ FROM {relation_b} as {alias_b}
328
+ LEFT OUTER JOIN {relation_a} as {alias_a}
329
+ ON {join_condition}
330
+ WHERE {alias_a}.{first_column} IS NULL
331
+ ) as missing
332
+ ) as diff_count ON row_count_diff.id = diff_count.id
333
+ '''.strip()
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from dbt.adapters.base.relation import BaseRelation, Policy
4
+
5
+
6
+ @dataclass
7
+ class YDBQuotePolicy(Policy):
8
+ database: bool = True
9
+ schema: bool = True
10
+ identifier: bool = True
11
+
12
+ @dataclass
13
+ class YDBIncludePolicy(Policy):
14
+ database: bool = False
15
+ schema: bool = True
16
+ identifier: bool = True
17
+
18
+ @dataclass(frozen=True, eq=False, repr=False)
19
+ class YDBRelation(BaseRelation):
20
+ quote_character: str = '`'
21
+ quote_policy: Policy = field(default_factory=lambda: YDBQuotePolicy())
22
+ include_policy: Policy = field(default_factory=lambda: YDBIncludePolicy())
23
+
24
+ def render(self) -> str:
25
+ # if there is nothing set, this will return the empty string.
26
+ return self.quoted("/".join([part for part in [self.schema, self.identifier] if part]))
@@ -0,0 +1,3 @@
1
+ import os
2
+
3
+ PACKAGE_PATH = os.path.dirname(__file__)
@@ -0,0 +1,5 @@
1
+ name: dbt_ydb
2
+ version: 0.0.0
3
+ config-version: 2
4
+
5
+ macro-paths: ["macros"]
@@ -0,0 +1,191 @@
1
+ /* For examples of how to fill out the macros please refer to the postgres adapter and docs
2
+ postgres adapter macros: https://github.com/dbt-labs/dbt-core/blob/main/plugins/postgres/dbt/include/postgres/macros/adapters.sql
3
+ dbt docs: https://docs.getdbt.com/docs/contributing/building-a-new-adapter
4
+ */
5
+
6
+ {% macro ydb__alter_column_type(relation,column_name,new_column_type) -%}
7
+ '''Changes column name or data type'''
8
+ /*
9
+ 1. Create a new column (w/ temp name and correct type)
10
+ 2. Copy data over to it
11
+ 3. Drop the existing column (cascade!)
12
+ 4. Rename the new column to existing column
13
+ */
14
+ {% endmacro %}
15
+
16
+ {% macro ydb__check_schema_exists(information_schema,schema) -%}
17
+ '''Checks if schema name exists and returns number or times it shows up.'''
18
+ /*
19
+ 1. Check if schemas exist
20
+ 2. return number of rows or columns that match searched parameter
21
+ */
22
+ {% endmacro %}
23
+
24
+ -- Example from postgres adapter in dbt-core
25
+ -- Notice how you can build out other methods than the designated ones for the impl.py file,
26
+ -- to make a more robust adapter. ex. (verify_database)
27
+
28
+ /*
29
+
30
+ {% macro postgres__create_schema(relation) -%}
31
+ {% if relation.database -%}
32
+ {{ adapter.verify_database(relation.database) }}
33
+ {%- endif -%} {%- call statement('create_schema') -%}
34
+ create schema if not exists {{ relation.without_identifier().include(database=False) }}
35
+ {%- endcall -%}
36
+ {% endmacro %}
37
+
38
+ */
39
+
40
+ {% macro ydb__create_schema(relation) -%}
41
+ '''Creates a new schema in the target database, if schema already exists, method is a no-op. '''
42
+ {% endmacro %}
43
+
44
+ /*
45
+
46
+ {% macro postgres__drop_schema(relation) -%}
47
+ {% if relation.database -%}
48
+ {{ adapter.verify_database(relation.database) }}
49
+ {%- endif -%}
50
+ {%- call statement('drop_schema') -%}
51
+ drop schema if exists {{ relation.without_identifier().include(database=False) }} cascade
52
+ {%- endcall -%}
53
+ {% endmacro %}
54
+
55
+ */
56
+
57
+ {% macro ydb__drop_relation(relation) -%}
58
+ '''Deletes relatonship identifer between tables.'''
59
+ /*
60
+ 1. If database exists
61
+ 2. Create a new schema if passed schema does not exist already
62
+ */
63
+ {%- set obj_type = relation.type -%}
64
+ {% call statement('drop_relation', auto_begin=False) -%}
65
+ drop {{ obj_type }} if exists {{ relation }}
66
+ {%- endcall %}
67
+
68
+ {% endmacro %}
69
+
70
+ {% macro ydb__drop_schema(relation) -%}
71
+ '''drops a schema in a target database.'''
72
+ /*
73
+ 1. If database exists
74
+ 2. search all calls of schema, and change include value to False, cascade it to backtrack
75
+ */
76
+ {% endmacro %}
77
+
78
+ /*
79
+
80
+ Example of 1 of 3 required macros that does not have a default implementation
81
+ {% macro postgres__get_columns_in_relation(relation) -%}
82
+ {% call statement('get_columns_in_relation', fetch_result=True) %}
83
+ select
84
+ column_name,
85
+ data_type,
86
+ character_maximum_length,
87
+ numeric_precision,
88
+ numeric_scale
89
+ from {{ relation.information_schema('columns') }}
90
+ where table_name = '{{ relation.identifier }}'
91
+ {% if relation.schema %}
92
+ and table_schema = '{{ relation.schema }}'
93
+ {% endif %}
94
+ order by ordinal_position
95
+ {% endcall %}
96
+ {% set table = load_result('get_columns_in_relation').table %}
97
+ {{ return(sql_convert_columns_in_relation(table)) }}
98
+ {% endmacro %}
99
+ */
100
+
101
+
102
+ {% macro ydb__get_columns_in_relation(relation) -%}
103
+ '''Returns a list of Columns in a table.'''
104
+ /*
105
+ 1. select as many values from column as needed
106
+ 2. search relations to columns
107
+ 3. where table name is equal to the relation identifier
108
+ 4. if a relation schema exists and table schema is equal to the relation schema
109
+ 5. order in whatever way you want to call.
110
+ 6. create a table by loading result from call
111
+ 7. return new table
112
+ */
113
+ {% endmacro %}
114
+
115
+ -- Example of 2 of 3 required macros that do not come with a default implementation
116
+
117
+ /*
118
+
119
+ {% macro postgres__list_relations_without_caching(schema_relation) %}
120
+ {% call statement('list_relations_without_caching', fetch_result=True) -%}
121
+ select
122
+ '{{ schema_relation.database }}' as database,
123
+ tablename as name,
124
+ schemaname as schema,
125
+ 'table' as type
126
+ from pg_tables
127
+ where schemaname ilike '{{ schema_relation.schema }}'
128
+ union all
129
+ select
130
+ '{{ schema_relation.database }}' as database,
131
+ viewname as name,
132
+ schemaname as schema,
133
+ 'view' as type
134
+ from pg_views
135
+ where schemaname ilike '{{ schema_relation.schema }}'
136
+ {% endcall %}
137
+ {{ return(load_result('list_relations_without_caching').table) }}
138
+ {% endmacro %}
139
+
140
+ */
141
+
142
+ /*
143
+ {% macro ydb__list_relations_without_caching(schema_relation) -%}
144
+ {{ return(adapter.list_relations_without_caching(schema_relation)) }}
145
+ {%- endmacro %}
146
+ */
147
+
148
+ /*
149
+ {% macro ydb__list_schemas(database) -%}
150
+ '''Returns a table of unique schemas.'''
151
+ /*
152
+ 1. search schemea by specific name
153
+ 2. create a table with names
154
+ */
155
+ {% endmacro %}
156
+ */
157
+
158
+ {% macro ydb__rename_relation(from_relation, to_relation) -%}
159
+ '''Renames a relation in the database.'''
160
+ /*
161
+ 1. Search for a specific relation name
162
+ 2. alter table by targeting specific name and passing in new name
163
+ */
164
+ {% if from_relation.type == "table" %}
165
+ {{ adapter.rename_table(from_relation, to_relation) }}
166
+
167
+ {% endif %}
168
+ {% endmacro %}
169
+
170
+ {% macro ydb__truncate_relation(relation) -%}
171
+ '''Removes all rows from a targeted set of tables.'''
172
+ /*
173
+ 1. grab all tables tied to the relation
174
+ 2. remove rows from relations
175
+ */
176
+ {% endmacro %}
177
+
178
+ /*
179
+
180
+ Example 3 of 3 of required macros that does not have a default implementation.
181
+ ** Good example of building out small methods ** please refer to impl.py for implementation of now() in postgres plugin
182
+ {% macro postgres__current_timestamp() -%}
183
+ now()
184
+ {%- endmacro %}
185
+
186
+ */
187
+
188
+ {% macro ydb__current_timestamp() -%}
189
+ '''Returns current UTC time'''
190
+ {# docs show not to be implemented currently. #}
191
+ {% endmacro %}
File without changes
@@ -0,0 +1,24 @@
1
+ {% macro ydb__test_accepted_values(model, column_name, values, quote=True) %}
2
+
3
+ select *
4
+ from (
5
+ select
6
+ {{ column_name }} as value_field,
7
+ count(*) as n_records
8
+
9
+ from {{ model }}
10
+ group by {{ column_name }}
11
+ ) all_values
12
+
13
+ where value_field not in (
14
+ {% for value in values -%}
15
+ {% if quote -%}
16
+ '{{ value }}'
17
+ {%- else -%}
18
+ {{ value }}
19
+ {%- endif -%}
20
+ {%- if not loop.last -%},{%- endif %}
21
+ {%- endfor %}
22
+ )
23
+
24
+ {% endmacro %}
@@ -0,0 +1,17 @@
1
+ {% macro ydb__test_relationships(model, column_name, to, field) %}
2
+
3
+ select
4
+ child.from_field
5
+ from (
6
+ select {{ column_name }} as from_field
7
+ from {{ model }}
8
+ where {{ column_name }} is not null
9
+ ) as child
10
+ left join (
11
+ select {{ field }} as to_field
12
+ from {{ to }}
13
+ ) as parent
14
+ on child.from_field = parent.to_field
15
+ where parent.to_field is null
16
+
17
+ {% endmacro %}
@@ -0,0 +1,107 @@
1
+
2
+ {% materialization incremental, adapter='ydb' -%}
3
+
4
+ -- relations
5
+ {%- set existing_relation = load_cached_relation(this) -%}
6
+
7
+ {%- set target_relation = this.incorporate(type='table') -%}
8
+ {%- set temp_relation = make_temp_relation(target_relation)-%}
9
+ {%- set intermediate_relation = make_intermediate_relation(target_relation)-%}
10
+ {%- set backup_relation_type = 'table' if existing_relation is none else existing_relation.type -%}
11
+ {%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
12
+
13
+ -- configs
14
+ {%- set unique_key = config.get('unique_key') -%}
15
+ {%- set full_refresh_mode = (should_full_refresh() or existing_relation.is_view) -%}
16
+ {%- set on_schema_change = incremental_validate_on_schema_change(config.get('on_schema_change'), default='ignore') -%}
17
+
18
+ -- the temp_ and backup_ relations should not already exist in the database; get_relation
19
+ -- will return None in that case. Otherwise, we get a relation that we can drop
20
+ -- later, before we try to use this name for the current operation. This has to happen before
21
+ -- BEGIN, in a separate transaction
22
+ {%- set preexisting_intermediate_relation = load_cached_relation(intermediate_relation)-%}
23
+ {%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
24
+ -- grab current tables grants config for comparision later on
25
+ {% set grant_config = config.get('grants') %}
26
+ {{ drop_relation_if_exists(preexisting_intermediate_relation) }}
27
+ {{ drop_relation_if_exists(preexisting_backup_relation) }}
28
+
29
+ {{ run_hooks(pre_hooks, inside_transaction=False) }}
30
+
31
+ -- `BEGIN` happens here:
32
+ {{ run_hooks(pre_hooks, inside_transaction=True) }}
33
+
34
+ {% set to_drop = [] %}
35
+
36
+ {% set incremental_strategy = config.get('incremental_strategy') or 'default' %}
37
+ {% set strategy_sql_macro_func = adapter.get_incremental_strategy_macro(context, incremental_strategy) %}
38
+
39
+ {% if existing_relation is none %}
40
+ {% set build_sql = get_create_table_as_sql(False, target_relation, sql) %}
41
+ {% set relation_for_indexes = target_relation %}
42
+ {% elif full_refresh_mode %}
43
+ {% set build_sql = get_create_table_as_sql(False, intermediate_relation, sql) %}
44
+ {% set relation_for_indexes = intermediate_relation %}
45
+ {% set need_swap = true %}
46
+ {% else %}
47
+ {% do run_query(get_create_table_as_sql(True, temp_relation, sql)) %}
48
+ {% set relation_for_indexes = temp_relation %}
49
+ {% set contract_config = config.get('contract') %}
50
+ {% if not contract_config or not contract_config.enforced %}
51
+ {% do adapter.expand_target_column_types(
52
+ from_relation=temp_relation,
53
+ to_relation=target_relation) %}
54
+ {% endif %}
55
+ {#-- Process schema changes. Returns dict of changes if successful. Use source columns for upserting/merging --#}
56
+ {% set dest_columns = process_schema_changes(on_schema_change, temp_relation, existing_relation) %}
57
+ {% if not dest_columns %}
58
+ {% set dest_columns = adapter.get_columns_in_relation(existing_relation) %}
59
+ {% endif %}
60
+
61
+ {#-- Get the incremental_strategy, the macro to use for the strategy, and build the sql --#}
62
+ {% set incremental_predicates = config.get('predicates', none) or config.get('incremental_predicates', none) %}
63
+ {% set strategy_arg_dict = ({'target_relation': target_relation, 'temp_relation': temp_relation, 'unique_key': unique_key, 'dest_columns': dest_columns, 'incremental_predicates': incremental_predicates }) %}
64
+ {% set build_sql = strategy_sql_macro_func(strategy_arg_dict) %}
65
+
66
+ {% endif %}
67
+
68
+ {% call statement("main") %}
69
+ {{ build_sql }}
70
+ {% endcall %}
71
+
72
+ {% if existing_relation is none or existing_relation.is_view or should_full_refresh() %}
73
+ {% do create_indexes(relation_for_indexes) %}
74
+ {% endif %}
75
+
76
+ {% if need_swap %}
77
+ {% if existing_relation.is_view %}
78
+ {% call statement('main') -%}
79
+ {{ get_create_view_as_sql(backup_relation, sql) }}
80
+ {%- endcall %}
81
+ {{ drop_relation(existing_relation) }}
82
+ {% else %}
83
+ {% do adapter.rename_relation(target_relation, backup_relation) %}
84
+ {% endif %}
85
+ {% do adapter.rename_relation(intermediate_relation, target_relation) %}
86
+ {% do to_drop.append(backup_relation) %}
87
+ {% endif %}
88
+
89
+ {% set should_revoke = should_revoke(existing_relation, full_refresh_mode) %}
90
+ {% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
91
+
92
+ {% do persist_docs(target_relation, model) %}
93
+
94
+ {{ run_hooks(post_hooks, inside_transaction=True) }}
95
+
96
+ -- `COMMIT` happens here
97
+ {% do adapter.commit() %}
98
+
99
+ {% for rel in to_drop %}
100
+ {% do adapter.drop_relation(rel) %}
101
+ {% endfor %}
102
+
103
+ {{ run_hooks(post_hooks, inside_transaction=False) }}
104
+
105
+ {{ return({'relations': [target_relation]}) }}
106
+
107
+ {%- endmaterialization %}
@@ -0,0 +1,19 @@
1
+ {% macro ydb__create_table_as(temporary, relation, sql) -%}
2
+ {%- set sql_header = config.get('sql_header', none) -%}
3
+ {%- set primary_key_column = model['config'].get('primary_key', 'id') -%}
4
+
5
+ {{ sql_header if sql_header is not none }}
6
+
7
+ create {% if temporary: -%}temporary{%- endif %} table
8
+ {{ relation.include(database=(not temporary), schema=(not temporary)) }}
9
+ (primary key ({{ primary_key_column }}))
10
+ {% set contract_config = config.get('contract') %}
11
+ {% if contract_config.enforced and (not temporary) %}
12
+ {{ get_assert_columns_equivalent(sql) }}
13
+ {{ get_table_columns_and_constraints() }}
14
+ {%- set sql = get_select_subquery(sql) %}
15
+ {% endif %}
16
+ as (
17
+ {{ sql }}
18
+ )
19
+ {%- endmacro %}
@@ -0,0 +1,91 @@
1
+ {%- materialization view, adapter='ydb' -%}
2
+
3
+ {%- set existing_relation = load_cached_relation(this) -%}
4
+ {%- set target_relation = this.incorporate(type='view') -%}
5
+ {%- set intermediate_relation = make_intermediate_relation(target_relation) -%}
6
+
7
+ -- the intermediate_relation should not already exist in the database; get_relation
8
+ -- will return None in that case. Otherwise, we get a relation that we can drop
9
+ -- later, before we try to use this name for the current operation
10
+ {%- set preexisting_intermediate_relation = load_cached_relation(intermediate_relation) -%}
11
+ /*
12
+ This relation (probably) doesn't exist yet. If it does exist, it's a leftover from
13
+ a previous run, and we're going to try to drop it immediately. At the end of this
14
+ materialization, we're going to rename the "existing_relation" to this identifier,
15
+ and then we're going to drop it. In order to make sure we run the correct one of:
16
+ - drop view ...
17
+ - drop table ...
18
+
19
+ We need to set the type of this relation to be the type of the existing_relation, if it exists,
20
+ or else "view" as a sane default if it does not. Note that if the existing_relation does not
21
+ exist, then there is nothing to move out of the way and subsequentally drop. In that case,
22
+ this relation will be effectively unused.
23
+ */
24
+ {%- set backup_relation_type = 'view' -%}
25
+
26
+ {%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
27
+ -- as above, the backup_relation should not already exist
28
+ {%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
29
+ -- grab current tables grants config for comparision later on
30
+ {% set grant_config = config.get('grants') %}
31
+
32
+ {{ run_hooks(pre_hooks, inside_transaction=False) }}
33
+
34
+ -- drop the temp relations if they exist already in the database
35
+ {{ drop_relation_if_exists(preexisting_intermediate_relation) }}
36
+ {{ drop_relation_if_exists(preexisting_backup_relation) }}
37
+
38
+ -- `BEGIN` happens here:
39
+ {{ run_hooks(pre_hooks, inside_transaction=True) }}
40
+
41
+ -- build model
42
+ {% call statement('main') -%}
43
+ {{ get_create_view_as_sql(intermediate_relation, sql) }}
44
+ {%- endcall %}
45
+
46
+ -- cleanup
47
+ -- move the existing view out of the way
48
+ {% if existing_relation is not none %}
49
+ /* Do the equivalent of rename_if_exists. 'existing_relation' could have been dropped
50
+ since the variable was first set. */
51
+ {% set existing_relation = load_cached_relation(existing_relation) %}
52
+ {% if existing_relation is not none %}
53
+ {% call statement('main') -%}
54
+ {{ get_create_view_as_sql(backup_relation, sql) }}
55
+ {%- endcall %}
56
+ {{ drop_relation_if_exists(existing_relation) }}
57
+ {% endif %}
58
+ {% endif %}
59
+
60
+ {% call statement('main') -%}
61
+ {{ get_create_view_as_sql(target_relation, sql) }}
62
+ {%- endcall %}
63
+
64
+ {% set should_revoke = should_revoke(existing_relation, full_refresh_mode=True) %}
65
+ {% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
66
+
67
+ {% do persist_docs(target_relation, model) %}
68
+
69
+ {{ run_hooks(post_hooks, inside_transaction=True) }}
70
+
71
+ {{ adapter.commit() }}
72
+
73
+ {{ drop_relation_if_exists(backup_relation) }}
74
+
75
+ {{ drop_relation_if_exists(intermediate_relation) }}
76
+
77
+
78
+ {{ run_hooks(post_hooks, inside_transaction=False) }}
79
+
80
+ {{ return({'relations': [target_relation]}) }}
81
+
82
+ {%- endmaterialization -%}
83
+
84
+ {% macro ydb__create_view_as(relation, sql) -%}
85
+ create view {{ relation.include(database=False) }}
86
+ with (security_invoker = TRUE)
87
+ as (
88
+ {{ sql }}
89
+ )
90
+
91
+ {%- endmacro %}
@@ -0,0 +1,38 @@
1
+ {% macro ydb__create_csv_table(model, agate_table) %}
2
+ {%- set column_override = model['config'].get('column_types', {}) -%}
3
+ {%- set quote_seed_column = model['config'].get('quote_columns', None) -%}
4
+ {%- set primary_key_column = model['config'].get('primary_key', agate_table.column_names[0]) -%}
5
+
6
+ {% set sql %}
7
+ create table {{ this.render() }} (
8
+ {%- for col_name in agate_table.column_names -%}
9
+ {%- set inferred_type = adapter.convert_type(agate_table, loop.index0) -%}
10
+ {%- set type = column_override.get(col_name, inferred_type) -%}
11
+ {%- set column_name = (col_name | string) -%}
12
+ {{ adapter.quote_seed_column(column_name, quote_seed_column) }} {{ type }} {%- if not loop.last -%}, {%- endif -%}
13
+ {%- endfor -%}
14
+ , primary key ({{ primary_key_column }})
15
+ )
16
+ {% endset %}
17
+
18
+ {% call statement('_') -%}
19
+ {{ sql }}
20
+ {%- endcall %}
21
+
22
+ {{ return(sql) }}
23
+ {% endmacro %}
24
+
25
+
26
+ {% macro ydb__load_csv_rows(model, agate_table) %}
27
+ {% set cols_sql = get_seed_column_quoted_csv(model, agate_table.column_names) %}
28
+ {% set data_sql = adapter.prepare_insert_values_from_csv(agate_table) %}
29
+
30
+ {% if data_sql %}
31
+ {% set sql -%}
32
+ upsert into {{ this.render() }} ({{ cols_sql }}) VALUES
33
+ {{ data_sql }}
34
+ {%- endset %}
35
+
36
+ {% do adapter.add_query(sql, bindings=agate_table, abridge_sql_log=True) %}
37
+ {% endif %}
38
+ {% endmacro %}
@@ -0,0 +1,19 @@
1
+ fixed:
2
+ type: ydb
3
+ prompts:
4
+ host:
5
+ hint: "your host name"
6
+ port:
7
+ default: 5432
8
+ type: "int"
9
+ user:
10
+ hint: "dev username"
11
+ password:
12
+ hint: "dev password"
13
+ hide_input: true
14
+ dbname:
15
+ hint: "default database"
16
+ threads:
17
+ hint: "1 or more"
18
+ type: "int"
19
+ default: 1
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "dbt-ydb"
3
+ version = "0.0.1" # AUTOVERSION
4
+ description = "DBT adapter for YDB"
5
+ authors = [
6
+ {name = "Yandex LLC", email = "ydb@yandex-team.ru"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10,<4.0"
10
+ dependencies = [
11
+ "dbt-core (>=1.8.0,<2.0.0)",
12
+ "dbt-common (>=1.23.0,<2.0.0)",
13
+ "ydb-dbapi (>=0.1.12,<0.2.0)",
14
+ "dbt-adapters (>=1.14.8,<2.0.0)"
15
+ ]
16
+
17
+ [tool.poetry]
18
+ packages = [
19
+ { include = "dbt" },
20
+ { include = "dbt/**/*.py" },
21
+ ]
22
+
23
+ [tool.poetry.group.dev.dependencies]
24
+ ruff = "^0.11.7"
25
+ pytest = "^8.3.5"
26
+ dbt-tests-adapter = "^1.11.0"
27
+
28
+ [build-system]
29
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
30
+ build-backend = "poetry.core.masonry.api"