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.
Files changed (122) hide show
  1. matrixone/__init__.py +155 -0
  2. matrixone/account.py +723 -0
  3. matrixone/async_client.py +3913 -0
  4. matrixone/async_metadata_manager.py +311 -0
  5. matrixone/async_orm.py +123 -0
  6. matrixone/async_vector_index_manager.py +633 -0
  7. matrixone/base_client.py +208 -0
  8. matrixone/client.py +4672 -0
  9. matrixone/config.py +452 -0
  10. matrixone/connection_hooks.py +286 -0
  11. matrixone/exceptions.py +89 -0
  12. matrixone/logger.py +782 -0
  13. matrixone/metadata.py +820 -0
  14. matrixone/moctl.py +219 -0
  15. matrixone/orm.py +2277 -0
  16. matrixone/pitr.py +646 -0
  17. matrixone/pubsub.py +771 -0
  18. matrixone/restore.py +411 -0
  19. matrixone/search_vector_index.py +1176 -0
  20. matrixone/snapshot.py +550 -0
  21. matrixone/sql_builder.py +844 -0
  22. matrixone/sqlalchemy_ext/__init__.py +161 -0
  23. matrixone/sqlalchemy_ext/adapters.py +163 -0
  24. matrixone/sqlalchemy_ext/dialect.py +534 -0
  25. matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
  26. matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
  27. matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
  28. matrixone/sqlalchemy_ext/ivf_config.py +252 -0
  29. matrixone/sqlalchemy_ext/table_builder.py +351 -0
  30. matrixone/sqlalchemy_ext/vector_index.py +1721 -0
  31. matrixone/sqlalchemy_ext/vector_type.py +948 -0
  32. matrixone/version.py +580 -0
  33. matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
  34. matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
  35. matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
  36. matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
  37. matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
  38. matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
  39. tests/__init__.py +19 -0
  40. tests/offline/__init__.py +20 -0
  41. tests/offline/conftest.py +77 -0
  42. tests/offline/test_account.py +703 -0
  43. tests/offline/test_async_client_query_comprehensive.py +1218 -0
  44. tests/offline/test_basic.py +54 -0
  45. tests/offline/test_case_sensitivity.py +227 -0
  46. tests/offline/test_connection_hooks_offline.py +287 -0
  47. tests/offline/test_dialect_schema_handling.py +609 -0
  48. tests/offline/test_explain_methods.py +346 -0
  49. tests/offline/test_filter_logical_in.py +237 -0
  50. tests/offline/test_fulltext_search_comprehensive.py +795 -0
  51. tests/offline/test_ivf_config.py +249 -0
  52. tests/offline/test_join_methods.py +281 -0
  53. tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
  54. tests/offline/test_logical_in_method.py +237 -0
  55. tests/offline/test_matrixone_version_parsing.py +264 -0
  56. tests/offline/test_metadata_offline.py +557 -0
  57. tests/offline/test_moctl.py +300 -0
  58. tests/offline/test_moctl_simple.py +251 -0
  59. tests/offline/test_model_support_offline.py +359 -0
  60. tests/offline/test_model_support_simple.py +225 -0
  61. tests/offline/test_pinecone_filter_offline.py +377 -0
  62. tests/offline/test_pitr.py +585 -0
  63. tests/offline/test_pubsub.py +712 -0
  64. tests/offline/test_query_update.py +283 -0
  65. tests/offline/test_restore.py +445 -0
  66. tests/offline/test_snapshot_comprehensive.py +384 -0
  67. tests/offline/test_sql_escaping_edge_cases.py +551 -0
  68. tests/offline/test_sqlalchemy_integration.py +382 -0
  69. tests/offline/test_sqlalchemy_vector_integration.py +434 -0
  70. tests/offline/test_table_builder.py +198 -0
  71. tests/offline/test_unified_filter.py +398 -0
  72. tests/offline/test_unified_transaction.py +495 -0
  73. tests/offline/test_vector_index.py +238 -0
  74. tests/offline/test_vector_operations.py +688 -0
  75. tests/offline/test_vector_type.py +174 -0
  76. tests/offline/test_version_core.py +328 -0
  77. tests/offline/test_version_management.py +372 -0
  78. tests/offline/test_version_standalone.py +652 -0
  79. tests/online/__init__.py +20 -0
  80. tests/online/conftest.py +216 -0
  81. tests/online/test_account_management.py +194 -0
  82. tests/online/test_advanced_features.py +344 -0
  83. tests/online/test_async_client_interfaces.py +330 -0
  84. tests/online/test_async_client_online.py +285 -0
  85. tests/online/test_async_model_insert_online.py +293 -0
  86. tests/online/test_async_orm_online.py +300 -0
  87. tests/online/test_async_simple_query_online.py +802 -0
  88. tests/online/test_async_transaction_simple_query.py +300 -0
  89. tests/online/test_basic_connection.py +130 -0
  90. tests/online/test_client_online.py +238 -0
  91. tests/online/test_config.py +90 -0
  92. tests/online/test_config_validation.py +123 -0
  93. tests/online/test_connection_hooks_new_online.py +217 -0
  94. tests/online/test_dialect_schema_handling_online.py +331 -0
  95. tests/online/test_filter_logical_in_online.py +374 -0
  96. tests/online/test_fulltext_comprehensive.py +1773 -0
  97. tests/online/test_fulltext_label_online.py +433 -0
  98. tests/online/test_fulltext_search_online.py +842 -0
  99. tests/online/test_ivf_stats_online.py +506 -0
  100. tests/online/test_logger_integration.py +311 -0
  101. tests/online/test_matrixone_query_orm.py +540 -0
  102. tests/online/test_metadata_online.py +579 -0
  103. tests/online/test_model_insert_online.py +255 -0
  104. tests/online/test_mysql_driver_validation.py +213 -0
  105. tests/online/test_orm_advanced_features.py +2022 -0
  106. tests/online/test_orm_cte_integration.py +269 -0
  107. tests/online/test_orm_online.py +270 -0
  108. tests/online/test_pinecone_filter.py +708 -0
  109. tests/online/test_pubsub_operations.py +352 -0
  110. tests/online/test_query_methods.py +225 -0
  111. tests/online/test_query_update_online.py +433 -0
  112. tests/online/test_search_vector_index.py +557 -0
  113. tests/online/test_simple_fulltext_online.py +915 -0
  114. tests/online/test_snapshot_comprehensive.py +998 -0
  115. tests/online/test_sqlalchemy_engine_integration.py +336 -0
  116. tests/online/test_sqlalchemy_integration.py +425 -0
  117. tests/online/test_transaction_contexts.py +1219 -0
  118. tests/online/test_transaction_insert_methods.py +356 -0
  119. tests/online/test_transaction_query_methods.py +288 -0
  120. tests/online/test_unified_filter_online.py +529 -0
  121. tests/online/test_vector_comprehensive.py +706 -0
  122. 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()