datus-db-core 0.1.0__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.
- datus_db_core-0.1.0/.gitignore +140 -0
- datus_db_core-0.1.0/PKG-INFO +29 -0
- datus_db_core-0.1.0/README.md +9 -0
- datus_db_core-0.1.0/datus_db_core/__init__.py +36 -0
- datus_db_core-0.1.0/datus_db_core/base.py +300 -0
- datus_db_core-0.1.0/datus_db_core/config.py +13 -0
- datus_db_core-0.1.0/datus_db_core/constants.py +20 -0
- datus_db_core-0.1.0/datus_db_core/exceptions.py +74 -0
- datus_db_core-0.1.0/datus_db_core/logging.py +9 -0
- datus_db_core-0.1.0/datus_db_core/mixins.py +46 -0
- datus_db_core-0.1.0/datus_db_core/models.py +83 -0
- datus_db_core-0.1.0/datus_db_core/registry.py +239 -0
- datus_db_core-0.1.0/datus_db_core/sql_utils.py +618 -0
- datus_db_core-0.1.0/pyproject.toml +35 -0
- datus_db_core-0.1.0/tests/__init__.py +0 -0
- datus_db_core-0.1.0/tests/unit/__init__.py +0 -0
- datus_db_core-0.1.0/tests/unit/test_base.py +375 -0
- datus_db_core-0.1.0/tests/unit/test_config.py +33 -0
- datus_db_core-0.1.0/tests/unit/test_constants.py +41 -0
- datus_db_core-0.1.0/tests/unit/test_exceptions.py +75 -0
- datus_db_core-0.1.0/tests/unit/test_mixins.py +71 -0
- datus_db_core-0.1.0/tests/unit/test_models.py +157 -0
- datus_db_core-0.1.0/tests/unit/test_registry.py +291 -0
- datus_db_core-0.1.0/tests/unit/test_sql_utils.py +325 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
*.cover
|
|
48
|
+
*.py,cover
|
|
49
|
+
.hypothesis/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
db.sqlite3-journal
|
|
61
|
+
|
|
62
|
+
# Flask stuff:
|
|
63
|
+
instance/
|
|
64
|
+
.webassets-cache
|
|
65
|
+
|
|
66
|
+
# Scrapy stuff:
|
|
67
|
+
.scrapy
|
|
68
|
+
|
|
69
|
+
# Sphinx documentation
|
|
70
|
+
docs/_build/
|
|
71
|
+
|
|
72
|
+
# PyBuilder
|
|
73
|
+
target/
|
|
74
|
+
|
|
75
|
+
# Jupyter Notebook
|
|
76
|
+
.ipynb_checkpoints
|
|
77
|
+
|
|
78
|
+
# IPython
|
|
79
|
+
profile_default/
|
|
80
|
+
ipython_config.py
|
|
81
|
+
|
|
82
|
+
# pyenv
|
|
83
|
+
.python-version
|
|
84
|
+
|
|
85
|
+
# pipenv
|
|
86
|
+
Pipfile.lock
|
|
87
|
+
|
|
88
|
+
# uv
|
|
89
|
+
uv.lock
|
|
90
|
+
|
|
91
|
+
# PEP 582
|
|
92
|
+
__pypackages__/
|
|
93
|
+
|
|
94
|
+
# Celery stuff
|
|
95
|
+
celerybeat-schedule
|
|
96
|
+
celerybeat.pid
|
|
97
|
+
|
|
98
|
+
# SageMath parsed files
|
|
99
|
+
*.sage.py
|
|
100
|
+
|
|
101
|
+
# Environments
|
|
102
|
+
.env
|
|
103
|
+
.venv
|
|
104
|
+
env/
|
|
105
|
+
venv/
|
|
106
|
+
ENV/
|
|
107
|
+
env.bak/
|
|
108
|
+
venv.bak/
|
|
109
|
+
|
|
110
|
+
# Spyder project settings
|
|
111
|
+
.spyderproject
|
|
112
|
+
.spyproject
|
|
113
|
+
|
|
114
|
+
# Rope project settings
|
|
115
|
+
.ropeproject
|
|
116
|
+
|
|
117
|
+
# mkdocs documentation
|
|
118
|
+
/site
|
|
119
|
+
|
|
120
|
+
# mypy
|
|
121
|
+
.mypy_cache/
|
|
122
|
+
.dmypy.json
|
|
123
|
+
dmypy.json
|
|
124
|
+
|
|
125
|
+
# Pyre type checker
|
|
126
|
+
.pyre/
|
|
127
|
+
|
|
128
|
+
# IDEs
|
|
129
|
+
.vscode/
|
|
130
|
+
.idea/
|
|
131
|
+
*.swp
|
|
132
|
+
*.swo
|
|
133
|
+
*~
|
|
134
|
+
|
|
135
|
+
# OS
|
|
136
|
+
.DS_Store
|
|
137
|
+
Thumbs.db
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
.omc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datus-db-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core database abstractions for Datus adapters
|
|
5
|
+
Project-URL: Homepage, https://github.com/Datus-ai/datus-db-adapters
|
|
6
|
+
Project-URL: Repository, https://github.com/Datus-ai/datus-db-adapters
|
|
7
|
+
Project-URL: Issues, https://github.com/Datus-ai/datus-db-adapters/issues
|
|
8
|
+
Author-email: DatusAI <support@datus.ai>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: adapter,core,database,datus
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
|
+
Requires-Dist: sqlglot>=20.0.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# datus-db-core
|
|
22
|
+
|
|
23
|
+
Core database abstractions for Datus adapters. Provides `BaseSqlConnector`, `ConnectorRegistry`, SQL parsing utilities, and shared models used by all database adapter packages.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install datus-db-core
|
|
29
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright 2025-present DatusAI, Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for details.
|
|
4
|
+
|
|
5
|
+
from datus_db_core.base import BaseSqlConnector, _to_sql_literal, list_to_in_str
|
|
6
|
+
from datus_db_core.config import ConnectionConfig
|
|
7
|
+
from datus_db_core.constants import SQLType
|
|
8
|
+
from datus_db_core.exceptions import DatusException, ErrorCode
|
|
9
|
+
from datus_db_core.logging import get_logger
|
|
10
|
+
from datus_db_core.mixins import CatalogSupportMixin, MaterializedViewSupportMixin, SchemaNamespaceMixin
|
|
11
|
+
from datus_db_core.models import TABLE_TYPE, ExecuteSQLInput, ExecuteSQLResult
|
|
12
|
+
from datus_db_core.registry import AdapterMetadata, ConnectorRegistry, connector_registry
|
|
13
|
+
from datus_db_core.sql_utils import metadata_identifier, parse_context_switch, parse_sql_type
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BaseSqlConnector",
|
|
17
|
+
"list_to_in_str",
|
|
18
|
+
"_to_sql_literal",
|
|
19
|
+
"ConnectionConfig",
|
|
20
|
+
"SQLType",
|
|
21
|
+
"DatusException",
|
|
22
|
+
"ErrorCode",
|
|
23
|
+
"get_logger",
|
|
24
|
+
"CatalogSupportMixin",
|
|
25
|
+
"MaterializedViewSupportMixin",
|
|
26
|
+
"SchemaNamespaceMixin",
|
|
27
|
+
"TABLE_TYPE",
|
|
28
|
+
"ExecuteSQLInput",
|
|
29
|
+
"ExecuteSQLResult",
|
|
30
|
+
"AdapterMetadata",
|
|
31
|
+
"ConnectorRegistry",
|
|
32
|
+
"connector_registry",
|
|
33
|
+
"metadata_identifier",
|
|
34
|
+
"parse_context_switch",
|
|
35
|
+
"parse_sql_type",
|
|
36
|
+
]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Copyright 2025-present DatusAI, Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for details.
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, Iterator, List, Literal, Optional, Set, Tuple
|
|
7
|
+
|
|
8
|
+
from datus_db_core.config import ConnectionConfig
|
|
9
|
+
from datus_db_core.constants import SQLType
|
|
10
|
+
from datus_db_core.logging import get_logger
|
|
11
|
+
from datus_db_core.models import TABLE_TYPE, ExecuteSQLInput, ExecuteSQLResult
|
|
12
|
+
from datus_db_core.sql_utils import metadata_identifier, parse_sql_type
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseSqlConnector(ABC):
|
|
18
|
+
def __init__(self, config: ConnectionConfig, dialect: str):
|
|
19
|
+
self.config = config
|
|
20
|
+
self.timeout_seconds = config.timeout_seconds
|
|
21
|
+
self.connection: Any = None
|
|
22
|
+
self.dialect = dialect
|
|
23
|
+
self.catalog_name = ""
|
|
24
|
+
self.database_name = ""
|
|
25
|
+
self.schema_name = ""
|
|
26
|
+
|
|
27
|
+
def close(self):
|
|
28
|
+
if self.connection:
|
|
29
|
+
self.connection.close()
|
|
30
|
+
self.connection = None
|
|
31
|
+
|
|
32
|
+
def connect(self):
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def __enter__(self):
|
|
36
|
+
self.connect()
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
40
|
+
if exc_type:
|
|
41
|
+
try:
|
|
42
|
+
self._safe_rollback()
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.warning(f"Failed to rollback during cleanup: {e}")
|
|
45
|
+
try:
|
|
46
|
+
self.close()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning(f"Failed to close connection during cleanup: {e}")
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def _safe_rollback(self):
|
|
52
|
+
if hasattr(self, "connection") and self.connection:
|
|
53
|
+
try:
|
|
54
|
+
if hasattr(self.connection, "rollback"):
|
|
55
|
+
self.connection.rollback()
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def execute(
|
|
60
|
+
self,
|
|
61
|
+
input_params: Any,
|
|
62
|
+
result_format: Optional[Literal["csv", "arrow", "pandas", "list"]] = None,
|
|
63
|
+
) -> ExecuteSQLResult:
|
|
64
|
+
self.validate_input(input_params)
|
|
65
|
+
if isinstance(input_params, dict):
|
|
66
|
+
input_params = ExecuteSQLInput(**input_params)
|
|
67
|
+
sql_query = input_params.sql_query.strip()
|
|
68
|
+
if result_format is None:
|
|
69
|
+
result_format = getattr(input_params, "result_format", "csv") or "csv"
|
|
70
|
+
try:
|
|
71
|
+
sql_type = parse_sql_type(sql_query, self.dialect)
|
|
72
|
+
if sql_type == SQLType.INSERT:
|
|
73
|
+
result = self.execute_insert(sql_query)
|
|
74
|
+
elif sql_type in (SQLType.UPDATE, SQLType.MERGE):
|
|
75
|
+
result = self.execute_update(sql_query)
|
|
76
|
+
elif sql_type == SQLType.DELETE:
|
|
77
|
+
result = self.execute_delete(sql_query)
|
|
78
|
+
elif sql_type == SQLType.CONTENT_SET:
|
|
79
|
+
result = self.execute_content_set(sql_query)
|
|
80
|
+
elif sql_type == SQLType.DDL:
|
|
81
|
+
result = self.execute_ddl(sql_query)
|
|
82
|
+
elif sql_type == SQLType.SELECT:
|
|
83
|
+
result = self.execute_query(sql_query, result_format)
|
|
84
|
+
elif sql_type == SQLType.METADATA_SHOW:
|
|
85
|
+
result = self.execute_query(sql_query, result_format)
|
|
86
|
+
elif sql_type == SQLType.EXPLAIN:
|
|
87
|
+
result = self.execute_explain(sql_query, result_format)
|
|
88
|
+
else:
|
|
89
|
+
return ExecuteSQLResult(
|
|
90
|
+
success=False,
|
|
91
|
+
error="Unknown type of SQL",
|
|
92
|
+
sql_query=sql_query,
|
|
93
|
+
sql_return="",
|
|
94
|
+
row_count=0,
|
|
95
|
+
result_format=result_format,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Executing SQL query failed: {e}")
|
|
101
|
+
return ExecuteSQLResult(
|
|
102
|
+
success=False,
|
|
103
|
+
error=str(e),
|
|
104
|
+
sql_query=sql_query,
|
|
105
|
+
sql_return="",
|
|
106
|
+
row_count=0,
|
|
107
|
+
result_format=result_format,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def execute_insert(self, sql: str) -> ExecuteSQLResult:
|
|
112
|
+
raise NotImplementedError()
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def execute_update(self, sql: str) -> ExecuteSQLResult:
|
|
116
|
+
raise NotImplementedError()
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def execute_delete(self, sql: str) -> ExecuteSQLResult:
|
|
120
|
+
raise NotImplementedError()
|
|
121
|
+
|
|
122
|
+
def validate_input(self, input_params: Any):
|
|
123
|
+
if isinstance(input_params, dict):
|
|
124
|
+
if "sql_query" not in input_params:
|
|
125
|
+
raise ValueError("'sql_query' parameter is required")
|
|
126
|
+
if not isinstance(input_params["sql_query"], str):
|
|
127
|
+
raise ValueError("'sql_query' must be a string")
|
|
128
|
+
else:
|
|
129
|
+
if not hasattr(input_params, "sql_query"):
|
|
130
|
+
raise ValueError("'sql_query' parameter is required")
|
|
131
|
+
if not isinstance(input_params.sql_query, str):
|
|
132
|
+
raise ValueError("'sql_query' must be a string")
|
|
133
|
+
|
|
134
|
+
def execute_arrow(self, sql: str) -> ExecuteSQLResult:
|
|
135
|
+
raise NotImplementedError()
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def execute_query(
|
|
139
|
+
self, sql: str, result_format: Literal["csv", "arrow", "pandas", "list"] = "csv"
|
|
140
|
+
) -> ExecuteSQLResult:
|
|
141
|
+
raise NotImplementedError()
|
|
142
|
+
|
|
143
|
+
def execute_explain(
|
|
144
|
+
self, sql: str, result_format: Literal["csv", "arrow", "pandas", "list"] = "csv"
|
|
145
|
+
) -> ExecuteSQLResult:
|
|
146
|
+
return self.execute_query(sql, result_format)
|
|
147
|
+
|
|
148
|
+
@abstractmethod
|
|
149
|
+
def execute_pandas(self, sql: str) -> ExecuteSQLResult:
|
|
150
|
+
raise NotImplementedError()
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def execute_ddl(self, sql: str) -> ExecuteSQLResult:
|
|
154
|
+
raise NotImplementedError()
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
def execute_csv(self, sql: str) -> ExecuteSQLResult:
|
|
158
|
+
raise NotImplementedError()
|
|
159
|
+
|
|
160
|
+
@abstractmethod
|
|
161
|
+
def get_databases(self, catalog_name: str = "", include_sys: bool = False) -> List[str]:
|
|
162
|
+
raise NotImplementedError()
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def get_tables(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> List[str]:
|
|
166
|
+
raise NotImplementedError()
|
|
167
|
+
|
|
168
|
+
def get_views(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> List[str]:
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
def _sys_databases(self) -> Set[str]:
|
|
172
|
+
return set()
|
|
173
|
+
|
|
174
|
+
def _sys_schemas(self) -> Set[str]:
|
|
175
|
+
return set()
|
|
176
|
+
|
|
177
|
+
def execute_csv_iterator(
|
|
178
|
+
self, query: str, max_rows: int = 100, with_header: bool = True
|
|
179
|
+
) -> Iterator[Tuple[str, ...]]:
|
|
180
|
+
raise NotImplementedError()
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def test_connection(self):
|
|
184
|
+
raise NotImplementedError()
|
|
185
|
+
|
|
186
|
+
def get_type(self) -> str:
|
|
187
|
+
return self.dialect
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def execute_queries(self, queries: List[str]) -> List[Any]:
|
|
191
|
+
raise NotImplementedError()
|
|
192
|
+
|
|
193
|
+
def get_tables_with_ddl(
|
|
194
|
+
self,
|
|
195
|
+
catalog_name: str = "",
|
|
196
|
+
database_name: str = "",
|
|
197
|
+
schema_name: str = "",
|
|
198
|
+
tables: Optional[List[str]] = None,
|
|
199
|
+
) -> List[Dict[str, str]]:
|
|
200
|
+
raise NotImplementedError()
|
|
201
|
+
|
|
202
|
+
def _reset_filter_tables(
|
|
203
|
+
self,
|
|
204
|
+
tables: Optional[List[str]] = None,
|
|
205
|
+
catalog_name: str = "",
|
|
206
|
+
database_name: str = "",
|
|
207
|
+
schema_name: str = "",
|
|
208
|
+
) -> List[str]:
|
|
209
|
+
filter_tables = []
|
|
210
|
+
if tables:
|
|
211
|
+
catalog_name = catalog_name or self.catalog_name
|
|
212
|
+
database_name = database_name or self.database_name
|
|
213
|
+
schema_name = schema_name or self.schema_name
|
|
214
|
+
for table_name in tables:
|
|
215
|
+
filter_tables.append(
|
|
216
|
+
self.full_name(
|
|
217
|
+
table_name=table_name,
|
|
218
|
+
catalog_name=catalog_name,
|
|
219
|
+
database_name=database_name,
|
|
220
|
+
schema_name=schema_name,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
return filter_tables
|
|
224
|
+
|
|
225
|
+
def get_views_with_ddl(
|
|
226
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
|
|
227
|
+
) -> List[Dict[str, str]]:
|
|
228
|
+
raise NotImplementedError()
|
|
229
|
+
|
|
230
|
+
def switch_context(self, catalog_name: str = "", database_name: str = "", schema_name: str = ""):
|
|
231
|
+
self.connect()
|
|
232
|
+
self.do_switch_context(catalog_name=catalog_name, database_name=database_name, schema_name=schema_name)
|
|
233
|
+
if catalog_name:
|
|
234
|
+
self.catalog_name = catalog_name
|
|
235
|
+
if database_name:
|
|
236
|
+
self.database_name = database_name
|
|
237
|
+
if schema_name:
|
|
238
|
+
self.schema_name = schema_name
|
|
239
|
+
|
|
240
|
+
def do_switch_context(self, catalog_name: str = "", database_name: str = "", schema_name: str = ""):
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def get_schema(
|
|
244
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
|
|
245
|
+
) -> List[Dict[str, str]]:
|
|
246
|
+
raise NotImplementedError()
|
|
247
|
+
|
|
248
|
+
def get_sample_rows(
|
|
249
|
+
self,
|
|
250
|
+
tables: Optional[List[str]] = None,
|
|
251
|
+
top_n: int = 5,
|
|
252
|
+
catalog_name: str = "",
|
|
253
|
+
database_name: str = "",
|
|
254
|
+
schema_name: str = "",
|
|
255
|
+
table_type: TABLE_TYPE = "table",
|
|
256
|
+
) -> List[Dict[str, Any]]:
|
|
257
|
+
raise NotImplementedError()
|
|
258
|
+
|
|
259
|
+
def full_name(
|
|
260
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
|
|
261
|
+
) -> str:
|
|
262
|
+
raise NotImplementedError()
|
|
263
|
+
|
|
264
|
+
def identifier(
|
|
265
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
|
|
266
|
+
) -> str:
|
|
267
|
+
return metadata_identifier(
|
|
268
|
+
dialect=self.dialect,
|
|
269
|
+
catalog_name=catalog_name,
|
|
270
|
+
database_name=database_name,
|
|
271
|
+
schema_name=schema_name,
|
|
272
|
+
table_name=table_name,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
@abstractmethod
|
|
276
|
+
def execute_content_set(self, sql_query: str) -> ExecuteSQLResult:
|
|
277
|
+
raise NotImplementedError()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def list_to_in_str(prefix: str, values: Optional[List[str]] = None) -> str:
|
|
281
|
+
if not values:
|
|
282
|
+
return ""
|
|
283
|
+
value_str = ",".join(_to_sql_literal(v, around_with_quotes=True) for v in values)
|
|
284
|
+
return f"{prefix} ({value_str})"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _escape_sql_string_standard(value: str) -> str:
|
|
288
|
+
return value.replace("'", "''")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _to_sql_literal(value: Optional[str], around_with_quotes: bool = False) -> str:
|
|
292
|
+
if value is None:
|
|
293
|
+
return "NULL"
|
|
294
|
+
if not value:
|
|
295
|
+
return "" if not around_with_quotes else "''"
|
|
296
|
+
replace_value = _escape_sql_string_standard(value)
|
|
297
|
+
if not around_with_quotes:
|
|
298
|
+
return replace_value
|
|
299
|
+
else:
|
|
300
|
+
return f"'{replace_value}'"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2025-present DatusAI, Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for details.
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionConfig(BaseModel):
|
|
9
|
+
"""Base connection configuration for all database connectors."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
|
|
13
|
+
timeout_seconds: int = Field(default=30, description="Connection timeout in seconds")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2025-present DatusAI, Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for details.
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SQLType(str, Enum):
|
|
9
|
+
"""SQL statement types."""
|
|
10
|
+
|
|
11
|
+
SELECT = "select"
|
|
12
|
+
INSERT = "insert"
|
|
13
|
+
UPDATE = "update"
|
|
14
|
+
DELETE = "delete"
|
|
15
|
+
MERGE = "merge"
|
|
16
|
+
DDL = "ddl"
|
|
17
|
+
METADATA_SHOW = "metadata"
|
|
18
|
+
EXPLAIN = "explain"
|
|
19
|
+
CONTENT_SET = "context_set"
|
|
20
|
+
UNKNOWN = "unknown"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright 2025-present DatusAI, Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for details.
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ErrorCode(Enum):
|
|
10
|
+
"""Error codes for Datus database exceptions."""
|
|
11
|
+
|
|
12
|
+
# Common errors
|
|
13
|
+
COMMON_UNKNOWN = ("100000", "Unknown error occurred")
|
|
14
|
+
COMMON_FIELD_INVALID = (
|
|
15
|
+
"100001",
|
|
16
|
+
"Unexpected value of {field_name}, expected value: {expected_values}, your value: {your_value}",
|
|
17
|
+
)
|
|
18
|
+
COMMON_INVALID_PARAMETER = ("100002", "Invalid parameter: {error_message}")
|
|
19
|
+
COMMON_FIELD_REQUIRED = ("100003", "Missing required field: {field_name}")
|
|
20
|
+
COMMON_UNSUPPORTED = ("100004", "Unsupported value `{your_value}` for field `{field_name}`")
|
|
21
|
+
COMMON_CONFIG_ERROR = ("100006", "Configuration error: {config_error}")
|
|
22
|
+
COMMON_MISSING_DEPENDENCY = ("100007", "Missing required dependency: {dependency}")
|
|
23
|
+
|
|
24
|
+
# Database errors
|
|
25
|
+
DB_FAILED = ("500000", "Database operation failed. Error details: {error_message}")
|
|
26
|
+
DB_CONNECTION_FAILED = ("500001", "Failed to establish connection to database. Error details: {error_message}")
|
|
27
|
+
DB_CONNECTION_TIMEOUT = ("500002", "Connection to database timed out. Error details: {error_message}")
|
|
28
|
+
DB_AUTHENTICATION_FAILED = (
|
|
29
|
+
"500003",
|
|
30
|
+
"Authentication failed for database. Please check your credentials. Error details: {error_message}",
|
|
31
|
+
)
|
|
32
|
+
DB_PERMISSION_DENIED = (
|
|
33
|
+
"500004",
|
|
34
|
+
"Permission denied when performing '{operation}' on database. Error details: {error_message}",
|
|
35
|
+
)
|
|
36
|
+
DB_EXECUTION_SYNTAX_ERROR = ("500005", "Invalid SQL syntax in query. Error details: {error_message}")
|
|
37
|
+
DB_EXECUTION_ERROR = ("500006", "Failed to execute query on database. Error details: {error_message}")
|
|
38
|
+
DB_EXECUTION_TIMEOUT = ("500007", "Query execution timed out on database. Error details: {error_message}")
|
|
39
|
+
DB_QUERY_METADATA_FAILED = ("500008", "Failed to retrieve metadata for query. Error details: {error_message}")
|
|
40
|
+
DB_CONSTRAINT_VIOLATION = ("500011", "Database constraint violation occurred. Error details: {error_message}")
|
|
41
|
+
DB_TRANSACTION_FAILED = ("500009", "Database transaction failed. Error details: {error_message}")
|
|
42
|
+
DB_TABLE_NOT_EXISTS = ("500010", "Table {table_name} does not exist.")
|
|
43
|
+
|
|
44
|
+
def __init__(self, code: str, desc: str):
|
|
45
|
+
self.code = code
|
|
46
|
+
self.desc = desc
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DatusException(Exception):
|
|
50
|
+
"""Datus custom exception with error code."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
code: ErrorCode,
|
|
55
|
+
message: Optional[str] = None,
|
|
56
|
+
message_args: Optional[dict[str, Any]] = None,
|
|
57
|
+
*args: object,
|
|
58
|
+
):
|
|
59
|
+
self.code = code
|
|
60
|
+
self.message_args = message_args or {}
|
|
61
|
+
self.message = self.build_msg(message, message_args)
|
|
62
|
+
super().__init__(self.message, *args)
|
|
63
|
+
|
|
64
|
+
def __str__(self):
|
|
65
|
+
return self.message
|
|
66
|
+
|
|
67
|
+
def build_msg(self, message: Optional[str] = None, message_args: Optional[dict[str, Any]] = None) -> str:
|
|
68
|
+
if message:
|
|
69
|
+
final_message = message
|
|
70
|
+
elif message_args:
|
|
71
|
+
final_message = self.code.desc.format(**message_args)
|
|
72
|
+
else:
|
|
73
|
+
final_message = self.code.desc
|
|
74
|
+
return f"error_code={self.code.code}, error_message={final_message}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright 2025-present DatusAI, Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for details.
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CatalogSupportMixin(ABC):
|
|
10
|
+
"""Mixin for databases that support catalog namespace."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def get_catalogs(self) -> List[str]:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def switch_catalog(self, catalog_name: str) -> None:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def default_catalog(self) -> str:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MaterializedViewSupportMixin(ABC):
|
|
26
|
+
"""Mixin for databases that support materialized views."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def get_materialized_views(
|
|
30
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
|
|
31
|
+
) -> List[str]:
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def get_materialized_views_with_ddl(
|
|
36
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
|
|
37
|
+
) -> List[Dict[str, str]]:
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SchemaNamespaceMixin(ABC):
|
|
42
|
+
"""Mixin for databases that support schema-level namespace."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_schemas(self, catalog_name: str = "", database_name: str = "", include_sys: bool = False) -> List[str]:
|
|
46
|
+
raise NotImplementedError
|