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
matrixone/metadata.py ADDED
@@ -0,0 +1,820 @@
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
+ MatrixOne Metadata Operations
17
+
18
+ This module provides metadata scanning capabilities for MatrixOne tables,
19
+ allowing users to analyze table statistics, column information, and data distribution.
20
+ """
21
+
22
+ from typing import Optional, List, Dict, Any, Union
23
+ from sqlalchemy.engine import Result
24
+ from dataclasses import dataclass
25
+ from enum import Enum
26
+
27
+
28
+ class MetadataColumn(Enum):
29
+ """Enumeration of available metadata columns"""
30
+
31
+ COL_NAME = "col_name"
32
+ OBJECT_NAME = "object_name"
33
+ IS_HIDDEN = "is_hidden"
34
+ OBJ_LOC = "obj_loc"
35
+ CREATE_TS = "create_ts"
36
+ DELETE_TS = "delete_ts"
37
+ ROWS_CNT = "rows_cnt"
38
+ NULL_CNT = "null_cnt"
39
+ COMPRESS_SIZE = "compress_size"
40
+ ORIGIN_SIZE = "origin_size"
41
+ MIN = "min"
42
+ MAX = "max"
43
+ SUM = "sum"
44
+
45
+
46
+ @dataclass
47
+ class MetadataRow:
48
+ """Structured representation of a metadata scan row"""
49
+
50
+ col_name: str
51
+ object_name: str
52
+ is_hidden: bool
53
+ obj_loc: str
54
+ create_ts: str
55
+ delete_ts: str
56
+ rows_cnt: int
57
+ null_cnt: int
58
+ compress_size: int
59
+ origin_size: int
60
+ min: Optional[Any] = None
61
+ max: Optional[Any] = None
62
+ sum: Optional[Any] = None
63
+
64
+ @classmethod
65
+ def from_sqlalchemy_row(cls, row) -> "MetadataRow":
66
+ """Create MetadataRow from SQLAlchemy Row object"""
67
+ return cls(
68
+ col_name=row._mapping['col_name'],
69
+ object_name=row._mapping['object_name'],
70
+ is_hidden=row._mapping['is_hidden'] == 'true',
71
+ obj_loc=row._mapping['obj_loc'],
72
+ create_ts=row._mapping['create_ts'],
73
+ delete_ts=row._mapping['delete_ts'],
74
+ rows_cnt=row._mapping['rows_cnt'],
75
+ null_cnt=row._mapping['null_cnt'],
76
+ compress_size=row._mapping['compress_size'],
77
+ origin_size=row._mapping['origin_size'],
78
+ min=row._mapping.get('min'),
79
+ max=row._mapping.get('max'),
80
+ sum=row._mapping.get('sum'),
81
+ )
82
+
83
+
84
+ class BaseMetadataManager:
85
+ """
86
+ Base metadata manager with shared SQL building logic.
87
+ """
88
+
89
+ def _build_metadata_scan_sql(
90
+ self,
91
+ dbname: str,
92
+ tablename: str,
93
+ is_tombstone: Optional[bool] = None,
94
+ indexname: Optional[str] = None,
95
+ distinct_object_name: Optional[bool] = None,
96
+ ) -> str:
97
+ """
98
+ Build metadata_scan SQL query.
99
+
100
+ Args:
101
+
102
+ dbname: Database name
103
+ tablename: Table name
104
+ is_tombstone: Optional tombstone flag (True/False)
105
+ indexname: Optional index name
106
+
107
+ Returns:
108
+
109
+ SQL query string
110
+ """
111
+ # Build the metadata_scan SQL query based on MatrixOne syntax
112
+ if is_tombstone and indexname:
113
+ # Format: "db_name.table_name.?index_name.#"
114
+ table_ref = f"{dbname}.{tablename}.?{indexname}.#"
115
+ elif is_tombstone:
116
+ # Format: "db_name.table_name.#"
117
+ table_ref = f"{dbname}.{tablename}.#"
118
+ elif indexname:
119
+ # Format: "db_name.table_name.?index_name"
120
+ table_ref = f"{dbname}.{tablename}.?{indexname}"
121
+ else:
122
+ # Format: "db_name.table_name"
123
+ table_ref = f"{dbname}.{tablename}"
124
+
125
+ # Column parameter - always use '*' for metadata_scan
126
+ column_param = "'*'"
127
+
128
+ # Build the SQL query
129
+ if distinct_object_name:
130
+ return f"SELECT DISTINCT(object_name) as object_name, * FROM metadata_scan('{table_ref}', {column_param}) g"
131
+ else:
132
+ return f"SELECT * FROM metadata_scan('{table_ref}', {column_param}) g"
133
+
134
+ def _process_scan_result(
135
+ self, result: Result, columns: Optional[Union[List[Union[MetadataColumn, str]], str]] = None
136
+ ) -> Union[Result, List[MetadataRow], List[Dict[str, Any]]]:
137
+ """
138
+ Process scan result based on columns parameter.
139
+
140
+ Args:
141
+
142
+ result: SQLAlchemy Result object
143
+ columns: Optional list of columns to return
144
+
145
+ Returns:
146
+
147
+ Processed result based on columns parameter
148
+ """
149
+ # If columns are specified, return structured results
150
+ if columns is not None:
151
+ if columns == "*" or (isinstance(columns, list) and len(columns) == 0):
152
+ # Return all columns as structured data
153
+ return [MetadataRow.from_sqlalchemy_row(row) for row in result.fetchall()]
154
+ else:
155
+ # Return only specified columns as structured data
156
+ rows = []
157
+ for row in result.fetchall():
158
+ metadata_row = MetadataRow.from_sqlalchemy_row(row)
159
+ # Filter to only requested columns
160
+ filtered_row = {}
161
+ for col in columns:
162
+ col_name = col.value if isinstance(col, MetadataColumn) else col
163
+ if hasattr(metadata_row, col_name):
164
+ filtered_row[col_name] = getattr(metadata_row, col_name)
165
+ rows.append(filtered_row)
166
+ return rows
167
+
168
+ # Return raw SQLAlchemy Result
169
+ return result
170
+
171
+ def _format_size(self, size_bytes: int) -> str:
172
+ """
173
+ Format size in bytes to human-readable format.
174
+
175
+ Args:
176
+
177
+ size_bytes: Size in bytes
178
+
179
+ Returns:
180
+
181
+ Formatted size string (e.g., "1.5 MB", "500 KB")
182
+ """
183
+ if size_bytes == 0:
184
+ return "0 B"
185
+
186
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
187
+ unit_index = 0
188
+ size = float(size_bytes)
189
+
190
+ while size >= 1024 and unit_index < len(units) - 1:
191
+ size /= 1024
192
+ unit_index += 1
193
+
194
+ # Format to 2 decimal places maximum
195
+ if size == int(size):
196
+ return f"{int(size)} {units[unit_index]}"
197
+ else:
198
+ return f"{size:.2f} {units[unit_index]}".rstrip('0').rstrip('.')
199
+
200
+ def _execute_sql(self, sql: str):
201
+ """
202
+ Execute SQL query. Override in subclasses to use different executors.
203
+
204
+ Args:
205
+
206
+ sql: SQL query to execute
207
+
208
+ Returns:
209
+
210
+ SQLAlchemy Result object
211
+ """
212
+ return self.client.execute(sql)
213
+
214
+ async def _execute_sql_async(self, sql: str):
215
+ """
216
+ Execute SQL query asynchronously. Override in subclasses to use different executors.
217
+
218
+ Args:
219
+
220
+ sql: SQL query to execute
221
+
222
+ Returns:
223
+
224
+ SQLAlchemy Result object
225
+ """
226
+ return await self.client.execute(sql)
227
+
228
+ def _get_table_brief_stats_logic(
229
+ self,
230
+ dbname: str,
231
+ tablename: str,
232
+ is_tombstone: Optional[bool] = None,
233
+ indexname: Optional[str] = None,
234
+ include_tombstone: bool = False,
235
+ include_indexes: Optional[List[str]] = None,
236
+ execute_func=None,
237
+ ) -> Dict[str, Dict[str, Any]]:
238
+ """
239
+ Common logic for get_table_brief_stats. Used by both sync and async versions.
240
+
241
+ Args:
242
+
243
+ dbname: Database name
244
+ tablename: Table name
245
+ is_tombstone: Optional tombstone flag (True/False)
246
+ indexname: Optional index name
247
+ include_tombstone: Whether to include tombstone statistics
248
+ include_indexes: List of index names to include
249
+ execute_func: Function to execute SQL (sync or async)
250
+
251
+ Returns:
252
+
253
+ Dictionary with brief statistics for table, tombstone, and indexes
254
+ """
255
+ result = {}
256
+
257
+ # Get table statistics
258
+ table_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone, indexname)
259
+ table_result = execute_func(table_sql)
260
+ table_rows = table_result.fetchall()
261
+
262
+ if table_rows:
263
+ # Aggregate table statistics
264
+ total_objects = len(set(row._mapping['object_name'] for row in table_rows))
265
+ total_original_size = sum(row._mapping.get('origin_size', 0) for row in table_rows)
266
+ total_compress_size = sum(row._mapping.get('compress_size', 0) for row in table_rows)
267
+ total_row_cnt = sum(row._mapping.get('rows_cnt', 0) for row in table_rows)
268
+ total_null_cnt = sum(row._mapping.get('null_cnt', 0) for row in table_rows)
269
+
270
+ result[tablename] = {
271
+ "total_objects": total_objects,
272
+ "original_size": self._format_size(total_original_size),
273
+ "compress_size": self._format_size(total_compress_size),
274
+ "row_cnt": total_row_cnt,
275
+ "null_cnt": total_null_cnt,
276
+ }
277
+
278
+ # Get tombstone statistics if requested
279
+ if include_tombstone:
280
+ tombstone_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone=True)
281
+ tombstone_result = execute_func(tombstone_sql)
282
+ tombstone_rows = tombstone_result.fetchall()
283
+
284
+ if tombstone_rows:
285
+ total_objects = len(set(row._mapping['object_name'] for row in tombstone_rows))
286
+ total_original_size = sum(row._mapping.get('origin_size', 0) for row in tombstone_rows)
287
+ total_compress_size = sum(row._mapping.get('compress_size', 0) for row in tombstone_rows)
288
+ total_row_cnt = sum(row._mapping.get('rows_cnt', 0) for row in tombstone_rows)
289
+ total_null_cnt = sum(row._mapping.get('null_cnt', 0) for row in tombstone_rows)
290
+
291
+ result["tombstone"] = {
292
+ "total_objects": total_objects,
293
+ "original_size": self._format_size(total_original_size),
294
+ "compress_size": self._format_size(total_compress_size),
295
+ "row_cnt": total_row_cnt,
296
+ "null_cnt": total_null_cnt,
297
+ }
298
+
299
+ # Get index statistics if requested
300
+ if include_indexes:
301
+ for index_name in include_indexes:
302
+ index_sql = self._build_metadata_scan_sql(dbname, tablename, indexname=index_name)
303
+ index_result = execute_func(index_sql)
304
+ index_rows = index_result.fetchall()
305
+
306
+ if index_rows:
307
+ total_objects = len(set(row._mapping['object_name'] for row in index_rows))
308
+ total_original_size = sum(row._mapping.get('origin_size', 0) for row in index_rows)
309
+ total_compress_size = sum(row._mapping.get('compress_size', 0) for row in index_rows)
310
+ total_row_cnt = sum(row._mapping.get('rows_cnt', 0) for row in index_rows)
311
+ total_null_cnt = sum(row._mapping.get('null_cnt', 0) for row in index_rows)
312
+
313
+ result[index_name] = {
314
+ "total_objects": total_objects,
315
+ "original_size": self._format_size(total_original_size),
316
+ "compress_size": self._format_size(total_compress_size),
317
+ "row_cnt": total_row_cnt,
318
+ "null_cnt": total_null_cnt,
319
+ }
320
+
321
+ return result
322
+
323
+ async def _get_table_brief_stats_logic_async(
324
+ self,
325
+ dbname: str,
326
+ tablename: str,
327
+ is_tombstone: Optional[bool] = None,
328
+ indexname: Optional[str] = None,
329
+ include_tombstone: bool = False,
330
+ include_indexes: Optional[List[str]] = None,
331
+ execute_func=None,
332
+ ) -> Dict[str, Dict[str, Any]]:
333
+ """
334
+ Common async logic for get_table_brief_stats. Used by async versions.
335
+
336
+ Args:
337
+
338
+ dbname: Database name
339
+ tablename: Table name
340
+ is_tombstone: Optional tombstone flag (True/False)
341
+ indexname: Optional index name
342
+ include_tombstone: Whether to include tombstone statistics
343
+ include_indexes: List of index names to include
344
+ execute_func: Async function to execute SQL
345
+
346
+ Returns:
347
+
348
+ Dictionary with brief statistics for table, tombstone, and indexes
349
+ """
350
+ result = {}
351
+
352
+ # Get table statistics
353
+ table_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone, indexname)
354
+ table_result = await execute_func(table_sql)
355
+ table_rows = table_result.fetchall()
356
+
357
+ if table_rows:
358
+ # Aggregate table statistics
359
+ total_objects = len(set(row._mapping['object_name'] for row in table_rows))
360
+ total_original_size = sum(row._mapping.get('origin_size', 0) for row in table_rows)
361
+ total_compress_size = sum(row._mapping.get('compress_size', 0) for row in table_rows)
362
+ total_row_cnt = sum(row._mapping.get('rows_cnt', 0) for row in table_rows)
363
+ total_null_cnt = sum(row._mapping.get('null_cnt', 0) for row in table_rows)
364
+
365
+ result[tablename] = {
366
+ "total_objects": total_objects,
367
+ "original_size": self._format_size(total_original_size),
368
+ "compress_size": self._format_size(total_compress_size),
369
+ "row_cnt": total_row_cnt,
370
+ "null_cnt": total_null_cnt,
371
+ }
372
+
373
+ # Get tombstone statistics if requested
374
+ if include_tombstone:
375
+ tombstone_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone=True)
376
+ tombstone_result = await execute_func(tombstone_sql)
377
+ tombstone_rows = tombstone_result.fetchall()
378
+
379
+ if tombstone_rows:
380
+ total_objects = len(set(row._mapping['object_name'] for row in tombstone_rows))
381
+ total_original_size = sum(row._mapping.get('origin_size', 0) for row in tombstone_rows)
382
+ total_compress_size = sum(row._mapping.get('compress_size', 0) for row in tombstone_rows)
383
+ total_row_cnt = sum(row._mapping.get('rows_cnt', 0) for row in tombstone_rows)
384
+ total_null_cnt = sum(row._mapping.get('null_cnt', 0) for row in tombstone_rows)
385
+
386
+ result["tombstone"] = {
387
+ "total_objects": total_objects,
388
+ "original_size": self._format_size(total_original_size),
389
+ "compress_size": self._format_size(total_compress_size),
390
+ "row_cnt": total_row_cnt,
391
+ "null_cnt": total_null_cnt,
392
+ }
393
+
394
+ # Get index statistics if requested
395
+ if include_indexes:
396
+ for index_name in include_indexes:
397
+ index_sql = self._build_metadata_scan_sql(dbname, tablename, indexname=index_name)
398
+ index_result = await execute_func(index_sql)
399
+ index_rows = index_result.fetchall()
400
+
401
+ if index_rows:
402
+ total_objects = len(set(row._mapping['object_name'] for row in index_rows))
403
+ total_original_size = sum(row._mapping.get('origin_size', 0) for row in index_rows)
404
+ total_compress_size = sum(row._mapping.get('compress_size', 0) for row in index_rows)
405
+ total_row_cnt = sum(row._mapping.get('rows_cnt', 0) for row in index_rows)
406
+ total_null_cnt = sum(row._mapping.get('null_cnt', 0) for row in index_rows)
407
+
408
+ result[index_name] = {
409
+ "total_objects": total_objects,
410
+ "original_size": self._format_size(total_original_size),
411
+ "compress_size": self._format_size(total_compress_size),
412
+ "row_cnt": total_row_cnt,
413
+ "null_cnt": total_null_cnt,
414
+ }
415
+
416
+ return result
417
+
418
+ async def _get_table_detail_stats_logic_async(
419
+ self,
420
+ dbname: str,
421
+ tablename: str,
422
+ is_tombstone: Optional[bool] = None,
423
+ indexname: Optional[str] = None,
424
+ include_tombstone: bool = False,
425
+ include_indexes: Optional[List[str]] = None,
426
+ execute_func=None,
427
+ ) -> Dict[str, List[Dict[str, Any]]]:
428
+ """
429
+ Common async logic for get_table_detail_stats. Used by async versions.
430
+
431
+ Args:
432
+
433
+ dbname: Database name
434
+ tablename: Table name
435
+ is_tombstone: Optional tombstone flag (True/False)
436
+ indexname: Optional index name
437
+ include_tombstone: Whether to include tombstone statistics
438
+ include_indexes: List of index names to include
439
+ execute_func: Async function to execute SQL
440
+
441
+ Returns:
442
+
443
+ Dictionary with detailed statistics for table, tombstone, and indexes
444
+ """
445
+ result = {}
446
+
447
+ # Get table statistics
448
+ table_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone, indexname)
449
+ table_result = await execute_func(table_sql)
450
+ table_rows = table_result.fetchall()
451
+
452
+ if table_rows:
453
+ # Convert to detailed format
454
+ table_details = []
455
+ for row in table_rows:
456
+ table_details.append(
457
+ {
458
+ "object_name": row._mapping['object_name'],
459
+ "create_ts": row._mapping['create_ts'],
460
+ "delete_ts": row._mapping['delete_ts'],
461
+ "row_cnt": row._mapping.get('rows_cnt', 0),
462
+ "null_cnt": row._mapping.get('null_cnt', 0),
463
+ "original_size": self._format_size(row._mapping.get('origin_size', 0)),
464
+ "compress_size": self._format_size(row._mapping.get('compress_size', 0)),
465
+ }
466
+ )
467
+ result[tablename] = table_details
468
+
469
+ # Get tombstone statistics if requested
470
+ if include_tombstone:
471
+ tombstone_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone=True)
472
+ tombstone_result = await execute_func(tombstone_sql)
473
+ tombstone_rows = tombstone_result.fetchall()
474
+
475
+ if tombstone_rows:
476
+ tombstone_details = []
477
+ for row in tombstone_rows:
478
+ tombstone_details.append(
479
+ {
480
+ "object_name": row._mapping['object_name'],
481
+ "create_ts": row._mapping['create_ts'],
482
+ "delete_ts": row._mapping['delete_ts'],
483
+ "row_cnt": row._mapping.get('rows_cnt', 0),
484
+ "null_cnt": row._mapping.get('null_cnt', 0),
485
+ "original_size": self._format_size(row._mapping.get('origin_size', 0)),
486
+ "compress_size": self._format_size(row._mapping.get('compress_size', 0)),
487
+ }
488
+ )
489
+ result["tombstone"] = tombstone_details
490
+
491
+ # Get index statistics if requested
492
+ if include_indexes:
493
+ for index_name in include_indexes:
494
+ index_sql = self._build_metadata_scan_sql(dbname, tablename, indexname=index_name)
495
+ index_result = await execute_func(index_sql)
496
+ index_rows = index_result.fetchall()
497
+
498
+ if index_rows:
499
+ index_details = []
500
+ for row in index_rows:
501
+ index_details.append(
502
+ {
503
+ "object_name": row._mapping['object_name'],
504
+ "create_ts": row._mapping['create_ts'],
505
+ "delete_ts": row._mapping['delete_ts'],
506
+ "row_cnt": row._mapping.get('rows_cnt', 0),
507
+ "null_cnt": row._mapping.get('null_cnt', 0),
508
+ "original_size": self._format_size(row._mapping.get('origin_size', 0)),
509
+ "compress_size": self._format_size(row._mapping.get('compress_size', 0)),
510
+ }
511
+ )
512
+ result[index_name] = index_details
513
+
514
+ return result
515
+
516
+ def get_table_brief_stats(
517
+ self,
518
+ dbname: str,
519
+ tablename: str,
520
+ is_tombstone: Optional[bool] = None,
521
+ indexname: Optional[str] = None,
522
+ include_tombstone: bool = False,
523
+ include_indexes: Optional[List[str]] = None,
524
+ ) -> Dict[str, Dict[str, Any]]:
525
+ """
526
+ Get brief statistics for a table, tombstone, and indexes.
527
+
528
+ Args:
529
+
530
+ dbname: Database name
531
+ tablename: Table name
532
+ is_tombstone: Optional tombstone flag (True/False)
533
+ indexname: Optional index name
534
+ include_tombstone: Whether to include tombstone statistics
535
+ include_indexes: List of index names to include
536
+
537
+ Returns:
538
+
539
+ Dictionary with brief statistics for table, tombstone, and indexes
540
+ """
541
+ return self._get_table_brief_stats_logic(
542
+ dbname, tablename, is_tombstone, indexname, include_tombstone, include_indexes, self._execute_sql
543
+ )
544
+
545
+ def _get_table_detail_stats_logic(
546
+ self,
547
+ dbname: str,
548
+ tablename: str,
549
+ is_tombstone: Optional[bool] = None,
550
+ indexname: Optional[str] = None,
551
+ include_tombstone: bool = False,
552
+ include_indexes: Optional[List[str]] = None,
553
+ execute_func=None,
554
+ ) -> Dict[str, List[Dict[str, Any]]]:
555
+ """
556
+ Common logic for get_table_detail_stats. Used by both sync and async versions.
557
+
558
+ Args:
559
+
560
+ dbname: Database name
561
+ tablename: Table name
562
+ is_tombstone: Optional tombstone flag (True/False)
563
+ indexname: Optional index name
564
+ include_tombstone: Whether to include tombstone statistics
565
+ include_indexes: List of index names to include
566
+ execute_func: Function to execute SQL (sync or async)
567
+
568
+ Returns:
569
+
570
+ Dictionary with detailed statistics for table, tombstone, and indexes
571
+ """
572
+ result = {}
573
+
574
+ # Get table statistics
575
+ table_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone, indexname)
576
+ table_result = execute_func(table_sql)
577
+ table_rows = table_result.fetchall()
578
+
579
+ if table_rows:
580
+ # Convert to detailed format
581
+ table_details = []
582
+ for row in table_rows:
583
+ table_details.append(
584
+ {
585
+ "object_name": row._mapping['object_name'],
586
+ "create_ts": row._mapping['create_ts'],
587
+ "delete_ts": row._mapping['delete_ts'],
588
+ "row_cnt": row._mapping.get('rows_cnt', 0),
589
+ "null_cnt": row._mapping.get('null_cnt', 0),
590
+ "original_size": self._format_size(row._mapping.get('origin_size', 0)),
591
+ "compress_size": self._format_size(row._mapping.get('compress_size', 0)),
592
+ }
593
+ )
594
+ result[tablename] = table_details
595
+
596
+ # Get tombstone statistics if requested
597
+ if include_tombstone:
598
+ tombstone_sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone=True)
599
+ tombstone_result = execute_func(tombstone_sql)
600
+ tombstone_rows = tombstone_result.fetchall()
601
+
602
+ if tombstone_rows:
603
+ tombstone_details = []
604
+ for row in tombstone_rows:
605
+ tombstone_details.append(
606
+ {
607
+ "object_name": row._mapping['object_name'],
608
+ "create_ts": row._mapping['create_ts'],
609
+ "delete_ts": row._mapping['delete_ts'],
610
+ "row_cnt": row._mapping.get('rows_cnt', 0),
611
+ "null_cnt": row._mapping.get('null_cnt', 0),
612
+ "original_size": self._format_size(row._mapping.get('origin_size', 0)),
613
+ "compress_size": self._format_size(row._mapping.get('compress_size', 0)),
614
+ }
615
+ )
616
+ result["tombstone"] = tombstone_details
617
+
618
+ # Get index statistics if requested
619
+ if include_indexes:
620
+ for index_name in include_indexes:
621
+ index_sql = self._build_metadata_scan_sql(dbname, tablename, indexname=index_name)
622
+ index_result = execute_func(index_sql)
623
+ index_rows = index_result.fetchall()
624
+
625
+ if index_rows:
626
+ index_details = []
627
+ for row in index_rows:
628
+ index_details.append(
629
+ {
630
+ "object_name": row._mapping['object_name'],
631
+ "create_ts": row._mapping['create_ts'],
632
+ "delete_ts": row._mapping['delete_ts'],
633
+ "row_cnt": row._mapping.get('rows_cnt', 0),
634
+ "null_cnt": row._mapping.get('null_cnt', 0),
635
+ "original_size": self._format_size(row._mapping.get('origin_size', 0)),
636
+ "compress_size": self._format_size(row._mapping.get('compress_size', 0)),
637
+ }
638
+ )
639
+ result[index_name] = index_details
640
+
641
+ return result
642
+
643
+ def get_table_detail_stats(
644
+ self,
645
+ dbname: str,
646
+ tablename: str,
647
+ is_tombstone: Optional[bool] = None,
648
+ indexname: Optional[str] = None,
649
+ include_tombstone: bool = False,
650
+ include_indexes: Optional[List[str]] = None,
651
+ ) -> Dict[str, List[Dict[str, Any]]]:
652
+ """
653
+ Get detailed statistics for a table, tombstone, and indexes.
654
+
655
+ Args:
656
+
657
+ dbname: Database name
658
+ tablename: Table name
659
+ is_tombstone: Optional tombstone flag (True/False)
660
+ indexname: Optional index name
661
+ include_tombstone: Whether to include tombstone statistics
662
+ include_indexes: List of index names to include
663
+
664
+ Returns:
665
+
666
+ Dictionary with detailed statistics for table, tombstone, and indexes
667
+ """
668
+ return self._get_table_detail_stats_logic(
669
+ dbname, tablename, is_tombstone, indexname, include_tombstone, include_indexes, self._execute_sql
670
+ )
671
+
672
+
673
+ class MetadataManager(BaseMetadataManager):
674
+ """
675
+ Metadata manager for MatrixOne table metadata operations.
676
+
677
+ Provides methods to scan table metadata including column statistics,
678
+ row counts, null counts, and data distribution information.
679
+ """
680
+
681
+ def __init__(self, client):
682
+ """
683
+ Initialize metadata manager.
684
+
685
+ Args:
686
+
687
+ client: MatrixOne client instance (Client or AsyncClient)
688
+ """
689
+ self.client = client
690
+
691
+ def scan(
692
+ self,
693
+ dbname: str,
694
+ tablename: str,
695
+ is_tombstone: Optional[bool] = None,
696
+ indexname: Optional[str] = None,
697
+ columns: Optional[Union[List[Union[MetadataColumn, str]], str]] = None,
698
+ distinct_object_name: Optional[bool] = None,
699
+ ) -> Union[Result, List[MetadataRow]]:
700
+ """
701
+ Scan table metadata using metadata_scan function.
702
+
703
+ Args:
704
+
705
+ dbname: Database name
706
+ tablename: Table name
707
+ is_tombstone: Optional tombstone flag (True/False)
708
+ indexname: Optional index name
709
+ columns: Optional list of columns to return. Can be MetadataColumn enum values or strings.
710
+ If None, returns all columns as SQLAlchemy Result. If specified, returns List[MetadataRow].
711
+ distinct_object_name: Optional flag to return distinct object names only.
712
+
713
+ Returns:
714
+
715
+ If columns is None: SQLAlchemy Result object containing metadata scan results
716
+ If columns is specified: List[MetadataRow] containing structured metadata
717
+
718
+ Example:
719
+
720
+ ```python
721
+ # Scan all columns of a table (returns SQLAlchemy Result)
722
+ result = client.metadata.scan("test_db", "users")
723
+
724
+ # Scan specific column
725
+ result = client.metadata.scan("test_db", "users", indexname="id")
726
+
727
+ # Scan with tombstone filter
728
+ result = client.metadata.scan("test_db", "users", is_tombstone=False)
729
+
730
+ # Scan tombstone objects
731
+ result = client.metadata.scan("test_db", "users", is_tombstone=True)
732
+
733
+ # Scan specific index
734
+ result = client.metadata.scan("test_db", "users", indexname="idx_name")
735
+
736
+ # Get structured results with specific columns
737
+ rows = client.metadata.scan("test_db", "users",
738
+ columns=[MetadataColumn.COL_NAME, MetadataColumn.ROWS_CNT])
739
+ for row in rows:
740
+ print(f"Column: {row.col_name}, Rows: {row.rows_cnt}")
741
+
742
+ # Get all structured results
743
+ rows = client.metadata.scan("test_db", "users", columns="*")
744
+ ```
745
+ """
746
+ # Build SQL query
747
+ sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone, indexname, distinct_object_name)
748
+
749
+ # Execute the query
750
+ result = self._execute_sql(sql)
751
+
752
+ # Process result based on columns parameter
753
+ return self._process_scan_result(result, columns)
754
+
755
+
756
+ class TransactionMetadataManager(BaseMetadataManager):
757
+ """
758
+ Transaction metadata manager for MatrixOne table metadata operations within transactions.
759
+
760
+ Provides methods to scan table metadata including column statistics,
761
+ row counts, null counts, and data distribution information within a transaction context.
762
+ """
763
+
764
+ def __init__(self, client, transaction_wrapper):
765
+ """
766
+ Initialize transaction metadata manager.
767
+
768
+ Args:
769
+
770
+ client: MatrixOne client instance
771
+ transaction_wrapper: Transaction wrapper instance
772
+ """
773
+ self.client = client
774
+ self.transaction_wrapper = transaction_wrapper
775
+
776
+ def _execute_sql(self, sql: str):
777
+ """
778
+ Execute SQL query using transaction wrapper.
779
+
780
+ Args:
781
+
782
+ sql: SQL query to execute
783
+
784
+ Returns:
785
+
786
+ SQLAlchemy Result object
787
+ """
788
+ return self.transaction_wrapper.execute(sql)
789
+
790
+ def scan(
791
+ self,
792
+ dbname: str,
793
+ tablename: str,
794
+ is_tombstone: Optional[bool] = None,
795
+ indexname: Optional[str] = None,
796
+ columns: Optional[Union[List[Union[MetadataColumn, str]], str]] = None,
797
+ distinct_object_name: Optional[bool] = None,
798
+ ) -> Union[Result, List[MetadataRow]]:
799
+ """
800
+ Scan table metadata using metadata_scan function within transaction.
801
+
802
+ Args:
803
+
804
+ dbname: Database name
805
+ tablename: Table name
806
+ is_tombstone: Optional tombstone flag (True/False)
807
+ indexname: Optional index name
808
+
809
+ Returns:
810
+
811
+ SQLAlchemy Result object containing metadata scan results
812
+ """
813
+ # Build SQL query
814
+ sql = self._build_metadata_scan_sql(dbname, tablename, is_tombstone, indexname, distinct_object_name)
815
+
816
+ # Execute the query within transaction
817
+ result = self.transaction_wrapper.execute(sql)
818
+
819
+ # Process result based on columns parameter
820
+ return self._process_scan_result(result, columns)