matrixone-python-sdk 0.1.0__py3-none-any.whl
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.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
@@ -0,0 +1,579 @@
|
|
1
|
+
# Copyright 2021 - 2022 Matrix Origin
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
Comprehensive online tests for MatrixOne metadata operations
|
17
|
+
|
18
|
+
This test suite demonstrates and validates:
|
19
|
+
1. Basic metadata scanning (raw SQLAlchemy results)
|
20
|
+
2. Structured metadata with fixed schema
|
21
|
+
3. Column selection capabilities (enum and string-based)
|
22
|
+
4. Synchronous and asynchronous operations
|
23
|
+
5. Transaction-based metadata operations
|
24
|
+
6. Table brief and detailed statistics
|
25
|
+
7. Index metadata scanning with moctl integration
|
26
|
+
8. Type-safe metadata row objects
|
27
|
+
9. Error handling and edge cases
|
28
|
+
"""
|
29
|
+
|
30
|
+
import asyncio
|
31
|
+
import unittest
|
32
|
+
from matrixone import Client, AsyncClient
|
33
|
+
from matrixone.metadata import MetadataColumn, MetadataRow
|
34
|
+
from matrixone.config import get_connection_kwargs
|
35
|
+
|
36
|
+
|
37
|
+
class TestMetadataOnline(unittest.TestCase):
|
38
|
+
"""Comprehensive test suite for metadata operations with real MatrixOne database"""
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def setUpClass(cls):
|
42
|
+
"""Set up test database and tables using raw SQL"""
|
43
|
+
cls.connection_params = get_connection_kwargs()
|
44
|
+
# Filter out unsupported parameters
|
45
|
+
filtered_params = {
|
46
|
+
'host': cls.connection_params['host'],
|
47
|
+
'port': cls.connection_params['port'],
|
48
|
+
'user': cls.connection_params['user'],
|
49
|
+
'password': cls.connection_params['password'],
|
50
|
+
'database': cls.connection_params['database'],
|
51
|
+
}
|
52
|
+
cls.client = Client()
|
53
|
+
cls.client.connect(**filtered_params)
|
54
|
+
|
55
|
+
# Create test database and tables using raw SQL
|
56
|
+
cls.client.execute("CREATE DATABASE IF NOT EXISTS test_metadata_db")
|
57
|
+
cls.client.execute("USE test_metadata_db")
|
58
|
+
|
59
|
+
# Drop existing tables
|
60
|
+
cls.client.execute("DROP TABLE IF EXISTS test_users")
|
61
|
+
cls.client.execute("DROP TABLE IF EXISTS test_products")
|
62
|
+
|
63
|
+
# Create test tables with indexes
|
64
|
+
cls.client.execute(
|
65
|
+
"""
|
66
|
+
CREATE TABLE test_users (
|
67
|
+
id INT PRIMARY KEY,
|
68
|
+
name VARCHAR(100) NOT NULL,
|
69
|
+
email VARCHAR(100) NOT NULL UNIQUE,
|
70
|
+
age INT NOT NULL,
|
71
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
72
|
+
)
|
73
|
+
"""
|
74
|
+
)
|
75
|
+
|
76
|
+
cls.client.execute(
|
77
|
+
"""
|
78
|
+
CREATE TABLE test_products (
|
79
|
+
id INT PRIMARY KEY,
|
80
|
+
name VARCHAR(200) NOT NULL,
|
81
|
+
category VARCHAR(50) NOT NULL,
|
82
|
+
price INT NOT NULL,
|
83
|
+
stock INT DEFAULT 0
|
84
|
+
)
|
85
|
+
"""
|
86
|
+
)
|
87
|
+
|
88
|
+
# Create indexes
|
89
|
+
try:
|
90
|
+
cls.client.execute("CREATE INDEX idx_test_users_name ON test_users(name)")
|
91
|
+
cls.client.execute("CREATE INDEX idx_test_users_email ON test_users(email)")
|
92
|
+
cls.client.execute("CREATE INDEX idx_test_products_category ON test_products(category)")
|
93
|
+
except Exception:
|
94
|
+
# Index creation might fail in some environments
|
95
|
+
pass
|
96
|
+
|
97
|
+
# Insert test data
|
98
|
+
cls._insert_test_data()
|
99
|
+
|
100
|
+
# Single checkpoint after all data is ready - this is expensive, so do it only once
|
101
|
+
cls.client.moctl.increment_checkpoint()
|
102
|
+
|
103
|
+
@classmethod
|
104
|
+
def _insert_test_data(cls):
|
105
|
+
"""Insert comprehensive test data"""
|
106
|
+
# Clear existing data
|
107
|
+
cls.client.execute("DELETE FROM test_users")
|
108
|
+
cls.client.execute("DELETE FROM test_products")
|
109
|
+
|
110
|
+
# Insert users
|
111
|
+
for i in range(1, 51): # 50 users
|
112
|
+
cls.client.execute(
|
113
|
+
f"""
|
114
|
+
INSERT INTO test_users (id, name, email, age)
|
115
|
+
VALUES ({i}, 'User{i}', 'user{i}@example.com', {20 + (i % 50)})
|
116
|
+
"""
|
117
|
+
)
|
118
|
+
|
119
|
+
# Insert products
|
120
|
+
categories = ['Electronics', 'Books', 'Clothing', 'Home', 'Sports']
|
121
|
+
for i in range(1, 31): # 30 products
|
122
|
+
category = categories[i % len(categories)]
|
123
|
+
cls.client.execute(
|
124
|
+
f"""
|
125
|
+
INSERT INTO test_products (id, name, category, price, stock)
|
126
|
+
VALUES ({i}, 'Product{i}', '{category}', {100 + (i * 10)}, {50 + (i % 20)})
|
127
|
+
"""
|
128
|
+
)
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def tearDownClass(cls):
|
132
|
+
"""Clean up test resources"""
|
133
|
+
try:
|
134
|
+
cls.client.disconnect()
|
135
|
+
except Exception:
|
136
|
+
pass
|
137
|
+
|
138
|
+
def test_basic_metadata_scanning(self):
|
139
|
+
"""Test basic metadata scanning operations"""
|
140
|
+
# Test raw SQLAlchemy results
|
141
|
+
result = self.client.metadata.scan("test_metadata_db", "test_users")
|
142
|
+
rows = result.fetchall()
|
143
|
+
|
144
|
+
self.assertGreater(len(rows), 0, "Should have metadata entries")
|
145
|
+
|
146
|
+
# Verify row structure
|
147
|
+
for row in rows:
|
148
|
+
self.assertIn('col_name', row._mapping)
|
149
|
+
self.assertIn('rows_cnt', row._mapping)
|
150
|
+
self.assertIn('origin_size', row._mapping)
|
151
|
+
self.assertIn('null_cnt', row._mapping)
|
152
|
+
|
153
|
+
# Test with specific table
|
154
|
+
result = self.client.metadata.scan("test_metadata_db", "test_products")
|
155
|
+
rows = result.fetchall()
|
156
|
+
self.assertGreater(len(rows), 0, "Should have product metadata entries")
|
157
|
+
|
158
|
+
def test_structured_metadata(self):
|
159
|
+
"""Test structured metadata with fixed schema"""
|
160
|
+
# Get all columns as structured MetadataRow objects
|
161
|
+
rows = self.client.metadata.scan("test_metadata_db", "test_users", columns="*")
|
162
|
+
|
163
|
+
self.assertGreater(len(rows), 0, "Should have structured metadata entries")
|
164
|
+
|
165
|
+
# Verify MetadataRow structure and type safety
|
166
|
+
for row in rows:
|
167
|
+
self.assertIsInstance(row, MetadataRow)
|
168
|
+
self.assertIsInstance(row.col_name, str)
|
169
|
+
self.assertIsInstance(row.rows_cnt, int)
|
170
|
+
self.assertIsInstance(row.null_cnt, int)
|
171
|
+
self.assertIsInstance(row.origin_size, int)
|
172
|
+
self.assertIsInstance(row.is_hidden, bool)
|
173
|
+
|
174
|
+
# Verify data integrity
|
175
|
+
self.assertGreaterEqual(row.rows_cnt, 0)
|
176
|
+
self.assertGreaterEqual(row.null_cnt, 0)
|
177
|
+
self.assertGreaterEqual(row.origin_size, 0)
|
178
|
+
|
179
|
+
def test_column_selection_enum(self):
|
180
|
+
"""Test column selection using MetadataColumn enum"""
|
181
|
+
# Select specific columns using enum
|
182
|
+
rows = self.client.metadata.scan(
|
183
|
+
"test_metadata_db",
|
184
|
+
"test_users",
|
185
|
+
columns=[MetadataColumn.COL_NAME, MetadataColumn.ROWS_CNT, MetadataColumn.ORIGIN_SIZE],
|
186
|
+
)
|
187
|
+
|
188
|
+
self.assertGreater(len(rows), 0, "Should have column-selected metadata")
|
189
|
+
|
190
|
+
# Verify selected columns
|
191
|
+
for row in rows:
|
192
|
+
self.assertIn('col_name', row)
|
193
|
+
self.assertIn('rows_cnt', row)
|
194
|
+
self.assertIn('origin_size', row)
|
195
|
+
# Should not have other columns
|
196
|
+
self.assertNotIn('null_cnt', row)
|
197
|
+
|
198
|
+
def test_column_selection_strings(self):
|
199
|
+
"""Test column selection using string names"""
|
200
|
+
# Select specific columns using strings
|
201
|
+
rows = self.client.metadata.scan("test_metadata_db", "test_users", columns=['col_name', 'null_cnt', 'compress_size'])
|
202
|
+
|
203
|
+
self.assertGreater(len(rows), 0, "Should have string-selected metadata")
|
204
|
+
|
205
|
+
# Verify selected columns
|
206
|
+
for row in rows:
|
207
|
+
self.assertIn('col_name', row)
|
208
|
+
self.assertIn('null_cnt', row)
|
209
|
+
self.assertIn('compress_size', row)
|
210
|
+
# Should not have other columns
|
211
|
+
self.assertNotIn('rows_cnt', row)
|
212
|
+
|
213
|
+
def test_distinct_object_name(self):
|
214
|
+
"""Test distinct object name functionality"""
|
215
|
+
rows = self.client.metadata.scan("test_metadata_db", "test_users", distinct_object_name=True)
|
216
|
+
|
217
|
+
# Should return distinct object names (might be empty in some environments)
|
218
|
+
object_names = set()
|
219
|
+
for row in rows:
|
220
|
+
object_name = row._mapping['object_name']
|
221
|
+
object_names.add(object_name)
|
222
|
+
|
223
|
+
# Just verify the call doesn't fail, distinct object names might be empty
|
224
|
+
self.assertIsInstance(object_names, set)
|
225
|
+
|
226
|
+
def test_table_brief_stats(self):
|
227
|
+
"""Test table brief statistics"""
|
228
|
+
brief_stats = self.client.metadata.get_table_brief_stats("test_metadata_db", "test_users")
|
229
|
+
|
230
|
+
self.assertIn("test_users", brief_stats, "Should have table stats")
|
231
|
+
table_stats = brief_stats["test_users"]
|
232
|
+
|
233
|
+
# Verify required fields
|
234
|
+
required_fields = ['total_objects', 'row_cnt', 'null_cnt', 'original_size', 'compress_size']
|
235
|
+
for field in required_fields:
|
236
|
+
self.assertIn(field, table_stats, f"Should have {field}")
|
237
|
+
|
238
|
+
# Verify data types and values
|
239
|
+
self.assertIsInstance(table_stats['total_objects'], int)
|
240
|
+
self.assertIsInstance(table_stats['row_cnt'], int)
|
241
|
+
self.assertIsInstance(table_stats['null_cnt'], int)
|
242
|
+
self.assertIsInstance(table_stats['original_size'], str) # Formatted size
|
243
|
+
self.assertIsInstance(table_stats['compress_size'], str) # Formatted size
|
244
|
+
|
245
|
+
# Verify reasonable values
|
246
|
+
self.assertGreater(table_stats['total_objects'], 0)
|
247
|
+
self.assertGreater(table_stats['row_cnt'], 0)
|
248
|
+
self.assertGreaterEqual(table_stats['null_cnt'], 0)
|
249
|
+
|
250
|
+
def test_table_detail_stats(self):
|
251
|
+
"""Test table detailed statistics"""
|
252
|
+
detail_stats = self.client.metadata.get_table_detail_stats("test_metadata_db", "test_users")
|
253
|
+
|
254
|
+
self.assertIn("test_users", detail_stats, "Should have detailed table stats")
|
255
|
+
table_details = detail_stats["test_users"]
|
256
|
+
|
257
|
+
self.assertGreater(len(table_details), 0, "Should have detailed object stats")
|
258
|
+
|
259
|
+
# Verify object structure
|
260
|
+
for detail in table_details:
|
261
|
+
required_fields = ['object_name', 'create_ts', 'row_cnt', 'null_cnt', 'original_size', 'compress_size']
|
262
|
+
for field in required_fields:
|
263
|
+
self.assertIn(field, detail, f"Should have {field}")
|
264
|
+
|
265
|
+
# Verify data types
|
266
|
+
self.assertIsInstance(detail['object_name'], str)
|
267
|
+
self.assertIsInstance(detail['row_cnt'], int)
|
268
|
+
self.assertIsInstance(detail['null_cnt'], int)
|
269
|
+
self.assertIsInstance(detail['original_size'], str) # Formatted size
|
270
|
+
self.assertIsInstance(detail['compress_size'], str) # Formatted size
|
271
|
+
|
272
|
+
def test_index_metadata_scanning(self):
|
273
|
+
"""Test index metadata scanning"""
|
274
|
+
# Test index metadata scanning
|
275
|
+
try:
|
276
|
+
result = self.client.metadata.scan("test_metadata_db", "test_users", indexname="idx_test_users_name")
|
277
|
+
rows = result.fetchall()
|
278
|
+
|
279
|
+
# Index metadata might be empty in some environments
|
280
|
+
# Just verify the call doesn't fail
|
281
|
+
self.assertIsInstance(rows, list)
|
282
|
+
|
283
|
+
except Exception as e:
|
284
|
+
# Index metadata might not be available in all environments
|
285
|
+
self.assertIn("index", str(e).lower())
|
286
|
+
|
287
|
+
def test_transaction_metadata_operations(self):
|
288
|
+
"""Test metadata operations within transactions"""
|
289
|
+
with self.client.transaction() as tx:
|
290
|
+
# Metadata scan within transaction
|
291
|
+
result = tx.metadata.scan("test_metadata_db", "test_users", columns="*")
|
292
|
+
rows = list(result)
|
293
|
+
|
294
|
+
self.assertGreater(len(rows), 0, "Should have metadata in transaction")
|
295
|
+
|
296
|
+
# Table statistics within transaction
|
297
|
+
brief_stats = tx.metadata.get_table_brief_stats("test_metadata_db", "test_users")
|
298
|
+
self.assertIn("test_users", brief_stats)
|
299
|
+
|
300
|
+
detail_stats = tx.metadata.get_table_detail_stats("test_metadata_db", "test_users")
|
301
|
+
self.assertIn("test_users", detail_stats)
|
302
|
+
|
303
|
+
def test_metadata_row_from_sqlalchemy(self):
|
304
|
+
"""Test MetadataRow creation from SQLAlchemy row"""
|
305
|
+
# Get a raw SQLAlchemy row
|
306
|
+
result = self.client.metadata.scan("test_metadata_db", "test_users")
|
307
|
+
raw_row = result.fetchone()
|
308
|
+
|
309
|
+
# Convert to MetadataRow
|
310
|
+
metadata_row = MetadataRow.from_sqlalchemy_row(raw_row)
|
311
|
+
|
312
|
+
# Verify conversion
|
313
|
+
self.assertIsInstance(metadata_row, MetadataRow)
|
314
|
+
self.assertIsInstance(metadata_row.col_name, str)
|
315
|
+
self.assertIsInstance(metadata_row.rows_cnt, int)
|
316
|
+
self.assertIsInstance(metadata_row.null_cnt, int)
|
317
|
+
self.assertIsInstance(metadata_row.origin_size, int)
|
318
|
+
self.assertIsInstance(metadata_row.is_hidden, bool)
|
319
|
+
|
320
|
+
def test_multiple_tables_metadata(self):
|
321
|
+
"""Test metadata operations on multiple tables"""
|
322
|
+
tables = ["test_users", "test_products"]
|
323
|
+
|
324
|
+
for table in tables:
|
325
|
+
# Basic scan
|
326
|
+
result = self.client.metadata.scan("test_metadata_db", table)
|
327
|
+
rows = result.fetchall()
|
328
|
+
self.assertGreater(len(rows), 0, f"Should have metadata for {table}")
|
329
|
+
|
330
|
+
# Brief stats
|
331
|
+
brief_stats = self.client.metadata.get_table_brief_stats("test_metadata_db", table)
|
332
|
+
self.assertIn(table, brief_stats, f"Should have brief stats for {table}")
|
333
|
+
|
334
|
+
# Detail stats
|
335
|
+
detail_stats = self.client.metadata.get_table_detail_stats("test_metadata_db", table)
|
336
|
+
self.assertIn(table, detail_stats, f"Should have detail stats for {table}")
|
337
|
+
|
338
|
+
def test_metadata_column_enum_completeness(self):
|
339
|
+
"""Test that all MetadataColumn enum values are available"""
|
340
|
+
# Get all available columns
|
341
|
+
rows = self.client.metadata.scan("test_metadata_db", "test_users", columns="*")
|
342
|
+
raw_row = rows[0]
|
343
|
+
|
344
|
+
# Check that all enum values correspond to actual columns
|
345
|
+
# Use raw SQLAlchemy row for _mapping access
|
346
|
+
if hasattr(raw_row, '_mapping'):
|
347
|
+
row_mapping = raw_row._mapping
|
348
|
+
else:
|
349
|
+
# For MetadataRow objects, convert to dict
|
350
|
+
row_mapping = {
|
351
|
+
'col_name': raw_row.col_name,
|
352
|
+
'object_name': raw_row.object_name,
|
353
|
+
'is_hidden': raw_row.is_hidden,
|
354
|
+
'obj_loc': raw_row.obj_loc,
|
355
|
+
'create_ts': raw_row.create_ts,
|
356
|
+
'delete_ts': raw_row.delete_ts,
|
357
|
+
'rows_cnt': raw_row.rows_cnt,
|
358
|
+
'null_cnt': raw_row.null_cnt,
|
359
|
+
'compress_size': raw_row.compress_size,
|
360
|
+
'origin_size': raw_row.origin_size,
|
361
|
+
'min': raw_row.min,
|
362
|
+
'max': raw_row.max,
|
363
|
+
'sum': raw_row.sum,
|
364
|
+
}
|
365
|
+
|
366
|
+
for column in MetadataColumn:
|
367
|
+
self.assertIn(column.value, row_mapping, f"Enum {column.name} should correspond to actual column {column.value}")
|
368
|
+
|
369
|
+
def test_error_handling(self):
|
370
|
+
"""Test error handling for invalid inputs"""
|
371
|
+
# Test with non-existent table - should raise QueryError
|
372
|
+
from matrixone.exceptions import QueryError
|
373
|
+
|
374
|
+
try:
|
375
|
+
self.client.metadata.scan("test_metadata_db", "non_existent_table")
|
376
|
+
self.fail("Expected QueryError for non-existent table")
|
377
|
+
except QueryError:
|
378
|
+
pass # Expected
|
379
|
+
|
380
|
+
# Test with non-existent database - should raise QueryError
|
381
|
+
try:
|
382
|
+
self.client.metadata.scan("non_existent_db", "test_users")
|
383
|
+
self.fail("Expected QueryError for non-existent database")
|
384
|
+
except QueryError:
|
385
|
+
pass # Expected
|
386
|
+
|
387
|
+
# Test with invalid column selection - might not raise error in some cases
|
388
|
+
# Just verify the call doesn't crash the system
|
389
|
+
try:
|
390
|
+
result = self.client.metadata.scan("test_metadata_db", "test_users", columns=["invalid_column"])
|
391
|
+
# If no error, just verify we get some result
|
392
|
+
self.assertIsNotNone(result)
|
393
|
+
except Exception as e:
|
394
|
+
# If error occurs, that's also acceptable
|
395
|
+
pass
|
396
|
+
|
397
|
+
|
398
|
+
class TestAsyncMetadataOnline(unittest.TestCase):
|
399
|
+
"""Test asynchronous metadata operations with real MatrixOne database"""
|
400
|
+
|
401
|
+
def setUp(self):
|
402
|
+
"""Set up async client for each test"""
|
403
|
+
self.connection_params = get_connection_kwargs()
|
404
|
+
# Filter out unsupported parameters for AsyncClient
|
405
|
+
filtered_params = {
|
406
|
+
'host': self.connection_params['host'],
|
407
|
+
'port': self.connection_params['port'],
|
408
|
+
'user': self.connection_params['user'],
|
409
|
+
'password': self.connection_params['password'],
|
410
|
+
'database': self.connection_params['database'],
|
411
|
+
}
|
412
|
+
self.async_client = AsyncClient()
|
413
|
+
|
414
|
+
def tearDown(self):
|
415
|
+
"""Clean up async client after each test"""
|
416
|
+
try:
|
417
|
+
asyncio.run(self.async_client.disconnect())
|
418
|
+
except Exception:
|
419
|
+
pass
|
420
|
+
|
421
|
+
def test_async_basic_metadata_scanning(self):
|
422
|
+
"""Test async basic metadata scanning"""
|
423
|
+
|
424
|
+
async def _test():
|
425
|
+
# Filter out unsupported parameters for AsyncClient
|
426
|
+
filtered_params = {
|
427
|
+
'host': self.connection_params['host'],
|
428
|
+
'port': self.connection_params['port'],
|
429
|
+
'user': self.connection_params['user'],
|
430
|
+
'password': self.connection_params['password'],
|
431
|
+
'database': self.connection_params['database'],
|
432
|
+
}
|
433
|
+
await self.async_client.connect(**filtered_params)
|
434
|
+
result = await self.async_client.metadata.scan("test_metadata_db", "test_users")
|
435
|
+
rows = result.fetchall()
|
436
|
+
|
437
|
+
self.assertGreater(len(rows), 0, "Should have async metadata entries")
|
438
|
+
|
439
|
+
# Verify row structure
|
440
|
+
for row in rows:
|
441
|
+
self.assertIn('col_name', row._mapping)
|
442
|
+
self.assertIn('rows_cnt', row._mapping)
|
443
|
+
|
444
|
+
asyncio.run(_test())
|
445
|
+
|
446
|
+
def test_async_structured_metadata(self):
|
447
|
+
"""Test async structured metadata"""
|
448
|
+
|
449
|
+
async def _test():
|
450
|
+
# Filter out unsupported parameters for AsyncClient
|
451
|
+
filtered_params = {
|
452
|
+
'host': self.connection_params['host'],
|
453
|
+
'port': self.connection_params['port'],
|
454
|
+
'user': self.connection_params['user'],
|
455
|
+
'password': self.connection_params['password'],
|
456
|
+
'database': self.connection_params['database'],
|
457
|
+
}
|
458
|
+
await self.async_client.connect(**filtered_params)
|
459
|
+
rows = await self.async_client.metadata.scan("test_metadata_db", "test_users", columns="*")
|
460
|
+
|
461
|
+
self.assertGreater(len(rows), 0, "Should have async structured metadata")
|
462
|
+
|
463
|
+
# Verify MetadataRow structure
|
464
|
+
for row in rows:
|
465
|
+
self.assertIsInstance(row, MetadataRow)
|
466
|
+
self.assertIsInstance(row.col_name, str)
|
467
|
+
self.assertIsInstance(row.rows_cnt, int)
|
468
|
+
|
469
|
+
asyncio.run(_test())
|
470
|
+
|
471
|
+
def test_async_column_selection(self):
|
472
|
+
"""Test async column selection"""
|
473
|
+
|
474
|
+
async def _test():
|
475
|
+
# Filter out unsupported parameters for AsyncClient
|
476
|
+
filtered_params = {
|
477
|
+
'host': self.connection_params['host'],
|
478
|
+
'port': self.connection_params['port'],
|
479
|
+
'user': self.connection_params['user'],
|
480
|
+
'password': self.connection_params['password'],
|
481
|
+
'database': self.connection_params['database'],
|
482
|
+
}
|
483
|
+
await self.async_client.connect(**filtered_params)
|
484
|
+
rows = await self.async_client.metadata.scan(
|
485
|
+
"test_metadata_db", "test_users", columns=[MetadataColumn.COL_NAME, MetadataColumn.ROWS_CNT]
|
486
|
+
)
|
487
|
+
|
488
|
+
self.assertGreater(len(rows), 0, "Should have async column-selected metadata")
|
489
|
+
|
490
|
+
# Verify selected columns
|
491
|
+
for row in rows:
|
492
|
+
self.assertIn('col_name', row)
|
493
|
+
self.assertIn('rows_cnt', row)
|
494
|
+
self.assertNotIn('null_cnt', row)
|
495
|
+
|
496
|
+
asyncio.run(_test())
|
497
|
+
|
498
|
+
def test_async_table_brief_stats(self):
|
499
|
+
"""Test async table brief statistics"""
|
500
|
+
|
501
|
+
async def _test():
|
502
|
+
# Filter out unsupported parameters for AsyncClient
|
503
|
+
filtered_params = {
|
504
|
+
'host': self.connection_params['host'],
|
505
|
+
'port': self.connection_params['port'],
|
506
|
+
'user': self.connection_params['user'],
|
507
|
+
'password': self.connection_params['password'],
|
508
|
+
'database': self.connection_params['database'],
|
509
|
+
}
|
510
|
+
await self.async_client.connect(**filtered_params)
|
511
|
+
brief_stats = await self.async_client.metadata.get_table_brief_stats("test_metadata_db", "test_users")
|
512
|
+
|
513
|
+
self.assertIn("test_users", brief_stats, "Should have async brief stats")
|
514
|
+
table_stats = brief_stats["test_users"]
|
515
|
+
|
516
|
+
# Verify required fields
|
517
|
+
required_fields = ['total_objects', 'row_cnt', 'null_cnt', 'original_size', 'compress_size']
|
518
|
+
for field in required_fields:
|
519
|
+
self.assertIn(field, table_stats, f"Should have {field}")
|
520
|
+
|
521
|
+
asyncio.run(_test())
|
522
|
+
|
523
|
+
def test_async_table_detail_stats(self):
|
524
|
+
"""Test async table detailed statistics"""
|
525
|
+
|
526
|
+
async def _test():
|
527
|
+
# Filter out unsupported parameters for AsyncClient
|
528
|
+
filtered_params = {
|
529
|
+
'host': self.connection_params['host'],
|
530
|
+
'port': self.connection_params['port'],
|
531
|
+
'user': self.connection_params['user'],
|
532
|
+
'password': self.connection_params['password'],
|
533
|
+
'database': self.connection_params['database'],
|
534
|
+
}
|
535
|
+
await self.async_client.connect(**filtered_params)
|
536
|
+
detail_stats = await self.async_client.metadata.get_table_detail_stats("test_metadata_db", "test_users")
|
537
|
+
|
538
|
+
self.assertIn("test_users", detail_stats, "Should have async detail stats")
|
539
|
+
table_details = detail_stats["test_users"]
|
540
|
+
|
541
|
+
self.assertGreater(len(table_details), 0, "Should have async detailed object stats")
|
542
|
+
|
543
|
+
# Verify object structure
|
544
|
+
for detail in table_details:
|
545
|
+
required_fields = ['object_name', 'create_ts', 'row_cnt', 'null_cnt', 'original_size', 'compress_size']
|
546
|
+
for field in required_fields:
|
547
|
+
self.assertIn(field, detail, f"Should have {field}")
|
548
|
+
|
549
|
+
asyncio.run(_test())
|
550
|
+
|
551
|
+
def test_async_distinct_object_name(self):
|
552
|
+
"""Test async distinct object name functionality"""
|
553
|
+
|
554
|
+
async def _test():
|
555
|
+
# Filter out unsupported parameters for AsyncClient
|
556
|
+
filtered_params = {
|
557
|
+
'host': self.connection_params['host'],
|
558
|
+
'port': self.connection_params['port'],
|
559
|
+
'user': self.connection_params['user'],
|
560
|
+
'password': self.connection_params['password'],
|
561
|
+
'database': self.connection_params['database'],
|
562
|
+
}
|
563
|
+
await self.async_client.connect(**filtered_params)
|
564
|
+
rows = await self.async_client.metadata.scan("test_metadata_db", "test_users", distinct_object_name=True)
|
565
|
+
|
566
|
+
# Should return distinct object names (might be empty in some environments)
|
567
|
+
object_names = set()
|
568
|
+
for row in rows:
|
569
|
+
object_name = row._mapping['object_name']
|
570
|
+
object_names.add(object_name)
|
571
|
+
|
572
|
+
# Just verify the call doesn't fail, distinct object names might be empty
|
573
|
+
self.assertIsInstance(object_names, set)
|
574
|
+
|
575
|
+
asyncio.run(_test())
|
576
|
+
|
577
|
+
|
578
|
+
if __name__ == '__main__':
|
579
|
+
unittest.main()
|