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/logger.py ADDED
@@ -0,0 +1,782 @@
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 Logger Module
17
+
18
+ Provides logging functionality for MatrixOne Python SDK with support for:
19
+ 1. Default logger configuration
20
+ 2. Custom logger integration
21
+ 3. Structured logging
22
+ 4. Performance logging
23
+ 5. Error tracking
24
+ """
25
+
26
+ import logging
27
+ import sys
28
+ from typing import Optional
29
+
30
+
31
+ class MatrixOneLogger:
32
+ """
33
+ MatrixOne Logger class that provides structured logging for the SDK
34
+
35
+ Features:
36
+ - Default logger configuration
37
+ - Custom logger integration
38
+ - Performance logging
39
+ - Error tracking
40
+ - Structured log messages
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ logger: Optional[logging.Logger] = None,
46
+ level: int = logging.INFO,
47
+ format_string: Optional[str] = None,
48
+ sql_log_mode: str = "auto",
49
+ slow_query_threshold: float = 1.0,
50
+ max_sql_display_length: int = 500,
51
+ ):
52
+ """
53
+ Initialize MatrixOne logger
54
+
55
+ Args::
56
+
57
+ logger: Custom logger instance. If None, creates a default logger
58
+ level: Logging level (default: INFO)
59
+ format_string: Custom format string for log messages
60
+ sql_log_mode: SQL logging mode ('off', 'auto', 'simple', 'full')
61
+ - 'off': No SQL logging
62
+ - 'auto': Smart logging - short SQL shown fully, long SQL summarized (default)
63
+ - 'simple': Show operation summary only (e.g., "INSERT INTO table (5 rows)")
64
+ - 'full': Show complete SQL regardless of length
65
+ slow_query_threshold: Threshold in seconds for slow query warnings (default: 1.0)
66
+ max_sql_display_length: Maximum SQL length in auto mode before summarizing (default: 500)
67
+ """
68
+ # Validate sql_log_mode
69
+ valid_modes = ['off', 'auto', 'simple', 'full']
70
+ if sql_log_mode not in valid_modes:
71
+ raise ValueError(f"Invalid sql_log_mode '{sql_log_mode}'. Must be one of {valid_modes}")
72
+
73
+ self.sql_log_mode = sql_log_mode
74
+ self.slow_query_threshold = slow_query_threshold
75
+ self.max_sql_display_length = max_sql_display_length
76
+
77
+ if logger is not None:
78
+ # Use provided logger
79
+ self.logger = logger
80
+ self._is_custom = True
81
+ else:
82
+ # Create default logger
83
+ self.logger = self._create_default_logger(level, format_string)
84
+ self._is_custom = False
85
+
86
+ def _create_default_logger(self, level: int, format_string: Optional[str]) -> logging.Logger:
87
+ """Create default logger with standard configuration"""
88
+ logger = logging.getLogger("matrixone")
89
+
90
+ # Avoid adding multiple handlers
91
+ if logger.handlers:
92
+ return logger
93
+
94
+ # Set level
95
+ logger.setLevel(level)
96
+
97
+ # Create console handler
98
+ console_handler = logging.StreamHandler(sys.stdout)
99
+ console_handler.setLevel(level)
100
+
101
+ # Set format
102
+ if format_string is None:
103
+ format_string = "%(asctime)s|%(name)s|%(levelname)s|[%(filename)s:%(lineno)d]: %(message)s"
104
+
105
+ formatter = logging.Formatter(format_string)
106
+ console_handler.setFormatter(formatter)
107
+
108
+ # Add handler to logger
109
+ logger.addHandler(console_handler)
110
+
111
+ # Prevent propagation to root logger
112
+ logger.propagate = False
113
+
114
+ return logger
115
+
116
+ def debug(self, message: str, **kwargs):
117
+ """Log debug message"""
118
+ # Use findCaller to get the actual caller's file and line
119
+ import sys
120
+
121
+ frame = sys._getframe(1)
122
+ filename = frame.f_code.co_filename
123
+ lineno = frame.f_lineno
124
+ # Create a new record with the caller's info
125
+ record = self.logger.makeRecord(
126
+ self.logger.name,
127
+ logging.DEBUG,
128
+ filename,
129
+ lineno,
130
+ self._format_message(message, **kwargs),
131
+ (),
132
+ None,
133
+ )
134
+ self.logger.handle(record)
135
+
136
+ def info(self, message: str, **kwargs):
137
+ """Log info message"""
138
+ # Use findCaller to get the actual caller's file and line
139
+ import sys
140
+
141
+ frame = sys._getframe(1)
142
+ filename = frame.f_code.co_filename
143
+ lineno = frame.f_lineno
144
+ # Create a new record with the caller's info
145
+ record = self.logger.makeRecord(
146
+ self.logger.name,
147
+ logging.INFO,
148
+ filename,
149
+ lineno,
150
+ self._format_message(message, **kwargs),
151
+ (),
152
+ None,
153
+ )
154
+ self.logger.handle(record)
155
+
156
+ def warning(self, message: str, **kwargs):
157
+ """Log warning message"""
158
+ # Use findCaller to get the actual caller's file and line
159
+ import sys
160
+
161
+ frame = sys._getframe(1)
162
+ filename = frame.f_code.co_filename
163
+ lineno = frame.f_lineno
164
+ # Create a new record with the caller's info
165
+ record = self.logger.makeRecord(
166
+ self.logger.name,
167
+ logging.WARNING,
168
+ filename,
169
+ lineno,
170
+ self._format_message(message, **kwargs),
171
+ (),
172
+ None,
173
+ )
174
+ self.logger.handle(record)
175
+
176
+ def error(self, message: str, **kwargs):
177
+ """Log error message"""
178
+ # Use findCaller to get the actual caller's file and line
179
+ import sys
180
+
181
+ frame = sys._getframe(1)
182
+ filename = frame.f_code.co_filename
183
+ lineno = frame.f_lineno
184
+ # Create a new record with the caller's info
185
+ record = self.logger.makeRecord(
186
+ self.logger.name,
187
+ logging.ERROR,
188
+ filename,
189
+ lineno,
190
+ self._format_message(message, **kwargs),
191
+ (),
192
+ None,
193
+ )
194
+ self.logger.handle(record)
195
+
196
+ def critical(self, message: str, **kwargs):
197
+ """Log critical message"""
198
+ # Use findCaller to get the actual caller's file and line
199
+ import sys
200
+
201
+ frame = sys._getframe(1)
202
+ filename = frame.f_code.co_filename
203
+ lineno = frame.f_lineno
204
+ # Create a new record with the caller's info
205
+ record = self.logger.makeRecord(
206
+ self.logger.name,
207
+ logging.CRITICAL,
208
+ filename,
209
+ lineno,
210
+ self._format_message(message, **kwargs),
211
+ (),
212
+ None,
213
+ )
214
+ self.logger.handle(record)
215
+
216
+ def _format_message(self, message: str, **kwargs) -> str:
217
+ """Format message with additional context"""
218
+ if not kwargs:
219
+ return message
220
+
221
+ # Add context information
222
+ context_parts = []
223
+ for key, value in kwargs.items():
224
+ context_parts.append(f"{key}={value}")
225
+
226
+ context_str = " | ".join(context_parts)
227
+ return f"{message} | {context_str}"
228
+
229
+ def log_connection(self, host: str, port: int, user: str, database: str, success: bool = True):
230
+ """Log connection events"""
231
+ status = "✓ Connected" if success else "✗ Connection failed"
232
+ # Use findCaller to get the actual caller's file and line
233
+ import sys
234
+
235
+ frame = sys._getframe(1)
236
+ filename = frame.f_code.co_filename
237
+ lineno = frame.f_lineno
238
+ # Create a new record with the caller's info
239
+ record = self.logger.makeRecord(
240
+ self.logger.name,
241
+ logging.INFO,
242
+ filename,
243
+ lineno,
244
+ self._format_message(f"{status} to MatrixOne", host=host, port=port, user=user, database=database),
245
+ (),
246
+ None,
247
+ )
248
+ self.logger.handle(record)
249
+
250
+ def log_disconnection(self, success: bool = True):
251
+ """Log disconnection events"""
252
+ status = "✓ Disconnected" if success else "✗ Disconnection failed"
253
+ # Use findCaller to get the actual caller's file and line
254
+ import sys
255
+
256
+ frame = sys._getframe(1)
257
+ filename = frame.f_code.co_filename
258
+ lineno = frame.f_lineno
259
+ # Create a new record with the caller's info
260
+ record = self.logger.makeRecord(
261
+ self.logger.name, logging.INFO, filename, lineno, f"{status} from MatrixOne", (), None
262
+ )
263
+ self.logger.handle(record)
264
+
265
+ def _extract_sql_summary(self, sql: str) -> str:
266
+ """
267
+ Extract operation summary from SQL query
268
+
269
+ Args::
270
+
271
+ sql: SQL query string
272
+
273
+ Returns::
274
+
275
+ Summary string describing the operation
276
+ """
277
+ sql_stripped = sql.strip()
278
+ sql_upper = sql_stripped.upper()
279
+
280
+ # Extract operation type and key details
281
+ import re
282
+
283
+ # SELECT operations
284
+ if sql_upper.startswith('SELECT'):
285
+ match = re.search(r'FROM\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
286
+ table = match.group(1).strip('`"') if match else '?'
287
+ return f"SELECT FROM {table}"
288
+
289
+ # INSERT operations
290
+ elif sql_upper.startswith('INSERT'):
291
+ match = re.search(r'INSERT\s+INTO\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
292
+ table = match.group(1).strip('`"') if match else '?'
293
+ # Count rows in batch insert by counting VALUES groups
294
+ if 'VALUES' in sql_upper:
295
+ # Count the number of value groups by counting '),' patterns
296
+ values_count = sql_stripped.count('),(') + 1
297
+ if values_count > 1:
298
+ return f"BATCH INSERT INTO {table} ({values_count} rows)"
299
+ return f"INSERT INTO {table}"
300
+
301
+ # UPDATE operations
302
+ elif sql_upper.startswith('UPDATE'):
303
+ match = re.search(r'UPDATE\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
304
+ table = match.group(1).strip('`"') if match else '?'
305
+ return f"UPDATE {table}"
306
+
307
+ # DELETE operations
308
+ elif sql_upper.startswith('DELETE'):
309
+ match = re.search(r'DELETE\s+FROM\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
310
+ table = match.group(1).strip('`"') if match else '?'
311
+ return f"DELETE FROM {table}"
312
+
313
+ # CREATE operations
314
+ elif sql_upper.startswith('CREATE'):
315
+ if 'INDEX' in sql_upper:
316
+ match = re.search(r'ON\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
317
+ table = match.group(1).strip('`"') if match else '?'
318
+ return f"CREATE INDEX ON {table}"
319
+ elif 'TABLE' in sql_upper:
320
+ match = re.search(r'CREATE\s+TABLE\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
321
+ table = match.group(1).strip('`"') if match else '?'
322
+ return f"CREATE TABLE {table}"
323
+ return "CREATE"
324
+
325
+ # DROP operations
326
+ elif sql_upper.startswith('DROP'):
327
+ if 'TABLE' in sql_upper:
328
+ match = re.search(r'DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
329
+ table = match.group(1).strip('`"') if match else '?'
330
+ return f"DROP TABLE {table}"
331
+ elif 'INDEX' in sql_upper:
332
+ return "DROP INDEX"
333
+ return "DROP"
334
+
335
+ # SET operations
336
+ elif sql_upper.startswith('SET'):
337
+ return "SET VARIABLE"
338
+
339
+ # Other operations
340
+ else:
341
+ # Return first 50 characters as fallback
342
+ return sql_stripped[:50] + "..." if len(sql_stripped) > 50 else sql_stripped
343
+
344
+ def _format_sql_for_log(
345
+ self, sql: str, is_error: bool = False, is_slow: bool = False, override_mode: Optional[str] = None
346
+ ) -> str:
347
+ """
348
+ Format SQL query for logging based on mode and query characteristics
349
+
350
+ Args::
351
+
352
+ sql: SQL query string
353
+ is_error: Whether this is an error query
354
+ is_slow: Whether this is a slow query
355
+ override_mode: Optional mode override for this specific query
356
+
357
+ Returns::
358
+
359
+ Formatted SQL string for logging
360
+ """
361
+ # Use override mode if provided, otherwise use instance mode
362
+ effective_mode = override_mode if override_mode is not None else self.sql_log_mode
363
+
364
+ # Mode 'off': Don't log SQL
365
+ if effective_mode == 'off':
366
+ return ""
367
+
368
+ # Errors and slow queries always show complete SQL for debugging
369
+ if is_error or is_slow:
370
+ return sql.strip()
371
+
372
+ # Mode 'full': Always show complete SQL
373
+ if effective_mode == 'full':
374
+ return sql.strip()
375
+
376
+ # Mode 'simple': Only show operation summary
377
+ if effective_mode == 'simple':
378
+ return self._extract_sql_summary(sql)
379
+
380
+ # Mode 'auto': Smart formatting based on length
381
+ if effective_mode == 'auto':
382
+ sql_stripped = sql.strip()
383
+ if len(sql_stripped) <= self.max_sql_display_length:
384
+ # Short SQL: show fully
385
+ return sql_stripped
386
+ else:
387
+ # Long SQL: show summary with length indicator
388
+ summary = self._extract_sql_summary(sql)
389
+ return f"{summary} [SQL length: {len(sql_stripped)} chars]"
390
+
391
+ return sql.strip()
392
+
393
+ def update_config(
394
+ self,
395
+ sql_log_mode: Optional[str] = None,
396
+ slow_query_threshold: Optional[float] = None,
397
+ max_sql_display_length: Optional[int] = None,
398
+ ):
399
+ """
400
+ Dynamically update logger configuration at runtime.
401
+
402
+ Args::
403
+
404
+ sql_log_mode: New SQL logging mode ('off', 'auto', 'simple', 'full')
405
+ slow_query_threshold: New threshold in seconds for slow query warnings
406
+ max_sql_display_length: New maximum SQL length in auto mode before summarizing
407
+
408
+ Example::
409
+
410
+ # Enable full SQL logging for debugging
411
+ client.logger.update_config(sql_log_mode='full')
412
+
413
+ # Update multiple settings
414
+ client.logger.update_config(
415
+ sql_log_mode='auto',
416
+ slow_query_threshold=2.0,
417
+ max_sql_display_length=1000
418
+ )
419
+ """
420
+ if sql_log_mode is not None:
421
+ valid_modes = ['off', 'auto', 'simple', 'full']
422
+ if sql_log_mode not in valid_modes:
423
+ raise ValueError(f"Invalid sql_log_mode '{sql_log_mode}'. Must be one of {valid_modes}")
424
+ self.sql_log_mode = sql_log_mode
425
+
426
+ if slow_query_threshold is not None:
427
+ if slow_query_threshold < 0:
428
+ raise ValueError("slow_query_threshold must be non-negative")
429
+ self.slow_query_threshold = slow_query_threshold
430
+
431
+ if max_sql_display_length is not None:
432
+ if max_sql_display_length < 1:
433
+ raise ValueError("max_sql_display_length must be positive")
434
+ self.max_sql_display_length = max_sql_display_length
435
+
436
+ def log_query(
437
+ self,
438
+ query: str,
439
+ execution_time: Optional[float] = None,
440
+ affected_rows: Optional[int] = None,
441
+ success: bool = True,
442
+ override_sql_log_mode: Optional[str] = None,
443
+ ):
444
+ """
445
+ Log SQL query execution with smart formatting.
446
+
447
+ Args::
448
+
449
+ query: SQL query string
450
+ execution_time: Query execution time in seconds
451
+ affected_rows: Number of rows affected
452
+ success: Whether the query succeeded
453
+ override_sql_log_mode: Temporarily override sql_log_mode for this query only
454
+ """
455
+ # Use override mode if provided, otherwise use instance mode
456
+ effective_mode = override_sql_log_mode if override_sql_log_mode is not None else self.sql_log_mode
457
+
458
+ # Skip logging if mode is 'off'
459
+ if effective_mode == 'off':
460
+ return
461
+
462
+ # Determine if this is a slow query or error
463
+ is_slow = execution_time is not None and execution_time >= self.slow_query_threshold
464
+ is_error = not success
465
+
466
+ # Format SQL based on mode and query characteristics
467
+ display_sql = self._format_sql_for_log(query, is_error=is_error, is_slow=is_slow, override_mode=effective_mode)
468
+
469
+ # Skip if no SQL to display
470
+ if not display_sql:
471
+ return
472
+
473
+ # Build log message
474
+ status_icon = "✓" if success else "✗"
475
+ message_parts = [status_icon]
476
+
477
+ # Add execution time if available
478
+ if execution_time is not None:
479
+ message_parts.append(f"{execution_time:.3f}s")
480
+
481
+ # Add affected rows if available
482
+ if affected_rows is not None:
483
+ message_parts.append(f"{affected_rows} rows")
484
+
485
+ # Add SQL with appropriate prefix
486
+ if is_error:
487
+ message_parts.append(f"[ERROR] {display_sql}")
488
+ elif is_slow:
489
+ message_parts.append(f"[SLOW] {display_sql}")
490
+ else:
491
+ message_parts.append(display_sql)
492
+
493
+ message = " | ".join(message_parts)
494
+
495
+ # Use findCaller to get the actual caller's file and line
496
+ import sys
497
+
498
+ frame = sys._getframe(1)
499
+ filename = frame.f_code.co_filename
500
+ lineno = frame.f_lineno
501
+
502
+ # Create a new record with the caller's info
503
+ record = self.logger.makeRecord(
504
+ self.logger.name,
505
+ logging.ERROR if is_error else logging.INFO,
506
+ filename,
507
+ lineno,
508
+ self._format_message(message),
509
+ (),
510
+ None,
511
+ )
512
+
513
+ self.logger.handle(record)
514
+
515
+ def log_performance(self, operation: str, duration: float, **kwargs):
516
+ """Log performance metrics (always enabled, control via log level)"""
517
+
518
+ # Use findCaller to get the actual caller's file and line
519
+ import sys
520
+
521
+ frame = sys._getframe(1)
522
+ filename = frame.f_code.co_filename
523
+ lineno = frame.f_lineno
524
+
525
+ kwargs["duration"] = f"{duration:.3f}s"
526
+ # Create a new record with the caller's info
527
+ record = self.logger.makeRecord(
528
+ self.logger.name,
529
+ logging.INFO,
530
+ filename,
531
+ lineno,
532
+ self._format_message(f"Performance: {operation}", **kwargs),
533
+ (),
534
+ None,
535
+ )
536
+ self.logger.handle(record)
537
+
538
+ def log_error(self, error: Exception, context: Optional[str] = None, include_traceback: bool = False):
539
+ """
540
+ Log errors with context and optional traceback.
541
+
542
+ Args:
543
+ error: The exception object
544
+ context: Optional context description (e.g., "Query execution", "Table creation")
545
+ include_traceback: If True, include full traceback in log (useful for debugging)
546
+ """
547
+ error_type = type(error).__name__
548
+ error_message = str(error)
549
+
550
+ kwargs = {"error_type": error_type, "error_message": error_message}
551
+ if context:
552
+ kwargs["context"] = context
553
+
554
+ # Use findCaller to get the actual caller's file and line
555
+ import sys
556
+
557
+ frame = sys._getframe(1)
558
+ filename = frame.f_code.co_filename
559
+ lineno = frame.f_lineno
560
+
561
+ # Prepare error message
562
+ error_msg = self._format_message(f"Error occurred: {error_type}", **kwargs)
563
+
564
+ # Add traceback if requested (useful for debugging)
565
+ exc_info = None
566
+ if include_traceback:
567
+ exc_info = sys.exc_info()
568
+
569
+ # Create a new record with the caller's info
570
+ record = self.logger.makeRecord(
571
+ self.logger.name,
572
+ logging.ERROR,
573
+ filename,
574
+ lineno,
575
+ error_msg,
576
+ (),
577
+ exc_info, # Include exception info if requested
578
+ )
579
+ self.logger.handle(record)
580
+
581
+ def log_transaction(self, action: str, success: bool = True, **kwargs):
582
+ """Log transaction events"""
583
+ status = "✓ Transaction" if success else "✗ Transaction failed"
584
+
585
+ # Use findCaller to get the actual caller's file and line
586
+ import sys
587
+
588
+ frame = sys._getframe(1)
589
+ filename = frame.f_code.co_filename
590
+ lineno = frame.f_lineno
591
+
592
+ # Create a new record with the caller's info
593
+ record = self.logger.makeRecord(
594
+ self.logger.name,
595
+ logging.INFO,
596
+ filename,
597
+ lineno,
598
+ self._format_message(f"{status}: {action}", **kwargs),
599
+ (),
600
+ None,
601
+ )
602
+ self.logger.handle(record)
603
+
604
+ def log_account_operation(
605
+ self,
606
+ operation: str,
607
+ account_name: Optional[str] = None,
608
+ user_name: Optional[str] = None,
609
+ success: bool = True,
610
+ ):
611
+ """Log account management operations"""
612
+ status = "✓ Account operation" if success else "✗ Account operation failed"
613
+ kwargs = {"operation": operation}
614
+ if account_name:
615
+ kwargs["account"] = account_name
616
+ if user_name:
617
+ kwargs["user"] = user_name
618
+
619
+ # Use findCaller to get the actual caller's file and line
620
+ import sys
621
+
622
+ frame = sys._getframe(1)
623
+ filename = frame.f_code.co_filename
624
+ lineno = frame.f_lineno
625
+
626
+ # Create a new record with the caller's info
627
+ record = self.logger.makeRecord(
628
+ self.logger.name,
629
+ logging.INFO,
630
+ filename,
631
+ lineno,
632
+ self._format_message(f"{status}: {operation}", **kwargs),
633
+ (),
634
+ None,
635
+ )
636
+ self.logger.handle(record)
637
+
638
+ def log_snapshot_operation(
639
+ self,
640
+ operation: str,
641
+ snapshot_name: Optional[str] = None,
642
+ level: Optional[str] = None,
643
+ success: bool = True,
644
+ ):
645
+ """Log snapshot operations"""
646
+ status = "✓ Snapshot operation" if success else "✗ Snapshot operation failed"
647
+ kwargs = {"operation": operation}
648
+ if snapshot_name:
649
+ kwargs["snapshot"] = snapshot_name
650
+ if level:
651
+ kwargs["level"] = level
652
+
653
+ # Use findCaller to get the actual caller's file and line
654
+ import sys
655
+
656
+ frame = sys._getframe(1)
657
+ filename = frame.f_code.co_filename
658
+ lineno = frame.f_lineno
659
+
660
+ # Create a new record with the caller's info
661
+ record = self.logger.makeRecord(
662
+ self.logger.name,
663
+ logging.INFO,
664
+ filename,
665
+ lineno,
666
+ self._format_message(f"{status}: {operation}", **kwargs),
667
+ (),
668
+ None,
669
+ )
670
+ self.logger.handle(record)
671
+
672
+ def log_pubsub_operation(
673
+ self,
674
+ operation: str,
675
+ publication_name: Optional[str] = None,
676
+ subscription_name: Optional[str] = None,
677
+ success: bool = True,
678
+ ):
679
+ """Log PubSub operations"""
680
+ status = "✓ PubSub operation" if success else "✗ PubSub operation failed"
681
+ kwargs = {"operation": operation}
682
+ if publication_name:
683
+ kwargs["publication"] = publication_name
684
+ if subscription_name:
685
+ kwargs["subscription"] = subscription_name
686
+
687
+ # Use findCaller to get the actual caller's file and line
688
+ import sys
689
+
690
+ frame = sys._getframe(1)
691
+ filename = frame.f_code.co_filename
692
+ lineno = frame.f_lineno
693
+
694
+ # Create a new record with the caller's info
695
+ record = self.logger.makeRecord(
696
+ self.logger.name,
697
+ logging.INFO,
698
+ filename,
699
+ lineno,
700
+ self._format_message(f"{status}: {operation}", **kwargs),
701
+ (),
702
+ None,
703
+ )
704
+ self.logger.handle(record)
705
+
706
+ def set_level(self, level: int):
707
+ """Set logging level"""
708
+ self.logger.setLevel(level)
709
+ for handler in self.logger.handlers:
710
+ handler.setLevel(level)
711
+
712
+ def add_handler(self, handler: logging.Handler):
713
+ """Add custom handler to logger"""
714
+ self.logger.addHandler(handler)
715
+
716
+ def remove_handler(self, handler: logging.Handler):
717
+ """Remove handler from logger"""
718
+ self.logger.removeHandler(handler)
719
+
720
+ def is_custom(self) -> bool:
721
+ """Check if using custom logger"""
722
+ return self._is_custom
723
+
724
+
725
+ def create_default_logger(
726
+ level: int = logging.INFO,
727
+ format_string: Optional[str] = None,
728
+ sql_log_mode: str = "auto",
729
+ slow_query_threshold: float = 1.0,
730
+ max_sql_display_length: int = 500,
731
+ ) -> MatrixOneLogger:
732
+ """
733
+ Create a default MatrixOne logger
734
+
735
+ Args::
736
+
737
+ level: Logging level
738
+ format_string: Custom format string
739
+ sql_log_mode: SQL logging mode ('off', 'auto', 'simple', 'full')
740
+ slow_query_threshold: Threshold in seconds for slow query warnings
741
+ max_sql_display_length: Maximum SQL length in auto mode before summarizing
742
+
743
+ Returns::
744
+
745
+ MatrixOneLogger instance
746
+ """
747
+ return MatrixOneLogger(
748
+ logger=None,
749
+ level=level,
750
+ format_string=format_string,
751
+ sql_log_mode=sql_log_mode,
752
+ slow_query_threshold=slow_query_threshold,
753
+ max_sql_display_length=max_sql_display_length,
754
+ )
755
+
756
+
757
+ def create_custom_logger(
758
+ logger: logging.Logger,
759
+ sql_log_mode: str = "auto",
760
+ slow_query_threshold: float = 1.0,
761
+ max_sql_display_length: int = 500,
762
+ ) -> MatrixOneLogger:
763
+ """
764
+ Create MatrixOne logger from custom logger
765
+
766
+ Args::
767
+
768
+ logger: Custom logger instance
769
+ sql_log_mode: SQL logging mode ('off', 'auto', 'simple', 'full')
770
+ slow_query_threshold: Threshold in seconds for slow query warnings
771
+ max_sql_display_length: Maximum SQL length in auto mode before summarizing
772
+
773
+ Returns::
774
+
775
+ MatrixOneLogger instance
776
+ """
777
+ return MatrixOneLogger(
778
+ logger=logger,
779
+ sql_log_mode=sql_log_mode,
780
+ slow_query_threshold=slow_query_threshold,
781
+ max_sql_display_length=max_sql_display_length,
782
+ )