datus-starrocks 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_starrocks-0.1.0/.gitignore +137 -0
- datus_starrocks-0.1.0/PKG-INFO +200 -0
- datus_starrocks-0.1.0/README.md +177 -0
- datus_starrocks-0.1.0/datus_starrocks/__init__.py +16 -0
- datus_starrocks-0.1.0/datus_starrocks/config.py +23 -0
- datus_starrocks-0.1.0/datus_starrocks/connector.py +340 -0
- datus_starrocks-0.1.0/pyproject.toml +47 -0
- datus_starrocks-0.1.0/tests/__init__.py +0 -0
- datus_starrocks-0.1.0/tests/test_connector.py +429 -0
|
@@ -0,0 +1,137 @@
|
|
|
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
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datus-starrocks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: StarRocks database adapter for Datus
|
|
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,database,datus,starrocks
|
|
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: datus-agent>0.2.1
|
|
18
|
+
Requires-Dist: datus-mysql>=0.1.0
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# datus-starrocks
|
|
25
|
+
|
|
26
|
+
StarRocks database adapter for Datus.
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
StarRocks is a high-performance analytical database that uses the MySQL protocol. This adapter extends the MySQL connector with StarRocks-specific features:
|
|
31
|
+
|
|
32
|
+
- Multi-catalog support
|
|
33
|
+
- Materialized views
|
|
34
|
+
- StarRocks-specific metadata queries
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install datus-starrocks
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will automatically install the required dependencies:
|
|
43
|
+
- `datus-agent`
|
|
44
|
+
- `datus-mysql` (which includes `datus-sqlalchemy`)
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
The adapter is automatically registered with Datus when installed. Configure your database connection:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
database:
|
|
52
|
+
type: starrocks
|
|
53
|
+
host: localhost
|
|
54
|
+
port: 9030
|
|
55
|
+
username: root
|
|
56
|
+
password: your_password
|
|
57
|
+
catalog: default_catalog
|
|
58
|
+
database: your_database
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or use programmatically:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from datus_starrocks import StarRocksConnector
|
|
65
|
+
|
|
66
|
+
# Create connector
|
|
67
|
+
connector = StarRocksConnector(
|
|
68
|
+
host="localhost",
|
|
69
|
+
port=9030,
|
|
70
|
+
user="root",
|
|
71
|
+
password="your_password",
|
|
72
|
+
catalog="default_catalog",
|
|
73
|
+
database="mydb"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Use context manager for automatic cleanup
|
|
77
|
+
with connector:
|
|
78
|
+
# Test connection
|
|
79
|
+
connector.test_connection()
|
|
80
|
+
|
|
81
|
+
# Get catalogs
|
|
82
|
+
catalogs = connector.get_catalogs()
|
|
83
|
+
print(f"Catalogs: {catalogs}")
|
|
84
|
+
|
|
85
|
+
# Get databases in catalog
|
|
86
|
+
databases = connector.get_databases(catalog_name="default_catalog")
|
|
87
|
+
print(f"Databases: {databases}")
|
|
88
|
+
|
|
89
|
+
# Get tables
|
|
90
|
+
tables = connector.get_tables(catalog_name="default_catalog", database_name="mydb")
|
|
91
|
+
print(f"Tables: {tables}")
|
|
92
|
+
|
|
93
|
+
# Get materialized views
|
|
94
|
+
mvs = connector.get_materialized_views(database_name="mydb")
|
|
95
|
+
print(f"Materialized Views: {mvs}")
|
|
96
|
+
|
|
97
|
+
# Get materialized views with DDL
|
|
98
|
+
mvs_with_ddl = connector.get_materialized_views_with_ddl(database_name="mydb")
|
|
99
|
+
for mv in mvs_with_ddl:
|
|
100
|
+
print(f"\n{mv['table_name']}:")
|
|
101
|
+
print(mv['definition'])
|
|
102
|
+
|
|
103
|
+
# Execute query
|
|
104
|
+
result = connector.execute_query("SELECT * FROM users LIMIT 10")
|
|
105
|
+
print(result.sql_return)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
### StarRocks-Specific Features
|
|
111
|
+
- **Multi-catalog support**: Query across multiple catalogs
|
|
112
|
+
- **Materialized views**: Full support for StarRocks materialized views
|
|
113
|
+
- **Catalog management**: Switch between catalogs seamlessly
|
|
114
|
+
|
|
115
|
+
### Inherited from MySQL
|
|
116
|
+
- Full CRUD operations (SELECT, INSERT, UPDATE, DELETE)
|
|
117
|
+
- DDL execution (CREATE, ALTER, DROP)
|
|
118
|
+
- Metadata retrieval (tables, views, schemas)
|
|
119
|
+
- Sample data extraction
|
|
120
|
+
- Multiple result formats (pandas, arrow, csv, list)
|
|
121
|
+
- Connection pooling and management
|
|
122
|
+
|
|
123
|
+
## StarRocks-Specific Examples
|
|
124
|
+
|
|
125
|
+
### Working with Catalogs
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# List all catalogs
|
|
129
|
+
catalogs = connector.get_catalogs()
|
|
130
|
+
|
|
131
|
+
# Switch catalog
|
|
132
|
+
connector.switch_context(catalog_name="hive_catalog")
|
|
133
|
+
|
|
134
|
+
# Query with explicit catalog
|
|
135
|
+
tables = connector.get_tables(
|
|
136
|
+
catalog_name="hive_catalog",
|
|
137
|
+
database_name="my_hive_db"
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Materialized Views
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# Get materialized views
|
|
145
|
+
mvs = connector.get_materialized_views(database_name="mydb")
|
|
146
|
+
|
|
147
|
+
# Get materialized views with full DDL
|
|
148
|
+
mvs_with_ddl = connector.get_materialized_views_with_ddl(database_name="mydb")
|
|
149
|
+
|
|
150
|
+
for mv in mvs_with_ddl:
|
|
151
|
+
print(f"Name: {mv['table_name']}")
|
|
152
|
+
print(f"Database: {mv['database_name']}")
|
|
153
|
+
print(f"Catalog: {mv['catalog_name']}")
|
|
154
|
+
print(f"Definition: {mv['definition']}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Fully-Qualified Names
|
|
158
|
+
|
|
159
|
+
StarRocks supports three-part names: `catalog.database.table`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# Build full name
|
|
163
|
+
full_name = connector.full_name(
|
|
164
|
+
catalog_name="default_catalog",
|
|
165
|
+
database_name="mydb",
|
|
166
|
+
table_name="users"
|
|
167
|
+
)
|
|
168
|
+
# Result: `default_catalog`.`mydb`.`users`
|
|
169
|
+
|
|
170
|
+
# Query with full name
|
|
171
|
+
result = connector.execute_query(f"SELECT * FROM {full_name} LIMIT 10")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Requirements
|
|
175
|
+
|
|
176
|
+
- Python >= 3.10
|
|
177
|
+
- StarRocks >= 2.0
|
|
178
|
+
- datus-agent >= 0.3.0
|
|
179
|
+
- datus-mysql >= 0.1.0
|
|
180
|
+
|
|
181
|
+
## Connection Cleanup
|
|
182
|
+
|
|
183
|
+
The connector includes special handling for PyMySQL cleanup errors that can occur with StarRocks connections. Use the context manager pattern for automatic cleanup:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
with StarRocksConnector(...) as connector:
|
|
187
|
+
# Your code here
|
|
188
|
+
pass
|
|
189
|
+
# Connection automatically cleaned up
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
Apache License 2.0
|
|
195
|
+
|
|
196
|
+
## Related Packages
|
|
197
|
+
|
|
198
|
+
- `datus-mysql` - MySQL adapter (base for StarRocks)
|
|
199
|
+
- `datus-sqlalchemy` - SQLAlchemy base connector
|
|
200
|
+
- `datus-snowflake` - Snowflake adapter
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# datus-starrocks
|
|
2
|
+
|
|
3
|
+
StarRocks database adapter for Datus.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
StarRocks is a high-performance analytical database that uses the MySQL protocol. This adapter extends the MySQL connector with StarRocks-specific features:
|
|
8
|
+
|
|
9
|
+
- Multi-catalog support
|
|
10
|
+
- Materialized views
|
|
11
|
+
- StarRocks-specific metadata queries
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install datus-starrocks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This will automatically install the required dependencies:
|
|
20
|
+
- `datus-agent`
|
|
21
|
+
- `datus-mysql` (which includes `datus-sqlalchemy`)
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
The adapter is automatically registered with Datus when installed. Configure your database connection:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
database:
|
|
29
|
+
type: starrocks
|
|
30
|
+
host: localhost
|
|
31
|
+
port: 9030
|
|
32
|
+
username: root
|
|
33
|
+
password: your_password
|
|
34
|
+
catalog: default_catalog
|
|
35
|
+
database: your_database
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or use programmatically:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from datus_starrocks import StarRocksConnector
|
|
42
|
+
|
|
43
|
+
# Create connector
|
|
44
|
+
connector = StarRocksConnector(
|
|
45
|
+
host="localhost",
|
|
46
|
+
port=9030,
|
|
47
|
+
user="root",
|
|
48
|
+
password="your_password",
|
|
49
|
+
catalog="default_catalog",
|
|
50
|
+
database="mydb"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Use context manager for automatic cleanup
|
|
54
|
+
with connector:
|
|
55
|
+
# Test connection
|
|
56
|
+
connector.test_connection()
|
|
57
|
+
|
|
58
|
+
# Get catalogs
|
|
59
|
+
catalogs = connector.get_catalogs()
|
|
60
|
+
print(f"Catalogs: {catalogs}")
|
|
61
|
+
|
|
62
|
+
# Get databases in catalog
|
|
63
|
+
databases = connector.get_databases(catalog_name="default_catalog")
|
|
64
|
+
print(f"Databases: {databases}")
|
|
65
|
+
|
|
66
|
+
# Get tables
|
|
67
|
+
tables = connector.get_tables(catalog_name="default_catalog", database_name="mydb")
|
|
68
|
+
print(f"Tables: {tables}")
|
|
69
|
+
|
|
70
|
+
# Get materialized views
|
|
71
|
+
mvs = connector.get_materialized_views(database_name="mydb")
|
|
72
|
+
print(f"Materialized Views: {mvs}")
|
|
73
|
+
|
|
74
|
+
# Get materialized views with DDL
|
|
75
|
+
mvs_with_ddl = connector.get_materialized_views_with_ddl(database_name="mydb")
|
|
76
|
+
for mv in mvs_with_ddl:
|
|
77
|
+
print(f"\n{mv['table_name']}:")
|
|
78
|
+
print(mv['definition'])
|
|
79
|
+
|
|
80
|
+
# Execute query
|
|
81
|
+
result = connector.execute_query("SELECT * FROM users LIMIT 10")
|
|
82
|
+
print(result.sql_return)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Features
|
|
86
|
+
|
|
87
|
+
### StarRocks-Specific Features
|
|
88
|
+
- **Multi-catalog support**: Query across multiple catalogs
|
|
89
|
+
- **Materialized views**: Full support for StarRocks materialized views
|
|
90
|
+
- **Catalog management**: Switch between catalogs seamlessly
|
|
91
|
+
|
|
92
|
+
### Inherited from MySQL
|
|
93
|
+
- Full CRUD operations (SELECT, INSERT, UPDATE, DELETE)
|
|
94
|
+
- DDL execution (CREATE, ALTER, DROP)
|
|
95
|
+
- Metadata retrieval (tables, views, schemas)
|
|
96
|
+
- Sample data extraction
|
|
97
|
+
- Multiple result formats (pandas, arrow, csv, list)
|
|
98
|
+
- Connection pooling and management
|
|
99
|
+
|
|
100
|
+
## StarRocks-Specific Examples
|
|
101
|
+
|
|
102
|
+
### Working with Catalogs
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# List all catalogs
|
|
106
|
+
catalogs = connector.get_catalogs()
|
|
107
|
+
|
|
108
|
+
# Switch catalog
|
|
109
|
+
connector.switch_context(catalog_name="hive_catalog")
|
|
110
|
+
|
|
111
|
+
# Query with explicit catalog
|
|
112
|
+
tables = connector.get_tables(
|
|
113
|
+
catalog_name="hive_catalog",
|
|
114
|
+
database_name="my_hive_db"
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Materialized Views
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Get materialized views
|
|
122
|
+
mvs = connector.get_materialized_views(database_name="mydb")
|
|
123
|
+
|
|
124
|
+
# Get materialized views with full DDL
|
|
125
|
+
mvs_with_ddl = connector.get_materialized_views_with_ddl(database_name="mydb")
|
|
126
|
+
|
|
127
|
+
for mv in mvs_with_ddl:
|
|
128
|
+
print(f"Name: {mv['table_name']}")
|
|
129
|
+
print(f"Database: {mv['database_name']}")
|
|
130
|
+
print(f"Catalog: {mv['catalog_name']}")
|
|
131
|
+
print(f"Definition: {mv['definition']}")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Fully-Qualified Names
|
|
135
|
+
|
|
136
|
+
StarRocks supports three-part names: `catalog.database.table`
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Build full name
|
|
140
|
+
full_name = connector.full_name(
|
|
141
|
+
catalog_name="default_catalog",
|
|
142
|
+
database_name="mydb",
|
|
143
|
+
table_name="users"
|
|
144
|
+
)
|
|
145
|
+
# Result: `default_catalog`.`mydb`.`users`
|
|
146
|
+
|
|
147
|
+
# Query with full name
|
|
148
|
+
result = connector.execute_query(f"SELECT * FROM {full_name} LIMIT 10")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Requirements
|
|
152
|
+
|
|
153
|
+
- Python >= 3.10
|
|
154
|
+
- StarRocks >= 2.0
|
|
155
|
+
- datus-agent >= 0.3.0
|
|
156
|
+
- datus-mysql >= 0.1.0
|
|
157
|
+
|
|
158
|
+
## Connection Cleanup
|
|
159
|
+
|
|
160
|
+
The connector includes special handling for PyMySQL cleanup errors that can occur with StarRocks connections. Use the context manager pattern for automatic cleanup:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
with StarRocksConnector(...) as connector:
|
|
164
|
+
# Your code here
|
|
165
|
+
pass
|
|
166
|
+
# Connection automatically cleaned up
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
Apache License 2.0
|
|
172
|
+
|
|
173
|
+
## Related Packages
|
|
174
|
+
|
|
175
|
+
- `datus-mysql` - MySQL adapter (base for StarRocks)
|
|
176
|
+
- `datus-sqlalchemy` - SQLAlchemy base connector
|
|
177
|
+
- `datus-snowflake` - Snowflake adapter
|
|
@@ -0,0 +1,16 @@
|
|
|
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 .config import StarRocksConfig
|
|
6
|
+
from .connector import StarRocksConnector
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__all__ = ["StarRocksConnector", "StarRocksConfig", "register"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register():
|
|
13
|
+
"""Register StarRocks connector with Datus registry."""
|
|
14
|
+
from datus.tools.db_tools import connector_registry
|
|
15
|
+
|
|
16
|
+
connector_registry.register("starrocks", StarRocksConnector)
|
|
@@ -0,0 +1,23 @@
|
|
|
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 typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StarRocksConfig(BaseModel):
|
|
11
|
+
"""StarRocks-specific configuration."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="forbid")
|
|
14
|
+
|
|
15
|
+
host: str = Field(..., description="StarRocks server host")
|
|
16
|
+
port: int = Field(default=9030, description="StarRocks server port")
|
|
17
|
+
username: str = Field(..., description="StarRocks username")
|
|
18
|
+
password: str = Field(default="", description="StarRocks password")
|
|
19
|
+
catalog: str = Field(default="default_catalog", description="Default catalog name")
|
|
20
|
+
database: Optional[str] = Field(default=None, description="Default database name")
|
|
21
|
+
charset: str = Field(default="utf8mb4", description="Character set to use")
|
|
22
|
+
autocommit: bool = Field(default=True, description="Enable autocommit mode")
|
|
23
|
+
timeout_seconds: int = Field(default=30, description="Connection timeout in seconds")
|
|
@@ -0,0 +1,340 @@
|
|
|
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 typing import Any, Dict, List, Union, override
|
|
6
|
+
|
|
7
|
+
from datus.tools.db_tools.base import list_to_in_str
|
|
8
|
+
from datus.tools.db_tools.mixins import CatalogSupportMixin, MaterializedViewSupportMixin
|
|
9
|
+
from datus.utils.constants import DBType
|
|
10
|
+
from datus.utils.loggings import get_logger
|
|
11
|
+
from datus_mysql import MySQLConnector
|
|
12
|
+
|
|
13
|
+
from .config import StarRocksConfig
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StarRocksConnector(MySQLConnector, CatalogSupportMixin, MaterializedViewSupportMixin):
|
|
19
|
+
"""
|
|
20
|
+
StarRocks database connector.
|
|
21
|
+
|
|
22
|
+
StarRocks uses MySQL protocol but adds multi-catalog support and materialized views.
|
|
23
|
+
This connector implements CatalogSupportMixin and MaterializedViewSupportMixin.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: Union[StarRocksConfig, dict]):
|
|
27
|
+
"""
|
|
28
|
+
Initialize StarRocks connector.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config: StarRocksConfig object or dict with configuration
|
|
32
|
+
"""
|
|
33
|
+
# Handle config object or dict
|
|
34
|
+
if isinstance(config, dict):
|
|
35
|
+
config = StarRocksConfig(**config)
|
|
36
|
+
elif not isinstance(config, StarRocksConfig):
|
|
37
|
+
raise TypeError(f"config must be StarRocksConfig or dict, got {type(config)}")
|
|
38
|
+
|
|
39
|
+
self.starrocks_config = config
|
|
40
|
+
|
|
41
|
+
# Pass MySQL config to parent connector
|
|
42
|
+
from datus_mysql import MySQLConfig
|
|
43
|
+
|
|
44
|
+
mysql_config = MySQLConfig(
|
|
45
|
+
host=config.host,
|
|
46
|
+
port=config.port,
|
|
47
|
+
username=config.username,
|
|
48
|
+
password=config.password,
|
|
49
|
+
database=config.database or "",
|
|
50
|
+
charset=config.charset,
|
|
51
|
+
autocommit=config.autocommit,
|
|
52
|
+
timeout_seconds=config.timeout_seconds,
|
|
53
|
+
)
|
|
54
|
+
super().__init__(mysql_config)
|
|
55
|
+
|
|
56
|
+
self.catalog_name = config.catalog
|
|
57
|
+
|
|
58
|
+
# Override dialect to StarRocks
|
|
59
|
+
self.dialect = DBType.STARROCKS
|
|
60
|
+
|
|
61
|
+
# ==================== Context Manager Support ====================
|
|
62
|
+
|
|
63
|
+
def __enter__(self):
|
|
64
|
+
"""Context manager entry."""
|
|
65
|
+
self.connect()
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
69
|
+
"""Context manager exit with cleanup."""
|
|
70
|
+
self.close()
|
|
71
|
+
return False # Don't suppress exceptions
|
|
72
|
+
|
|
73
|
+
# ==================== Catalog Management (CatalogSupportMixin) ====================
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
def default_catalog(self) -> str:
|
|
77
|
+
"""StarRocks default catalog."""
|
|
78
|
+
return "default_catalog"
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def get_catalogs(self) -> List[str]:
|
|
82
|
+
"""Get list of catalogs."""
|
|
83
|
+
result = self._execute_pandas("SHOW CATALOGS")
|
|
84
|
+
if result.empty:
|
|
85
|
+
return []
|
|
86
|
+
return result["Catalog"].tolist()
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
def switch_catalog(self, catalog_name: str) -> None:
|
|
90
|
+
"""Switch to a different catalog.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
catalog_name: Name of the catalog to switch to
|
|
94
|
+
"""
|
|
95
|
+
self.switch_context(catalog_name=catalog_name)
|
|
96
|
+
self.catalog_name = catalog_name
|
|
97
|
+
|
|
98
|
+
def reset_catalog_to_default(self, catalog: str) -> str:
|
|
99
|
+
"""Reset the catalog to the default catalog if it is not set or is 'def'."""
|
|
100
|
+
if not catalog or catalog == "def":
|
|
101
|
+
return self.default_catalog()
|
|
102
|
+
return catalog
|
|
103
|
+
|
|
104
|
+
def _before_metadata_query(self, catalog_name: str = "", database_name: str = "") -> None:
|
|
105
|
+
"""Switch catalog before metadata queries if needed."""
|
|
106
|
+
target_catalog = catalog_name or self.catalog_name or self.default_catalog()
|
|
107
|
+
if target_catalog and target_catalog != self.catalog_name:
|
|
108
|
+
self.switch_context(catalog_name=target_catalog)
|
|
109
|
+
|
|
110
|
+
# ==================== Metadata Retrieval ====================
|
|
111
|
+
|
|
112
|
+
def _get_metadata(
|
|
113
|
+
self,
|
|
114
|
+
table_type: str = "table",
|
|
115
|
+
catalog_name: str = "",
|
|
116
|
+
database_name: str = "",
|
|
117
|
+
) -> List[Dict[str, str]]:
|
|
118
|
+
"""
|
|
119
|
+
Get metadata for tables/views with catalog support.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
table_type: Type of object (table, view, mv)
|
|
123
|
+
catalog_name: Catalog name
|
|
124
|
+
database_name: Database name to query
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of metadata dictionaries with catalog_name properly set
|
|
128
|
+
"""
|
|
129
|
+
# Determine the target catalog
|
|
130
|
+
current_catalog = self.reset_catalog_to_default(catalog_name or self.catalog_name)
|
|
131
|
+
|
|
132
|
+
# Switch to the correct catalog before querying
|
|
133
|
+
self._before_metadata_query(catalog_name=current_catalog, database_name=database_name)
|
|
134
|
+
|
|
135
|
+
# Get base metadata from parent
|
|
136
|
+
result = super()._get_metadata(table_type, catalog_name, database_name)
|
|
137
|
+
|
|
138
|
+
# Set the correct catalog_name and filter results by catalog as safety check
|
|
139
|
+
filtered_result = []
|
|
140
|
+
for item in result:
|
|
141
|
+
# Filter by catalog if the item has catalog_name set
|
|
142
|
+
if "catalog_name" in item and item["catalog_name"] and item["catalog_name"] != current_catalog:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
item["catalog_name"] = current_catalog
|
|
146
|
+
# Update identifier to include catalog
|
|
147
|
+
item["identifier"] = self.identifier(
|
|
148
|
+
catalog_name=current_catalog, database_name=item["database_name"], table_name=item["table_name"]
|
|
149
|
+
)
|
|
150
|
+
filtered_result.append(item)
|
|
151
|
+
|
|
152
|
+
return filtered_result
|
|
153
|
+
|
|
154
|
+
@override
|
|
155
|
+
def get_tables(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> List[str]:
|
|
156
|
+
"""Get list of table names."""
|
|
157
|
+
result = self._get_metadata(table_type="table", catalog_name=catalog_name, database_name=database_name)
|
|
158
|
+
return [table["table_name"] for table in result]
|
|
159
|
+
|
|
160
|
+
@override
|
|
161
|
+
def get_views(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> List[str]:
|
|
162
|
+
"""Get list of view names."""
|
|
163
|
+
try:
|
|
164
|
+
result = self._get_metadata(table_type="view", catalog_name=catalog_name, database_name=database_name)
|
|
165
|
+
return [view["table_name"] for view in result]
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.warning(f"Failed to get views: {e}")
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
def get_materialized_views(
|
|
171
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
|
|
172
|
+
) -> List[str]:
|
|
173
|
+
"""Get list of materialized view names."""
|
|
174
|
+
try:
|
|
175
|
+
result = self._get_metadata(table_type="mv", catalog_name=catalog_name, database_name=database_name)
|
|
176
|
+
return [mv["table_name"] for mv in result]
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.warning(f"Failed to get materialized views: {e}")
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
def get_materialized_views_with_ddl(
|
|
182
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = ""
|
|
183
|
+
) -> List[Dict[str, str]]:
|
|
184
|
+
"""
|
|
185
|
+
Get materialized views with DDL definitions.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
catalog_name: Catalog name
|
|
189
|
+
database_name: Database name
|
|
190
|
+
schema_name: Schema name (unused in StarRocks)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of materialized view metadata with DDL
|
|
194
|
+
"""
|
|
195
|
+
current_catalog = self.reset_catalog_to_default(catalog_name or self.catalog_name)
|
|
196
|
+
|
|
197
|
+
self._before_metadata_query(catalog_name=current_catalog, database_name=database_name)
|
|
198
|
+
|
|
199
|
+
# Query materialized views from information_schema
|
|
200
|
+
query_sql = (
|
|
201
|
+
"SELECT TABLE_SCHEMA, TABLE_NAME, MATERIALIZED_VIEW_DEFINITION "
|
|
202
|
+
"FROM information_schema.materialized_views"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if database_name:
|
|
206
|
+
query_sql = f"{query_sql} WHERE TABLE_SCHEMA = '{database_name}'"
|
|
207
|
+
else:
|
|
208
|
+
ignore_dbs = list(self._sys_databases())
|
|
209
|
+
query_sql = f"{query_sql} {list_to_in_str('WHERE TABLE_SCHEMA NOT IN', ignore_dbs)}"
|
|
210
|
+
|
|
211
|
+
result = self._execute_pandas(query_sql)
|
|
212
|
+
|
|
213
|
+
mv_list = []
|
|
214
|
+
for i in range(len(result)):
|
|
215
|
+
mv_list.append(
|
|
216
|
+
{
|
|
217
|
+
"identifier": self.identifier(
|
|
218
|
+
catalog_name=current_catalog,
|
|
219
|
+
database_name=str(result["TABLE_SCHEMA"][i]),
|
|
220
|
+
table_name=str(result["TABLE_NAME"][i]),
|
|
221
|
+
),
|
|
222
|
+
"catalog_name": current_catalog,
|
|
223
|
+
"database_name": result["TABLE_SCHEMA"][i],
|
|
224
|
+
"schema_name": "",
|
|
225
|
+
"table_name": result["TABLE_NAME"][i],
|
|
226
|
+
"definition": result["MATERIALIZED_VIEW_DEFINITION"][i],
|
|
227
|
+
"table_type": "mv",
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return mv_list
|
|
232
|
+
|
|
233
|
+
# ==================== Database Management ====================
|
|
234
|
+
|
|
235
|
+
@override
|
|
236
|
+
def get_databases(self, catalog_name: str = "", include_sys: bool = False) -> List[str]:
|
|
237
|
+
"""Get list of databases in the catalog."""
|
|
238
|
+
return super().get_databases(catalog_name, include_sys=include_sys)
|
|
239
|
+
|
|
240
|
+
# ==================== Full Name Construction ====================
|
|
241
|
+
|
|
242
|
+
@override
|
|
243
|
+
def full_name(
|
|
244
|
+
self, catalog_name: str = "", database_name: str = "", schema_name: str = "", table_name: str = ""
|
|
245
|
+
) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Build fully-qualified table name with catalog support.
|
|
248
|
+
|
|
249
|
+
StarRocks format: `catalog`.`database`.`table`
|
|
250
|
+
"""
|
|
251
|
+
catalog_name = self.reset_catalog_to_default(catalog_name)
|
|
252
|
+
|
|
253
|
+
if catalog_name:
|
|
254
|
+
if database_name:
|
|
255
|
+
return f"`{catalog_name}`.`{database_name}`.`{table_name}`"
|
|
256
|
+
else:
|
|
257
|
+
return f"`{table_name}`"
|
|
258
|
+
else:
|
|
259
|
+
if database_name:
|
|
260
|
+
return f"`{database_name}`.`{table_name}`"
|
|
261
|
+
return f"`{table_name}`"
|
|
262
|
+
|
|
263
|
+
@override
|
|
264
|
+
def _sqlalchemy_schema(self, catalog_name: str = "", database_name: str = "", schema_name: str = "") -> str:
|
|
265
|
+
"""Get schema name for SQLAlchemy Inspector with catalog support."""
|
|
266
|
+
database_name = database_name or self.database_name
|
|
267
|
+
|
|
268
|
+
if self.support_catalog():
|
|
269
|
+
catalog_name = catalog_name or self.catalog_name or self.default_catalog()
|
|
270
|
+
if database_name:
|
|
271
|
+
return f"{catalog_name}.{database_name}"
|
|
272
|
+
return None
|
|
273
|
+
else:
|
|
274
|
+
return database_name if database_name else None
|
|
275
|
+
|
|
276
|
+
# ==================== Connection Cleanup ====================
|
|
277
|
+
|
|
278
|
+
@override
|
|
279
|
+
def close(self):
|
|
280
|
+
"""
|
|
281
|
+
Close connection with special handling for PyMySQL cleanup errors.
|
|
282
|
+
|
|
283
|
+
StarRocks may trigger PyMySQL struct.pack errors during cleanup,
|
|
284
|
+
which we safely ignore.
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
super().close()
|
|
288
|
+
except Exception as e:
|
|
289
|
+
error_str = str(e)
|
|
290
|
+
|
|
291
|
+
# Check for known PyMySQL cleanup errors
|
|
292
|
+
pymysql_errors = ["struct.error", "struct.pack", "COMMAND.COM_QUIT", "required argument is not an integer"]
|
|
293
|
+
|
|
294
|
+
if any(err in error_str for err in pymysql_errors):
|
|
295
|
+
logger.debug(f"Ignoring PyMySQL cleanup error: {e}")
|
|
296
|
+
|
|
297
|
+
# Force cleanup of connection variables
|
|
298
|
+
if hasattr(self, "connection"):
|
|
299
|
+
self.connection = None
|
|
300
|
+
if hasattr(self, "engine"):
|
|
301
|
+
try:
|
|
302
|
+
if self.engine:
|
|
303
|
+
self.engine.dispose()
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|
|
306
|
+
finally:
|
|
307
|
+
self.engine = None
|
|
308
|
+
else:
|
|
309
|
+
# Re-raise unexpected errors
|
|
310
|
+
logger.error(f"Unexpected close error: {e}")
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
# ==================== Utility Methods ====================
|
|
314
|
+
|
|
315
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
316
|
+
"""Convert connector to serializable dictionary."""
|
|
317
|
+
return {
|
|
318
|
+
"db_type": DBType.STARROCKS,
|
|
319
|
+
"host": self.host,
|
|
320
|
+
"port": self.port,
|
|
321
|
+
"user": self.user,
|
|
322
|
+
"catalog": self.catalog_name,
|
|
323
|
+
"database": self.database_name,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def get_type(self) -> str:
|
|
327
|
+
"""Return the database type."""
|
|
328
|
+
return DBType.STARROCKS
|
|
329
|
+
|
|
330
|
+
@override
|
|
331
|
+
def test_connection(self) -> bool:
|
|
332
|
+
"""Test the database connection with proper cleanup."""
|
|
333
|
+
try:
|
|
334
|
+
return super().test_connection()
|
|
335
|
+
finally:
|
|
336
|
+
# Ensure connection is closed after test
|
|
337
|
+
try:
|
|
338
|
+
self.close()
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.debug(f"Ignoring cleanup error during test: {e}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "datus-starrocks"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "StarRocks database adapter for Datus"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = {text = "Apache-2.0"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "DatusAI", email = "support@datus.ai"}
|
|
10
|
+
]
|
|
11
|
+
keywords = ["datus", "database", "starrocks", "adapter"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"datus-agent>0.2.1",
|
|
22
|
+
"datus-mysql>=0.1.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = [
|
|
27
|
+
"pytest>=7.0.0",
|
|
28
|
+
"pytest-cov>=4.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Datus-ai/datus-db-adapters"
|
|
33
|
+
Repository = "https://github.com/Datus-ai/datus-db-adapters"
|
|
34
|
+
Issues = "https://github.com/Datus-ai/datus-db-adapters/issues"
|
|
35
|
+
|
|
36
|
+
[project.entry-points."datus.adapters"]
|
|
37
|
+
starrocks = "datus_starrocks:register"
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources]
|
|
40
|
+
datus-mysql = { workspace = true }
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["hatchling"]
|
|
44
|
+
build-backend = "hatchling.build"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["datus_starrocks"]
|
|
File without changes
|
|
@@ -0,0 +1,429 @@
|
|
|
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
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Generator
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from datus.tools.db_tools.mixins import CatalogSupportMixin, MaterializedViewSupportMixin
|
|
11
|
+
from datus.utils.exceptions import DatusException, ErrorCode
|
|
12
|
+
from datus_starrocks import StarRocksConfig, StarRocksConnector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def config() -> StarRocksConfig:
|
|
17
|
+
"""Create StarRocks configuration from environment or defaults."""
|
|
18
|
+
return StarRocksConfig(
|
|
19
|
+
host=os.getenv("STARROCKS_HOST", "localhost"),
|
|
20
|
+
port=int(os.getenv("STARROCKS_PORT", "9030")),
|
|
21
|
+
username=os.getenv("STARROCKS_USER", "root"),
|
|
22
|
+
password=os.getenv("STARROCKS_PASSWORD", ""),
|
|
23
|
+
catalog=os.getenv("STARROCKS_CATALOG", "default_catalog"),
|
|
24
|
+
database=os.getenv("STARROCKS_DATABASE", "quickstart"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def connector(config: StarRocksConfig) -> Generator[StarRocksConnector, None, None]:
|
|
30
|
+
"""Create and cleanup StarRocks connector."""
|
|
31
|
+
conn = StarRocksConnector(config)
|
|
32
|
+
yield conn
|
|
33
|
+
conn.close()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ==================== Mixin Tests ====================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_connector_implements_catalog_mixin(connector: StarRocksConnector):
|
|
40
|
+
"""Verify StarRocks connector implements CatalogSupportMixin."""
|
|
41
|
+
assert isinstance(connector, CatalogSupportMixin)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_connector_implements_materialized_view_mixin(connector: StarRocksConnector):
|
|
45
|
+
"""Verify StarRocks connector implements MaterializedViewSupportMixin."""
|
|
46
|
+
assert isinstance(connector, MaterializedViewSupportMixin)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ==================== Connection Tests ====================
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_connection_with_config_object(config: StarRocksConfig):
|
|
53
|
+
"""Test connection using config object."""
|
|
54
|
+
conn = StarRocksConnector(config)
|
|
55
|
+
assert conn.test_connection()
|
|
56
|
+
conn.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_connection_with_dict():
|
|
60
|
+
"""Test connection using dict config."""
|
|
61
|
+
conn = StarRocksConnector(
|
|
62
|
+
{
|
|
63
|
+
"host": os.getenv("STARROCKS_HOST", "localhost"),
|
|
64
|
+
"port": int(os.getenv("STARROCKS_PORT", "9030")),
|
|
65
|
+
"username": os.getenv("STARROCKS_USER", "root"),
|
|
66
|
+
"password": os.getenv("STARROCKS_PASSWORD", ""),
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
assert conn.test_connection()
|
|
70
|
+
conn.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_context_manager(config: StarRocksConfig):
|
|
74
|
+
"""Test connector as context manager."""
|
|
75
|
+
with StarRocksConnector(config) as conn:
|
|
76
|
+
assert conn.test_connection()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ==================== Catalog Tests (CatalogSupportMixin) ====================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_get_catalogs(connector: StarRocksConnector):
|
|
83
|
+
"""Test getting list of catalogs."""
|
|
84
|
+
catalogs = connector.get_catalogs()
|
|
85
|
+
assert len(catalogs) > 0
|
|
86
|
+
assert connector.default_catalog() in catalogs
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_default_catalog(connector: StarRocksConnector):
|
|
90
|
+
"""Test default catalog."""
|
|
91
|
+
assert connector.default_catalog() == "default_catalog"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_switch_catalog(connector: StarRocksConnector):
|
|
95
|
+
"""Test switching catalogs."""
|
|
96
|
+
original_catalog = connector.catalog_name
|
|
97
|
+
catalogs = connector.get_catalogs()
|
|
98
|
+
|
|
99
|
+
if len(catalogs) > 1:
|
|
100
|
+
target_catalog = [c for c in catalogs if c != original_catalog][0]
|
|
101
|
+
connector.switch_catalog(target_catalog)
|
|
102
|
+
assert connector.catalog_name == target_catalog
|
|
103
|
+
|
|
104
|
+
# Switch back
|
|
105
|
+
connector.switch_catalog(original_catalog)
|
|
106
|
+
assert connector.catalog_name == original_catalog
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ==================== Database Tests ====================
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_get_databases(connector: StarRocksConnector):
|
|
113
|
+
"""Test getting list of databases."""
|
|
114
|
+
databases = connector.get_databases()
|
|
115
|
+
assert len(databases) > 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ==================== Table Metadata Tests ====================
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_get_tables(connector: StarRocksConnector):
|
|
122
|
+
"""Test getting table list."""
|
|
123
|
+
tables = connector.get_tables()
|
|
124
|
+
assert isinstance(tables, list)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_get_tables_with_ddl(connector: StarRocksConnector, config: StarRocksConfig):
|
|
128
|
+
"""Test getting tables with DDL."""
|
|
129
|
+
tables = connector.get_tables_with_ddl(catalog_name=config.catalog)
|
|
130
|
+
|
|
131
|
+
if len(tables) > 0:
|
|
132
|
+
table = tables[0]
|
|
133
|
+
assert "table_name" in table
|
|
134
|
+
assert "definition" in table
|
|
135
|
+
assert table["table_type"] == "table"
|
|
136
|
+
assert "database_name" in table
|
|
137
|
+
assert table["schema_name"] == ""
|
|
138
|
+
assert table["catalog_name"] == config.catalog
|
|
139
|
+
assert "identifier" in table
|
|
140
|
+
assert len(table["identifier"].split(".")) == 3
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ==================== View Tests ====================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_get_views(connector: StarRocksConnector):
|
|
147
|
+
"""Test getting view list."""
|
|
148
|
+
views = connector.get_views()
|
|
149
|
+
assert isinstance(views, list)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_get_views_with_ddl(connector: StarRocksConnector, config: StarRocksConfig):
|
|
153
|
+
"""Test getting views with DDL."""
|
|
154
|
+
views = connector.get_views_with_ddl(catalog_name=config.catalog)
|
|
155
|
+
|
|
156
|
+
if len(views) > 0:
|
|
157
|
+
view = views[0]
|
|
158
|
+
assert "table_name" in view
|
|
159
|
+
assert "definition" in view
|
|
160
|
+
assert view["table_type"] == "view"
|
|
161
|
+
assert "database_name" in view
|
|
162
|
+
assert view["schema_name"] == ""
|
|
163
|
+
assert "catalog_name" in view
|
|
164
|
+
|
|
165
|
+
identifier_parts = view["identifier"].split(".")
|
|
166
|
+
assert len(identifier_parts) == 3
|
|
167
|
+
assert identifier_parts[0] == view["catalog_name"]
|
|
168
|
+
assert identifier_parts[1] == view["database_name"]
|
|
169
|
+
assert identifier_parts[2] == view["table_name"]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ==================== Materialized View Tests (MaterializedViewSupportMixin) ====================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_get_materialized_views(connector: StarRocksConnector, config: StarRocksConfig):
|
|
176
|
+
"""Test getting materialized view list."""
|
|
177
|
+
mvs = connector.get_materialized_views(catalog_name=config.catalog)
|
|
178
|
+
assert isinstance(mvs, list)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_get_materialized_views_with_ddl(connector: StarRocksConnector):
|
|
182
|
+
"""Test getting materialized views with DDL."""
|
|
183
|
+
mvs = connector.get_materialized_views_with_ddl()
|
|
184
|
+
|
|
185
|
+
if len(mvs) > 0:
|
|
186
|
+
mv = mvs[0]
|
|
187
|
+
assert "table_name" in mv
|
|
188
|
+
assert "definition" in mv
|
|
189
|
+
assert mv["table_type"] == "mv"
|
|
190
|
+
assert "database_name" in mv
|
|
191
|
+
assert mv["schema_name"] == ""
|
|
192
|
+
assert "catalog_name" in mv
|
|
193
|
+
|
|
194
|
+
identifier_parts = mv["identifier"].split(".")
|
|
195
|
+
assert len(identifier_parts) == 3
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ==================== Sample Data Tests ====================
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_get_sample_rows_default(connector: StarRocksConnector):
|
|
202
|
+
"""Test getting sample rows with defaults."""
|
|
203
|
+
sample_rows = connector.get_sample_rows()
|
|
204
|
+
assert isinstance(sample_rows, list)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_get_sample_rows_with_database(connector: StarRocksConnector, config: StarRocksConfig):
|
|
208
|
+
"""Test getting sample rows for specific database."""
|
|
209
|
+
sample_rows = connector.get_sample_rows(catalog_name=config.catalog, database_name=config.database)
|
|
210
|
+
|
|
211
|
+
if len(sample_rows) > 0:
|
|
212
|
+
item = sample_rows[0]
|
|
213
|
+
assert "database_name" in item
|
|
214
|
+
assert "table_name" in item
|
|
215
|
+
assert "catalog_name" in item
|
|
216
|
+
assert item["schema_name"] == ""
|
|
217
|
+
assert "identifier" in item
|
|
218
|
+
assert len(item["identifier"].split(".")) == 3
|
|
219
|
+
assert "sample_rows" in item
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_get_sample_rows_specific_tables(connector: StarRocksConnector, config: StarRocksConfig):
|
|
223
|
+
"""Test getting sample rows for specific tables."""
|
|
224
|
+
# First get available tables
|
|
225
|
+
tables = connector.get_tables(catalog_name=config.catalog, database_name=config.database)
|
|
226
|
+
|
|
227
|
+
if len(tables) > 0:
|
|
228
|
+
table_name = tables[0]
|
|
229
|
+
sample_rows = connector.get_sample_rows(
|
|
230
|
+
catalog_name=config.catalog, database_name=config.database, tables=[table_name], top_n=3
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert len(sample_rows) == 1
|
|
234
|
+
assert sample_rows[0]["table_name"] == table_name
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ==================== SQL Execution Tests ====================
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_execute_query(connector: StarRocksConnector):
|
|
241
|
+
"""Test executing simple query."""
|
|
242
|
+
result = connector.execute({"sql_query": "SELECT 1 as num"}, result_format="list")
|
|
243
|
+
assert result.success
|
|
244
|
+
assert not result.error
|
|
245
|
+
assert result.sql_return == [{"num": 1}]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_execute_explain(connector: StarRocksConnector, config: StarRocksConfig):
|
|
249
|
+
"""Test executing EXPLAIN query."""
|
|
250
|
+
tables = connector.get_tables(catalog_name=config.catalog, database_name=config.database)
|
|
251
|
+
|
|
252
|
+
if len(tables) > 0:
|
|
253
|
+
table_name = tables[0]
|
|
254
|
+
full_name = connector.full_name(
|
|
255
|
+
catalog_name=config.catalog, database_name=config.database, table_name=table_name
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
result = connector.execute({"sql_query": f"EXPLAIN SELECT * FROM {full_name} LIMIT 1"})
|
|
259
|
+
assert result.success
|
|
260
|
+
assert not result.error
|
|
261
|
+
assert result.sql_return
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_execute_ddl_create_drop(connector: StarRocksConnector, config: StarRocksConfig):
|
|
265
|
+
"""Test DDL operations (CREATE/DROP)."""
|
|
266
|
+
suffix = uuid.uuid4().hex[:8]
|
|
267
|
+
table_name = f"datus_test_{suffix}"
|
|
268
|
+
|
|
269
|
+
connector.switch_context(database_name=config.database)
|
|
270
|
+
|
|
271
|
+
create_sql = f"""
|
|
272
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
273
|
+
`id` BIGINT NOT NULL,
|
|
274
|
+
`name` VARCHAR(64)
|
|
275
|
+
) ENGINE=OLAP
|
|
276
|
+
PRIMARY KEY (`id`)
|
|
277
|
+
DISTRIBUTED BY HASH(`id`) BUCKETS 1
|
|
278
|
+
PROPERTIES (
|
|
279
|
+
"replication_num" = "1"
|
|
280
|
+
);
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
create_result = connector.execute_ddl(create_sql)
|
|
285
|
+
assert create_result.success, f"Failed to create table: {create_result.error}"
|
|
286
|
+
finally:
|
|
287
|
+
connector.execute_ddl(f"DROP TABLE IF EXISTS {table_name}")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_execute_insert(connector: StarRocksConnector, config: StarRocksConfig):
|
|
291
|
+
"""Test INSERT operation."""
|
|
292
|
+
suffix = uuid.uuid4().hex[:8]
|
|
293
|
+
table_name = f"datus_insert_test_{suffix}"
|
|
294
|
+
|
|
295
|
+
connector.switch_context(database_name=config.database)
|
|
296
|
+
|
|
297
|
+
create_sql = f"""
|
|
298
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
299
|
+
`id` BIGINT NOT NULL,
|
|
300
|
+
`name` VARCHAR(64)
|
|
301
|
+
) ENGINE=OLAP
|
|
302
|
+
PRIMARY KEY (`id`)
|
|
303
|
+
DISTRIBUTED BY HASH(`id`) BUCKETS 1
|
|
304
|
+
PROPERTIES (
|
|
305
|
+
"replication_num" = "1"
|
|
306
|
+
);
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
create_result = connector.execute_ddl(create_sql)
|
|
311
|
+
if not create_result.success:
|
|
312
|
+
pytest.skip(f"Unable to create test table: {create_result.error}")
|
|
313
|
+
|
|
314
|
+
# Insert data
|
|
315
|
+
insert_result = connector.execute_insert(f"INSERT INTO {table_name} (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
|
|
316
|
+
assert insert_result.success
|
|
317
|
+
|
|
318
|
+
# Verify
|
|
319
|
+
query_result = connector.execute(
|
|
320
|
+
{"sql_query": f"SELECT id, name FROM {table_name} ORDER BY id"}, result_format="list"
|
|
321
|
+
)
|
|
322
|
+
assert query_result.success
|
|
323
|
+
assert query_result.sql_return == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
|
|
324
|
+
finally:
|
|
325
|
+
connector.execute_ddl(f"DROP TABLE IF EXISTS {table_name}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_execute_update(connector: StarRocksConnector, config: StarRocksConfig):
|
|
329
|
+
"""Test UPDATE operation."""
|
|
330
|
+
suffix = uuid.uuid4().hex[:8]
|
|
331
|
+
table_name = f"datus_update_test_{suffix}"
|
|
332
|
+
|
|
333
|
+
connector.switch_context(database_name=config.database)
|
|
334
|
+
|
|
335
|
+
create_sql = f"""
|
|
336
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
337
|
+
`id` BIGINT NOT NULL,
|
|
338
|
+
`name` VARCHAR(64)
|
|
339
|
+
) ENGINE=OLAP
|
|
340
|
+
PRIMARY KEY (`id`)
|
|
341
|
+
DISTRIBUTED BY HASH(`id`) BUCKETS 1
|
|
342
|
+
PROPERTIES (
|
|
343
|
+
"replication_num" = "1"
|
|
344
|
+
);
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
create_result = connector.execute_ddl(create_sql)
|
|
349
|
+
if not create_result.success:
|
|
350
|
+
pytest.skip(f"Unable to create test table: {create_result.error}")
|
|
351
|
+
|
|
352
|
+
# Insert initial data
|
|
353
|
+
connector.execute_insert(f"INSERT INTO {table_name} (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
|
|
354
|
+
|
|
355
|
+
# Update
|
|
356
|
+
update_result = connector.execute(
|
|
357
|
+
{"sql_query": f"UPDATE {table_name} SET name = 'Alice Updated' WHERE id = 1"}, result_format="list"
|
|
358
|
+
)
|
|
359
|
+
assert update_result.success
|
|
360
|
+
|
|
361
|
+
# Verify
|
|
362
|
+
query_result = connector.execute(
|
|
363
|
+
{"sql_query": f"SELECT id, name FROM {table_name} ORDER BY id"}, result_format="list"
|
|
364
|
+
)
|
|
365
|
+
assert query_result.sql_return == [{"id": 1, "name": "Alice Updated"}, {"id": 2, "name": "Bob"}]
|
|
366
|
+
finally:
|
|
367
|
+
connector.execute_ddl(f"DROP TABLE IF EXISTS {table_name}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_execute_delete(connector: StarRocksConnector, config: StarRocksConfig):
|
|
371
|
+
"""Test DELETE operation."""
|
|
372
|
+
suffix = uuid.uuid4().hex[:8]
|
|
373
|
+
table_name = f"datus_delete_test_{suffix}"
|
|
374
|
+
|
|
375
|
+
connector.switch_context(database_name=config.database)
|
|
376
|
+
|
|
377
|
+
create_sql = f"""
|
|
378
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
379
|
+
`id` BIGINT NOT NULL,
|
|
380
|
+
`name` VARCHAR(64)
|
|
381
|
+
) ENGINE=OLAP
|
|
382
|
+
PRIMARY KEY (`id`)
|
|
383
|
+
DISTRIBUTED BY HASH(`id`) BUCKETS 1
|
|
384
|
+
PROPERTIES (
|
|
385
|
+
"replication_num" = "1"
|
|
386
|
+
);
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
create_result = connector.execute_ddl(create_sql)
|
|
391
|
+
if not create_result.success:
|
|
392
|
+
pytest.skip(f"Unable to create test table: {create_result.error}")
|
|
393
|
+
|
|
394
|
+
# Insert initial data
|
|
395
|
+
connector.execute_insert(f"INSERT INTO {table_name} (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
|
|
396
|
+
|
|
397
|
+
# Delete
|
|
398
|
+
delete_result = connector.execute({"sql_query": f"DELETE FROM {table_name} WHERE id = 2"}, result_format="list")
|
|
399
|
+
assert delete_result.success
|
|
400
|
+
|
|
401
|
+
# Verify
|
|
402
|
+
query_result = connector.execute(
|
|
403
|
+
{"sql_query": f"SELECT id, name FROM {table_name} ORDER BY id"}, result_format="list"
|
|
404
|
+
)
|
|
405
|
+
assert query_result.sql_return == [{"id": 1, "name": "Alice"}]
|
|
406
|
+
finally:
|
|
407
|
+
connector.execute_ddl(f"DROP TABLE IF EXISTS {table_name}")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ==================== Error Handling Tests ====================
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_exception_on_nonexistent_table(connector: StarRocksConnector, config: StarRocksConfig):
|
|
414
|
+
"""Test exception handling for non-existent table."""
|
|
415
|
+
with pytest.raises(DatusException, match=ErrorCode.DB_EXECUTION_ERROR.code):
|
|
416
|
+
connector.get_sample_rows(catalog_name=config.catalog, tables=["nonexistent_table_" + uuid.uuid4().hex])
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_execute_merge_returns_error(connector: StarRocksConnector):
|
|
420
|
+
"""Test MERGE statement error handling."""
|
|
421
|
+
merge_sql = (
|
|
422
|
+
"MERGE INTO nonexistent_target AS t USING nonexistent_source AS s ON t.id = s.id "
|
|
423
|
+
"WHEN MATCHED THEN UPDATE SET t.value = s.value "
|
|
424
|
+
"WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value)"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
result = connector.execute({"sql_query": merge_sql})
|
|
428
|
+
assert result.sql_query == merge_sql
|
|
429
|
+
assert not result.success or result.error # Either fails or returns error
|