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 +42 -0
- dbt_ydb-0.0.1/README.md +24 -0
- dbt_ydb-0.0.1/dbt/adapters/ydb/__init__.py +11 -0
- dbt_ydb-0.0.1/dbt/adapters/ydb/__version__.py +1 -0
- dbt_ydb-0.0.1/dbt/adapters/ydb/column.py +136 -0
- dbt_ydb-0.0.1/dbt/adapters/ydb/connections.py +179 -0
- dbt_ydb-0.0.1/dbt/adapters/ydb/impl.py +333 -0
- dbt_ydb-0.0.1/dbt/adapters/ydb/relation.py +26 -0
- dbt_ydb-0.0.1/dbt/include/ydb/__init__.py +3 -0
- dbt_ydb-0.0.1/dbt/include/ydb/dbt_project.yml +5 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/adapters.sql +191 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/catalog.sql +0 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/generic_test_sql/accepted_values.sql +24 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/generic_test_sql/relationships.sql +17 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/materializations/models/incremental/incremental.sql +107 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/materializations/models/table.sql +19 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/materializations/models/view.sql +91 -0
- dbt_ydb-0.0.1/dbt/include/ydb/macros/materializations/seeds/seed.sql +38 -0
- dbt_ydb-0.0.1/dbt/include/ydb/profile_template.yml +19 -0
- dbt_ydb-0.0.1/pyproject.toml +30 -0
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
|
dbt_ydb-0.0.1/README.md
ADDED
|
@@ -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,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"
|