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.
@@ -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