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/orm.py ADDED
@@ -0,0 +1,2277 @@
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 ORM - SQLAlchemy-like interface for MatrixOne database
17
+
18
+ This module provides a SQLAlchemy-compatible ORM interface for MatrixOne.
19
+ It supports both custom MatrixOne models and full SQLAlchemy integration.
20
+
21
+ For aggregate functions (COUNT, SUM, AVG, etc.), we recommend using SQLAlchemy's func module:
22
+ from sqlalchemy import func
23
+ query.select(func.count("id"))
24
+ query.select(func.sum("amount"))
25
+
26
+ This provides better type safety and integration with SQLAlchemy.
27
+ """
28
+
29
+ import logging
30
+ from typing import Any, Dict, List, Optional, TypeVar
31
+
32
+ # SQLAlchemy compatibility import
33
+ try:
34
+ from sqlalchemy.orm import declarative_base
35
+ except ImportError:
36
+ from sqlalchemy.ext.declarative import declarative_base
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ T = TypeVar("T")
41
+
42
+ # Export declarative_base for direct import
43
+ __all__ = ["declarative_base", "Query", "MatrixOneQuery"]
44
+
45
+
46
+ # For SQL functions, we recommend using SQLAlchemy's func module for better integration:
47
+ #
48
+ # from sqlalchemy import func
49
+ #
50
+ # # For SQLAlchemy models:
51
+ # query.select(func.count(User.id))
52
+ # query.select(func.sum(Order.amount))
53
+ # query.select(func.avg(Product.price))
54
+ #
55
+ # # For MatrixOne models, you can use string column names:
56
+ # query.select(func.count("id"))
57
+ # query.select(func.sum("amount"))
58
+ # query.select(func.avg("price"))
59
+ #
60
+ # This provides better type safety, SQL generation, and integration with SQLAlchemy.
61
+
62
+
63
+ # Remove SimpleModel - use SQLAlchemy models directly
64
+
65
+
66
+ class Query:
67
+ """
68
+ Query builder for ORM operations with SQLAlchemy-style interface.
69
+
70
+ This class provides a fluent interface for building SQL queries in a way that
71
+ mimics SQLAlchemy's query builder. It supports both SQLAlchemy models and
72
+ custom MatrixOne models.
73
+
74
+ Key Features:
75
+
76
+ - Fluent method chaining for building complex queries
77
+ - Support for SELECT, INSERT, UPDATE, DELETE operations
78
+ - SQLAlchemy expression support in filter(), having(), and other methods
79
+ - Automatic SQL generation and parameter binding
80
+ - Snapshot support for point-in-time queries
81
+
82
+ Usage::
83
+
84
+ # Basic query building
85
+ query = client.query(User)
86
+ results = query.filter(User.age > 25).order_by(User.name).limit(10).all()
87
+
88
+ # Complex queries with joins and aggregations
89
+ query = client.query(User, func.count(Order.id).label('order_count'))
90
+ results = (query
91
+ .join(Order, User.id == Order.user_id)
92
+ .group_by(User.id)
93
+ .having(func.count(Order.id) > 5)
94
+ .all())
95
+
96
+ Note: This is the legacy query builder. For new code, consider using MatrixOneQuery which
97
+ provides enhanced SQLAlchemy compatibility.
98
+ """
99
+
100
+ def __init__(self, model_class, client, snapshot_name: Optional[str] = None):
101
+ self.model_class = model_class
102
+ self.client = client
103
+ self.snapshot_name = snapshot_name
104
+ self._select_columns = []
105
+ self._where_conditions = []
106
+ self._where_params = []
107
+ self._joins = []
108
+ self._group_by_columns = []
109
+ self._having_conditions = []
110
+ self._having_params = []
111
+ self._order_by_columns = []
112
+ self._limit_count = None
113
+ self._offset_count = None
114
+ self._query_type = "SELECT"
115
+ # For INSERT
116
+ self._insert_values = []
117
+ # For UPDATE
118
+ self._update_set_columns = []
119
+ self._update_set_values = []
120
+
121
+ def snapshot(self, snapshot_name: str) -> "Query":
122
+ """Set snapshot for this query - SQLAlchemy style chaining"""
123
+ self.snapshot_name = snapshot_name
124
+ return self
125
+
126
+ def select(self, *columns) -> "Query":
127
+ """Select specific columns"""
128
+ if not columns:
129
+ # Select all columns from the SQLAlchemy model
130
+ if hasattr(self.model_class, "__table__"):
131
+ self._select_columns = [col.name for col in self.model_class.__table__.columns]
132
+ else:
133
+ self._select_columns = []
134
+ else:
135
+ self._select_columns = [str(col) for col in columns]
136
+ return self
137
+
138
+ def filter(self, condition: str, *params) -> "Query":
139
+ """Add WHERE condition"""
140
+ self._where_conditions.append(condition)
141
+ self._where_params.extend(params)
142
+ return self
143
+
144
+ def filter_by(self, **kwargs) -> "Query":
145
+ """Add WHERE conditions from keyword arguments"""
146
+ for key, value in kwargs.items():
147
+ # Check if the column exists in the SQLAlchemy model
148
+ if hasattr(self.model_class, "__table__") and key in [col.name for col in self.model_class.__table__.columns]:
149
+ self._where_conditions.append(f"{key} = ?")
150
+ self._where_params.append(value)
151
+ return self
152
+
153
+ def join(self, table: str, condition: str) -> "Query":
154
+ """Add JOIN clause"""
155
+ self._joins.append(f"JOIN {table} ON {condition}")
156
+ return self
157
+
158
+ def group_by(self, *columns) -> "Query":
159
+ """
160
+ Add GROUP BY clause to the query.
161
+
162
+ The GROUP BY clause is used to group rows that have the same values in specified columns,
163
+ typically used with aggregate functions like COUNT, SUM, AVG, etc.
164
+
165
+ Args::
166
+
167
+ *columns: Columns to group by as strings.
168
+ Can include column names, expressions, or functions.
169
+
170
+ Returns::
171
+
172
+ Query: Self for method chaining.
173
+
174
+ Examples::
175
+
176
+ # Basic GROUP BY
177
+ query.group_by("department")
178
+ query.group_by("department", "status")
179
+ query.group_by("YEAR(created_at)")
180
+
181
+ # Multiple columns
182
+ query.group_by("department", "status", "category")
183
+
184
+ # With expressions
185
+ query.group_by("YEAR(created_at)", "MONTH(created_at)")
186
+
187
+ Notes:
188
+ - GROUP BY is typically used with aggregate functions (COUNT, SUM, AVG, etc.)
189
+ - Multiple columns can be grouped together
190
+ - For SQLAlchemy expression support, use MatrixOneQuery instead
191
+
192
+ Raises::
193
+
194
+ ValueError: If columns are not strings
195
+ """
196
+ self._group_by_columns.extend([str(col) for col in columns])
197
+ return self
198
+
199
+ def having(self, condition: str, *params) -> "Query":
200
+ """
201
+ Add HAVING condition to the query.
202
+
203
+ The HAVING clause is used to filter groups after GROUP BY operations,
204
+ similar to WHERE clause but applied to aggregated results.
205
+
206
+ Args::
207
+
208
+ condition (str): The HAVING condition as a string.
209
+ Can include '?' placeholders for parameter substitution.
210
+ *params: Parameters to replace '?' placeholders in the condition.
211
+
212
+ Returns::
213
+
214
+ Query: Self for method chaining.
215
+
216
+ Examples::
217
+
218
+ # Basic HAVING with placeholders
219
+ query.group_by(User.department)
220
+ query.having("COUNT(*) > ?", 5)
221
+ query.having("AVG(age) > ?", 25)
222
+
223
+ # HAVING without placeholders
224
+ query.group_by(User.department)
225
+ query.having("COUNT(*) > 5")
226
+ query.having("AVG(age) > 25")
227
+
228
+ # Multiple HAVING conditions
229
+ query.group_by(User.department)
230
+ query.having("COUNT(*) > ?", 5)
231
+ query.having("AVG(age) > ?", 25)
232
+ query.having("MAX(age) < ?", 65)
233
+
234
+ Notes:
235
+ - HAVING clauses are typically used with GROUP BY operations
236
+ - Use '?' placeholders for safer parameter substitution
237
+ - Multiple HAVING conditions are combined with AND logic
238
+ - For SQLAlchemy expression support, use MatrixOneQuery instead
239
+
240
+ Raises::
241
+
242
+ ValueError: If condition is not a string
243
+ """
244
+ self._having_conditions.append(condition)
245
+ self._having_params.extend(params)
246
+ return self
247
+
248
+ def order_by(self, *columns) -> "Query":
249
+ """
250
+ Add ORDER BY clause to the query.
251
+
252
+ The ORDER BY clause is used to sort the result set by one or more columns,
253
+ either in ascending (ASC) or descending (DESC) order.
254
+
255
+ Args::
256
+
257
+ *columns: Columns to order by as strings.
258
+ Can include column names with optional ASC/DESC keywords.
259
+
260
+ Returns::
261
+
262
+ Query: Self for method chaining.
263
+
264
+ Examples::
265
+
266
+ # Basic ORDER BY
267
+ query.order_by("name")
268
+ query.order_by("created_at DESC")
269
+ query.order_by("department ASC", "name DESC")
270
+
271
+ # Multiple columns
272
+ query.order_by("department", "name", "age DESC")
273
+
274
+ # With expressions
275
+ query.order_by("COUNT(*) DESC")
276
+ query.order_by("AVG(salary) ASC")
277
+
278
+ Notes:
279
+ - ORDER BY sorts the result set in ascending order by default
280
+ - Use "DESC" for descending order, "ASC" for explicit ascending order
281
+ - Multiple columns are ordered from left to right
282
+ - For SQLAlchemy expression support, use MatrixOneQuery instead
283
+
284
+ Raises::
285
+
286
+ ValueError: If columns are not strings
287
+ """
288
+ for col in columns:
289
+ self._order_by_columns.append(str(col))
290
+ return self
291
+
292
+ def limit(self, count: int) -> "Query":
293
+ """Add LIMIT clause"""
294
+ self._limit_count = count
295
+ return self
296
+
297
+ def offset(self, count: int) -> "Query":
298
+ """Add OFFSET clause"""
299
+ self._offset_count = count
300
+ return self
301
+
302
+ def _build_select_sql(self) -> tuple[str, List[Any]]:
303
+ """Build SELECT SQL query using unified SQL builder"""
304
+ from .sql_builder import MatrixOneSQLBuilder
305
+
306
+ builder = MatrixOneSQLBuilder()
307
+
308
+ # Build SELECT clause
309
+ if self._select_columns:
310
+ builder.select(*self._select_columns)
311
+ else:
312
+ builder.select_all()
313
+
314
+ # Build FROM clause with snapshot
315
+ builder.from_table(self.model_class._table_name, self.snapshot_name)
316
+
317
+ # Add JOIN clauses
318
+ builder._joins = self._joins.copy()
319
+
320
+ # Add WHERE conditions and parameters
321
+ builder._where_conditions = self._where_conditions.copy()
322
+ builder._where_params = self._where_params.copy()
323
+
324
+ # Add GROUP BY columns
325
+ if self._group_by_columns:
326
+ builder.group_by(*self._group_by_columns)
327
+
328
+ # Add HAVING conditions and parameters
329
+ builder._having_conditions = self._having_conditions.copy()
330
+ builder._having_params = self._having_params.copy()
331
+
332
+ # Add ORDER BY columns
333
+ if self._order_by_columns:
334
+ builder.order_by(*self._order_by_columns)
335
+
336
+ # Add LIMIT and OFFSET
337
+ if self._limit_count is not None:
338
+ builder.limit(self._limit_count)
339
+ if self._offset_count is not None:
340
+ builder.offset(self._offset_count)
341
+
342
+ return builder.build()
343
+
344
+ def all(self) -> List:
345
+ """Execute query and return all results"""
346
+ sql, params = self._build_select_sql()
347
+ result = self._execute(sql, params)
348
+
349
+ models = []
350
+ for row in result.rows:
351
+ # Convert row to dictionary
352
+ row_dict = {}
353
+ if hasattr(self.model_class, "__table__"):
354
+ column_names = [col.name for col in self.model_class.__table__.columns]
355
+ else:
356
+ column_names = self._select_columns or []
357
+
358
+ for i, col_name in enumerate(column_names):
359
+ if i < len(row):
360
+ row_dict[col_name] = row[i]
361
+ # Create SQLAlchemy model instance
362
+ model = self.model_class(**row_dict)
363
+ models.append(model)
364
+
365
+ return models
366
+
367
+ def first(self) -> Optional:
368
+ """Execute query and return first result"""
369
+ self._limit_count = 1
370
+ results = self.all()
371
+ return results[0] if results else None
372
+
373
+ def one(self):
374
+ """
375
+ Execute query and return exactly one result.
376
+
377
+ This method executes the query and expects exactly one row to be returned.
378
+ If no rows are found or multiple rows are found, it raises appropriate exceptions.
379
+
380
+ Returns::
381
+ Model instance: The single result row as a model instance.
382
+
383
+ Raises::
384
+ NoResultFound: If no results are found.
385
+ MultipleResultsFound: If more than one result is found.
386
+
387
+ Examples::
388
+
389
+ # Get a user by unique ID
390
+ user = client.query(User).filter(User.id == 1).one()
391
+
392
+ # Get a user by unique email
393
+ user = client.query(User).filter(User.email == "admin@example.com").one()
394
+
395
+ Notes::
396
+ - Use this method when you expect exactly one result
397
+ - For cases where zero or one result is acceptable, use one_or_none()
398
+ - For cases where multiple results are acceptable, use all() or first()
399
+ """
400
+ results = self.all()
401
+ if len(results) == 0:
402
+ from sqlalchemy.exc import NoResultFound
403
+
404
+ raise NoResultFound("No row was found for one()")
405
+ elif len(results) > 1:
406
+ from sqlalchemy.exc import MultipleResultsFound
407
+
408
+ raise MultipleResultsFound("Multiple rows were found for one()")
409
+ return results[0]
410
+
411
+ def one_or_none(self):
412
+ """
413
+ Execute query and return exactly one result or None.
414
+
415
+ This method executes the query and returns exactly one row if found,
416
+ or None if no rows are found. If multiple rows are found, it raises an exception.
417
+
418
+ Returns::
419
+ Model instance or None: The single result row as a model instance,
420
+ or None if no results are found.
421
+
422
+ Raises::
423
+ MultipleResultsFound: If more than one result is found.
424
+
425
+ Examples::
426
+
427
+ # Get a user by ID, return None if not found
428
+ user = client.query(User).filter(User.id == 999).one_or_none()
429
+ if user:
430
+ print(f"Found user: {user.name}")
431
+
432
+ # Get a user by email, return None if not found
433
+ user = client.query(User).filter(User.email == "nonexistent@example.com").one_or_none()
434
+ if user is None:
435
+ print("User not found")
436
+
437
+ Notes::
438
+ - Use this method when zero or one result is acceptable
439
+ - For cases where exactly one result is required, use one()
440
+ - For cases where multiple results are acceptable, use all() or first()
441
+ """
442
+ results = self.all()
443
+ if len(results) == 0:
444
+ return None
445
+ elif len(results) > 1:
446
+ from sqlalchemy.exc import MultipleResultsFound
447
+
448
+ raise MultipleResultsFound("Multiple rows were found for one_or_none()")
449
+ return results[0]
450
+
451
+ def scalar(self):
452
+ """
453
+ Execute query and return the first column of the first result.
454
+
455
+ This method executes the query and returns the value of the first column
456
+ from the first row, or None if no results are found. This is useful for
457
+ getting single values like counts, sums, or specific column values.
458
+
459
+ Returns::
460
+ Any or None: The value of the first column from the first row,
461
+ or None if no results are found.
462
+
463
+ Examples::
464
+
465
+ # Get the count of all users
466
+ count = client.query(User).select(func.count(User.id)).scalar()
467
+
468
+ # Get the name of the first user
469
+ name = client.query(User).select(User.name).first().scalar()
470
+
471
+ # Get the maximum age
472
+ max_age = client.query(User).select(func.max(User.age)).scalar()
473
+
474
+ # Get a specific user's name by ID
475
+ name = client.query(User).select(User.name).filter(User.id == 1).scalar()
476
+
477
+ Notes::
478
+ - This method is particularly useful with aggregate functions
479
+ - For custom select queries, returns the first selected column value
480
+ - For model queries, returns the first column value from the model
481
+ - Returns None if no results are found
482
+ """
483
+ result = self.first()
484
+ if result is None:
485
+ return None
486
+
487
+ # If result is a model instance, return the first column value
488
+ if hasattr(result, '__dict__'):
489
+ # Get the first column value from the model
490
+ if hasattr(self.model_class, "__table__"):
491
+ first_column = list(self.model_class.__table__.columns)[0]
492
+ return getattr(result, first_column.name)
493
+ else:
494
+ # For raw queries, return the first attribute
495
+ attrs = [attr for attr in dir(result) if not attr.startswith('_')]
496
+ if attrs:
497
+ return getattr(result, attrs[0])
498
+ return None
499
+ else:
500
+ # For raw data, return the first element
501
+ if isinstance(result, (list, tuple)) and len(result) > 0:
502
+ return result[0]
503
+ return result
504
+
505
+ def count(self) -> int:
506
+ """Execute query and return count of results"""
507
+ # Create a new query for counting
508
+ count_query = Query(self.model_class, self.client, self.snapshot_name)
509
+ count_query._where_conditions = self._where_conditions.copy()
510
+ count_query._where_params = self._where_params.copy()
511
+ count_query._joins = self._joins.copy()
512
+ count_query._group_by_columns = self._group_by_columns.copy()
513
+ count_query._having_conditions = self._having_conditions.copy()
514
+ count_query._having_params = self._having_params.copy()
515
+
516
+ sql, params = count_query._build_select_sql()
517
+ # Replace SELECT clause with COUNT(*)
518
+ sql = sql.replace("SELECT *", "SELECT COUNT(*)")
519
+ if count_query._select_columns:
520
+ sql = sql.replace(f"SELECT {', '.join(count_query._select_columns)}", "SELECT COUNT(*)")
521
+
522
+ result = self._execute(sql, params)
523
+ return result.rows[0][0] if result.rows else 0
524
+
525
+ def insert(self, **kwargs) -> "Query":
526
+ """Start INSERT operation"""
527
+ self._query_type = "INSERT"
528
+ self._insert_values.append(kwargs)
529
+ return self
530
+
531
+ def bulk_insert(self, values_list: List[Dict[str, Any]]) -> "Query":
532
+ """Bulk insert multiple records"""
533
+ self._query_type = "INSERT"
534
+ self._insert_values.extend(values_list)
535
+ return self
536
+
537
+ def _build_insert_sql(self) -> tuple[str, List[Any]]:
538
+ """Build INSERT SQL query using unified SQL builder"""
539
+ from .sql_builder import build_insert_query
540
+
541
+ if not self._insert_values:
542
+ raise ValueError("No values provided for INSERT")
543
+
544
+ return build_insert_query(table_name=self.model_class._table_name, values=self._insert_values)
545
+
546
+ def update(self, **kwargs) -> "Query":
547
+ """Start UPDATE operation"""
548
+ self._query_type = "UPDATE"
549
+ for key, value in kwargs.items():
550
+ self._update_set_columns.append(f"{key} = ?")
551
+ self._update_set_values.append(value)
552
+ return self
553
+
554
+ def _build_update_sql(self) -> tuple[str, List[Any]]:
555
+ """Build UPDATE SQL query using unified SQL builder"""
556
+ from .sql_builder import build_update_query
557
+
558
+ if not self._update_set_columns:
559
+ raise ValueError("No SET clauses provided for UPDATE")
560
+
561
+ # Convert set columns and values to dict
562
+ set_values = {}
563
+ for i, col in enumerate(self._update_set_columns):
564
+ # Extract column name from "column = ?" format
565
+ col_name = col.split(" = ")[0]
566
+ set_values[col_name] = self._update_set_values[i]
567
+
568
+ return build_update_query(
569
+ table_name=self.model_class._table_name,
570
+ set_values=set_values,
571
+ where_conditions=self._where_conditions,
572
+ where_params=self._where_params,
573
+ )
574
+
575
+ def delete(self) -> Any:
576
+ """Execute DELETE operation"""
577
+ self._query_type = "DELETE"
578
+ sql, params = self._build_delete_sql()
579
+ return self._execute(sql, params)
580
+
581
+ def _build_delete_sql(self) -> tuple[str, List[Any]]:
582
+ """Build DELETE SQL query using unified SQL builder"""
583
+ from .sql_builder import build_delete_query
584
+
585
+ return build_delete_query(
586
+ table_name=self.model_class._table_name,
587
+ where_conditions=self._where_conditions,
588
+ where_params=self._where_params,
589
+ )
590
+
591
+ def execute(self) -> Any:
592
+ """Execute the query based on its type"""
593
+ if self._query_type == "SELECT":
594
+ return self.all()
595
+ elif self._query_type == "INSERT":
596
+ sql, params = self._build_insert_sql()
597
+ return self._execute(sql, params)
598
+ elif self._query_type == "UPDATE":
599
+ sql, params = self._build_update_sql()
600
+ return self._execute(sql, params)
601
+ elif self._query_type == "DELETE":
602
+ sql, params = self._build_delete_sql()
603
+ return self._execute(sql, params)
604
+ else:
605
+ raise ValueError(f"Unknown query type: {self._query_type}")
606
+
607
+
608
+ # CTE (Common Table Expression) support
609
+ class CTE:
610
+ """CTE (Common Table Expression) class for MatrixOne queries"""
611
+
612
+ def __init__(self, name: str, query, recursive: bool = False):
613
+ self.name = name
614
+ self.query = query
615
+ self.recursive = recursive
616
+ self._sql = None
617
+ self._params = None
618
+
619
+ def _compile(self):
620
+ """Compile the CTE query to SQL"""
621
+ if self._sql is None:
622
+ if hasattr(self.query, "_build_sql"):
623
+ # This is a BaseMatrixOneQuery object
624
+ self._sql, self._params = self.query._build_sql()
625
+ elif isinstance(self.query, str):
626
+ # This is a raw SQL string
627
+ self._sql = self.query
628
+ self._params = []
629
+ else:
630
+ raise ValueError("CTE query must be a BaseMatrixOneQuery object or SQL string")
631
+ return self._sql, self._params
632
+
633
+ def as_sql(self) -> tuple[str, list]:
634
+ """Get the compiled SQL and parameters for this CTE"""
635
+ return self._compile()
636
+
637
+ def __str__(self):
638
+ return f"CTE({self.name})"
639
+
640
+
641
+ # Base Query Builder - SQLAlchemy style
642
+ class BaseMatrixOneQuery:
643
+ """
644
+ Base MatrixOne Query builder that contains common SQL building logic.
645
+
646
+ This base class provides SQLAlchemy-compatible query building with:
647
+ - Full SQLAlchemy expression support in having(), filter(), and other methods
648
+ - Automatic SQL generation and parameter binding
649
+ - Support for both SQLAlchemy expressions and string conditions
650
+ - Method chaining for fluent query building
651
+
652
+ Key Features:
653
+
654
+ - SQLAlchemy expression support (e.g., func.count(User.id) > 5)
655
+ - String condition support (e.g., "COUNT(*) > ?", 5)
656
+ - Automatic column name resolution and SQL generation
657
+ - Full compatibility with SQLAlchemy 1.4+ and 2.0+
658
+
659
+ Note: This is a base class. For most use cases, use MatrixOneQuery instead.
660
+ """
661
+
662
+ def __init__(self, model_class, client, transaction_wrapper=None, snapshot=None):
663
+ self.model_class = model_class
664
+ self.client = client
665
+ self.transaction_wrapper = transaction_wrapper
666
+ self._snapshot_name = snapshot
667
+ self._table_alias = None # Add table alias support
668
+ self._select_columns = []
669
+ self._joins = []
670
+ self._where_conditions = []
671
+ self._where_params = []
672
+ self._group_by_columns = []
673
+ self._having_conditions = []
674
+ self._having_params = []
675
+ self._order_by_columns = []
676
+ self._limit_count = None
677
+ self._offset_count = None
678
+ self._ctes = [] # List of CTE definitions
679
+ self._query_type = "SELECT"
680
+ # For INSERT
681
+ self._insert_values = []
682
+ # For UPDATE
683
+ self._update_set_columns = []
684
+ self._update_set_values = []
685
+
686
+ # Handle None model_class (for column-only queries)
687
+ if model_class is None:
688
+ self._is_sqlalchemy_model = False
689
+ self._table_name = None # Will be set later by query() method
690
+ self._columns = {}
691
+ else:
692
+ # Detect if this is a SQLAlchemy model
693
+ self._is_sqlalchemy_model = self._detect_sqlalchemy_model()
694
+
695
+ # Get table name and columns based on model type
696
+ if isinstance(model_class, str):
697
+ # String table name
698
+ self._table_name = model_class
699
+ self._columns = {}
700
+ elif self._is_sqlalchemy_model:
701
+ self._table_name = model_class.__tablename__
702
+ self._columns = {col.name: col for col in model_class.__table__.columns}
703
+ else:
704
+ # Fallback to class name
705
+ self._table_name = getattr(model_class, "_table_name", model_class.__name__.lower())
706
+ self._columns = getattr(model_class, "_columns", {})
707
+
708
+ def _execute(self, sql, params=None):
709
+ """Execute SQL using either transaction wrapper or client"""
710
+ if self.transaction_wrapper:
711
+ return self.transaction_wrapper.execute(sql, params)
712
+ else:
713
+ return self.client.execute(sql, params)
714
+
715
+ def _detect_sqlalchemy_model(self) -> bool:
716
+ """Detect if the model class is a SQLAlchemy model"""
717
+ return (
718
+ hasattr(self.model_class, "__tablename__")
719
+ and hasattr(self.model_class, "__mapper__")
720
+ and hasattr(self.model_class, "__table__")
721
+ )
722
+
723
+ def select(self, *columns) -> "BaseMatrixOneQuery":
724
+ """Add SELECT columns - SQLAlchemy style"""
725
+ self._select_columns = list(columns)
726
+ return self
727
+
728
+ def cte(self, name: str, recursive: bool = False) -> CTE:
729
+ """Create a CTE (Common Table Expression) from this query - SQLAlchemy style
730
+
731
+ Args::
732
+
733
+ name: Name of the CTE
734
+ recursive: Whether this is a recursive CTE
735
+
736
+ Returns::
737
+
738
+ CTE object that can be used in other queries
739
+
740
+ Examples::
741
+
742
+ # Create a CTE from a query
743
+ user_stats = client.query(User).filter(User.active == True).cte("user_stats")
744
+
745
+ # Use the CTE in another query
746
+ result = client.query(user_stats).all()
747
+
748
+ # Recursive CTE example
749
+ hierarchy = client.query(Employee).filter(Employee.manager_id == None).cte("hierarchy", recursive=True)
750
+ """
751
+ return CTE(name, self, recursive)
752
+
753
+ def join(self, target, onclause=None, isouter=False, full=False) -> "BaseMatrixOneQuery":
754
+ """Add JOIN clause - SQLAlchemy style
755
+
756
+ Args::
757
+
758
+ target: Table or model to join with
759
+ onclause: ON condition for the join (optional, will be inferred if not provided)
760
+ isouter: If True, creates LEFT OUTER JOIN (default: False for INNER JOIN)
761
+ full: If True, creates FULL OUTER JOIN (default: False)
762
+
763
+ Returns::
764
+
765
+ Self for method chaining
766
+
767
+ Examples::
768
+
769
+ # Basic inner join with explicit condition
770
+ query.join(Address, User.id == Address.user_id)
771
+
772
+ # Inner join with string condition
773
+ query.join('addresses', 'users.id = addresses.user_id')
774
+
775
+ # Left outer join
776
+ query.join(Address, isouter=True)
777
+
778
+ # Join without explicit condition (will be inferred if possible)
779
+ query.join(Address)
780
+ """
781
+ # Determine join type
782
+ if full:
783
+ join_type = "FULL OUTER JOIN"
784
+ elif isouter:
785
+ join_type = "LEFT OUTER JOIN"
786
+ else:
787
+ join_type = "INNER JOIN"
788
+
789
+ # Handle different target types
790
+ if hasattr(target, 'name') and hasattr(target, 'as_sql'):
791
+ # This is a CTE object
792
+ table_name = target.name
793
+ elif hasattr(target, '__tablename__'):
794
+ # This is a SQLAlchemy model
795
+ table_name = target.__tablename__
796
+ elif hasattr(target, '_table_name'):
797
+ # This is a custom model with _table_name
798
+ table_name = target._table_name
799
+ else:
800
+ # String table name
801
+ table_name = str(target)
802
+
803
+ # Handle onclause
804
+ if onclause is not None:
805
+ # Process SQLAlchemy expressions
806
+ if hasattr(onclause, 'compile'):
807
+ # This is a SQLAlchemy expression, compile it
808
+ compiled = onclause.compile(compile_kwargs={"literal_binds": True})
809
+ on_condition = str(compiled)
810
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
811
+ import re
812
+
813
+ on_condition = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", on_condition)
814
+ # Handle SQLAlchemy's table prefixes (e.g., "users.name" -> "name")
815
+ on_condition = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", on_condition)
816
+ else:
817
+ # String condition
818
+ on_condition = str(onclause)
819
+
820
+ join_clause = f"{join_type} {table_name} ON {on_condition}"
821
+ else:
822
+ # No explicit onclause - create join without ON condition
823
+ # This matches SQLAlchemy behavior where ON condition can be inferred
824
+ join_clause = f"{join_type} {table_name}"
825
+
826
+ self._joins.append(join_clause)
827
+ return self
828
+
829
+ def innerjoin(self, target, onclause=None) -> "BaseMatrixOneQuery":
830
+ """Add INNER JOIN clause - SQLAlchemy style (alias for join with isouter=False)"""
831
+ return self.join(target, onclause, isouter=False)
832
+
833
+ def leftjoin(self, target, onclause=None) -> "BaseMatrixOneQuery":
834
+ """Add LEFT JOIN clause - SQLAlchemy style (alias for join with isouter=True)"""
835
+ return self.join(target, onclause, isouter=True)
836
+
837
+ def rightjoin(self, target, onclause=None) -> "BaseMatrixOneQuery":
838
+ """Add RIGHT JOIN clause - SQLAlchemy style"""
839
+ # MatrixOne doesn't support RIGHT JOIN, so we'll use LEFT JOIN with reversed tables
840
+ # This is a limitation of MatrixOne, but we provide the method for compatibility
841
+ if onclause is not None:
842
+ # Process SQLAlchemy expressions
843
+ if hasattr(onclause, 'compile'):
844
+ compiled = onclause.compile(compile_kwargs={"literal_binds": True})
845
+ on_condition = str(compiled)
846
+ import re
847
+
848
+ on_condition = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", on_condition)
849
+ on_condition = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", on_condition)
850
+ else:
851
+ on_condition = str(onclause)
852
+
853
+ # For RIGHT JOIN, we need to reverse the condition
854
+ # This is a simplified approach - in practice, you might need more complex logic
855
+ join_clause = f"LEFT JOIN {target} ON {on_condition}"
856
+ else:
857
+ join_clause = f"LEFT JOIN {target}"
858
+
859
+ self._joins.append(join_clause)
860
+ return self
861
+
862
+ def fullouterjoin(self, target, onclause=None) -> "BaseMatrixOneQuery":
863
+ """Add FULL OUTER JOIN clause - SQLAlchemy style (alias for join with full=True)"""
864
+ return self.join(target, onclause, full=True)
865
+
866
+ def outerjoin(self, target, onclause=None) -> "BaseMatrixOneQuery":
867
+ """Add LEFT OUTER JOIN clause - SQLAlchemy style (alias for leftjoin)"""
868
+ return self.leftjoin(target, onclause)
869
+
870
+ def group_by(self, *columns) -> "BaseMatrixOneQuery":
871
+ """
872
+ Add GROUP BY clause to the query - SQLAlchemy style compatibility.
873
+
874
+ The GROUP BY clause is used to group rows that have the same values in specified columns,
875
+ typically used with aggregate functions like COUNT, SUM, AVG, etc.
876
+
877
+ Args::
878
+
879
+ *columns: Columns to group by. Can be:
880
+ - SQLAlchemy column expressions (e.g., User.department, func.year(User.created_at))
881
+ - String column names (e.g., "department", "created_at")
882
+ - SQLAlchemy function expressions (e.g., func.year(User.created_at))
883
+
884
+ Returns::
885
+
886
+ BaseMatrixOneQuery: Self for method chaining.
887
+
888
+ Examples::
889
+
890
+ # SQLAlchemy column expressions (recommended)
891
+ query.group_by(User.department)
892
+ query.group_by(User.department, User.status)
893
+ query.group_by(func.year(User.created_at))
894
+ query.group_by(func.date(User.created_at), User.department)
895
+
896
+ # String column names
897
+ query.group_by("department")
898
+ query.group_by("department", "status")
899
+
900
+ # Complex expressions
901
+ query.group_by(
902
+ User.department,
903
+ func.year(User.created_at),
904
+ func.month(User.created_at)
905
+ )
906
+
907
+ Notes:
908
+ - GROUP BY is typically used with aggregate functions (COUNT, SUM, AVG, etc.)
909
+ - SQLAlchemy expressions provide better type safety and integration
910
+ - Multiple columns can be grouped together
911
+ - Column references in SQLAlchemy expressions are automatically
912
+ converted to MatrixOne-compatible format
913
+
914
+ Raises::
915
+
916
+ ValueError: If invalid column type is provided
917
+ SQLAlchemyError: If SQLAlchemy expression compilation fails
918
+ """
919
+ for col in columns:
920
+ if isinstance(col, str):
921
+ self._group_by_columns.append(col)
922
+ elif hasattr(col, "compile"): # SQLAlchemy expression
923
+ # Compile the expression to SQL string
924
+ compiled = col.compile(compile_kwargs={"literal_binds": True})
925
+ sql_str = str(compiled)
926
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
927
+ import re
928
+
929
+ sql_str = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", sql_str)
930
+ # Handle SQLAlchemy's table prefixes (e.g., "users.name" -> "name")
931
+ sql_str = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", sql_str)
932
+ self._group_by_columns.append(sql_str)
933
+ else:
934
+ self._group_by_columns.append(str(col))
935
+ return self
936
+
937
+ def having(self, condition, *params) -> "BaseMatrixOneQuery":
938
+ """
939
+ Add HAVING clause to the query - SQLAlchemy style compatibility.
940
+
941
+ The HAVING clause is used to filter groups after GROUP BY operations,
942
+ similar to WHERE clause but applied to aggregated results.
943
+
944
+ Args::
945
+
946
+ condition: The HAVING condition. Can be:
947
+ - SQLAlchemy expression (e.g., func.count(User.id) > 5)
948
+ - String condition with placeholders (e.g., "COUNT(*) > ?")
949
+ - String condition without placeholders (e.g., "COUNT(*) > 5")
950
+ *params: Additional parameters for string-based conditions.
951
+ Used to replace '?' placeholders in condition string.
952
+
953
+ Returns::
954
+
955
+ BaseMatrixOneQuery: Self for method chaining.
956
+
957
+ Examples::
958
+
959
+ # SQLAlchemy expression syntax (recommended)
960
+ query.group_by(User.department)
961
+ query.having(func.count(User.id) > 5)
962
+ query.having(func.avg(User.age) > 25)
963
+ query.having(func.count(func.distinct(User.id)) > 3)
964
+
965
+ # String-based syntax with placeholders
966
+ query.group_by(User.department)
967
+ query.having("COUNT(*) > ?", 5)
968
+ query.having("AVG(age) > ?", 25)
969
+
970
+ # String-based syntax without placeholders
971
+ query.group_by(User.department)
972
+ query.having("COUNT(*) > 5")
973
+ query.having("AVG(age) > 25")
974
+
975
+ # Multiple HAVING conditions
976
+ query.group_by(User.department)
977
+ query.having(func.count(User.id) > 5)
978
+ query.having(func.avg(User.age) > 25)
979
+ query.having(func.max(User.age) < 65)
980
+
981
+ # Mixed string and expression syntax
982
+ query.group_by(User.department)
983
+ query.having("COUNT(*) > ?", 5) # String
984
+ query.having(func.avg(User.age) > 25) # Expression
985
+
986
+ Notes:
987
+ - HAVING clauses are typically used with GROUP BY operations
988
+ - SQLAlchemy expressions provide better type safety and integration
989
+ - String conditions with placeholders are safer against SQL injection
990
+ - Multiple HAVING conditions are combined with AND logic
991
+ - Column references in SQLAlchemy expressions are automatically
992
+ converted to MatrixOne-compatible format
993
+
994
+ Supported SQLAlchemy Functions:
995
+ - func.count(): Count rows or distinct values
996
+ - func.avg(): Calculate average
997
+ - func.sum(): Calculate sum
998
+ - func.min(): Find minimum value
999
+ - func.max(): Find maximum value
1000
+ - func.distinct(): Get distinct values
1001
+
1002
+ Raises::
1003
+
1004
+ ValueError: If invalid condition type is provided
1005
+ SQLAlchemyError: If SQLAlchemy expression compilation fails
1006
+ """
1007
+ # Check if condition contains FulltextFilter objects
1008
+ if hasattr(condition, "compile") and self._contains_fulltext_filter(condition):
1009
+ # Handle SQLAlchemy expressions that contain FulltextFilter objects
1010
+ formatted_condition = self._process_fulltext_expression(condition)
1011
+ self._having_conditions.append(formatted_condition)
1012
+ elif hasattr(condition, "compile"):
1013
+ # This is a SQLAlchemy expression (OR, AND, BinaryExpression, etc.), compile it to SQL
1014
+ compiled = condition.compile(compile_kwargs={"literal_binds": True})
1015
+ formatted_condition = str(compiled)
1016
+
1017
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
1018
+ import re
1019
+
1020
+ formatted_condition = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", formatted_condition)
1021
+
1022
+ # Handle SQLAlchemy's table prefixes (e.g., "users.name" -> "name")
1023
+ formatted_condition = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", formatted_condition)
1024
+
1025
+ self._having_conditions.append(formatted_condition)
1026
+ else:
1027
+ # Handle string conditions - replace ? placeholders with actual values
1028
+ formatted_condition = str(condition)
1029
+
1030
+ # If there are params but no ? placeholders, append them to the condition
1031
+ if params and "?" not in formatted_condition:
1032
+ for param in params:
1033
+ if hasattr(param, "_build_sql"):
1034
+ # This is a MatrixOne expression, compile it
1035
+ sql, _ = param._build_sql()
1036
+ formatted_condition += f" AND {sql}"
1037
+ else:
1038
+ # Regular parameter
1039
+ if isinstance(param, str):
1040
+ formatted_condition += f" AND '{param}'"
1041
+ else:
1042
+ formatted_condition += f" AND {param}"
1043
+ else:
1044
+ # Replace ? placeholders with actual values
1045
+ for param in params:
1046
+ if isinstance(param, str):
1047
+ formatted_condition = formatted_condition.replace("?", f"'{param}'", 1)
1048
+ else:
1049
+ formatted_condition = formatted_condition.replace("?", str(param), 1)
1050
+
1051
+ self._having_conditions.append(formatted_condition)
1052
+
1053
+ return self
1054
+
1055
+ def snapshot(self, snapshot_name: str) -> "BaseMatrixOneQuery":
1056
+ """Add snapshot support - SQLAlchemy style chaining"""
1057
+ self._snapshot_name = snapshot_name
1058
+ return self
1059
+
1060
+ def alias(self, alias_name: str) -> "BaseMatrixOneQuery":
1061
+ """Set table alias for this query - SQLAlchemy style chaining"""
1062
+ self._table_alias = alias_name
1063
+ return self
1064
+
1065
+ def subquery(self, alias_name: str = None) -> str:
1066
+ """Convert this query to a subquery with optional alias"""
1067
+ sql, params = self._build_sql()
1068
+ if alias_name:
1069
+ return f"({sql}) AS {alias_name}"
1070
+ else:
1071
+ return f"({sql})"
1072
+
1073
+ def filter(self, condition, *params) -> "BaseMatrixOneQuery":
1074
+ """Add WHERE conditions - SQLAlchemy style unified interface"""
1075
+
1076
+ # Check if condition is a LogicalIn object
1077
+ if hasattr(condition, "compile") and hasattr(condition, "column") and hasattr(condition, "values"):
1078
+ # This is a LogicalIn object
1079
+ formatted_condition = condition.compile()
1080
+ self._where_conditions.append(formatted_condition)
1081
+ # LogicalIn objects now generate complete SQL with values, no additional parameters needed
1082
+ # Check if condition contains FulltextFilter objects
1083
+ elif hasattr(condition, "compile") and self._contains_fulltext_filter(condition):
1084
+ # Handle SQLAlchemy expressions that contain FulltextFilter objects
1085
+ formatted_condition = self._process_fulltext_expression(condition)
1086
+ self._where_conditions.append(formatted_condition)
1087
+ elif hasattr(condition, "compile"):
1088
+ # This is a SQLAlchemy expression (OR, AND, BinaryExpression, etc.), compile it to SQL
1089
+ compiled = condition.compile(compile_kwargs={"literal_binds": True})
1090
+ formatted_condition = str(compiled)
1091
+
1092
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
1093
+ import re
1094
+
1095
+ formatted_condition = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", formatted_condition)
1096
+
1097
+ # Handle SQLAlchemy's table prefixes (e.g., "users.name" -> "name")
1098
+ formatted_condition = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", formatted_condition)
1099
+
1100
+ self._where_conditions.append(formatted_condition)
1101
+ else:
1102
+ # Handle string conditions - replace ? placeholders with actual values
1103
+ formatted_condition = str(condition)
1104
+
1105
+ # If there are params but no ? placeholders, append them to the condition
1106
+ if params and "?" not in formatted_condition:
1107
+ for param in params:
1108
+ if hasattr(param, "_build_sql"):
1109
+ # This is a subquery object, convert it to SQL
1110
+ subquery_sql, _ = param._build_sql()
1111
+ formatted_condition += f" ({subquery_sql})"
1112
+ else:
1113
+ formatted_condition += f" {param}"
1114
+ else:
1115
+ # Add params to _where_params for processing
1116
+ if params:
1117
+ self._where_params.extend(params)
1118
+
1119
+ # Process any remaining ? placeholders by replacing them with the next parameter
1120
+ while "?" in formatted_condition and self._where_params:
1121
+ param = self._where_params.pop(0)
1122
+ if isinstance(param, str):
1123
+ formatted_condition = formatted_condition.replace("?", f"'{param}'", 1)
1124
+ elif hasattr(param, "_build_sql"):
1125
+ # This is a subquery object, convert it to SQL
1126
+ subquery_sql, _ = param._build_sql()
1127
+ formatted_condition = formatted_condition.replace("?", f"({subquery_sql})", 1)
1128
+ else:
1129
+ formatted_condition = formatted_condition.replace("?", str(param), 1)
1130
+
1131
+ self._where_conditions.append(formatted_condition)
1132
+
1133
+ return self
1134
+
1135
+ def _contains_fulltext_filter(self, condition) -> bool:
1136
+ """Check if a SQLAlchemy expression contains FulltextFilter objects."""
1137
+ from .sqlalchemy_ext.fulltext_search import FulltextFilter
1138
+
1139
+ if isinstance(condition, FulltextFilter):
1140
+ return True
1141
+
1142
+ # Check nested clauses
1143
+ if hasattr(condition, 'clauses'):
1144
+ for clause in condition.clauses:
1145
+ if self._contains_fulltext_filter(clause):
1146
+ return True
1147
+
1148
+ return False
1149
+
1150
+ def _process_fulltext_expression(self, condition) -> str:
1151
+ """Process SQLAlchemy expressions that contain FulltextFilter objects."""
1152
+ import re
1153
+
1154
+ from .sqlalchemy_ext.fulltext_search import FulltextFilter
1155
+
1156
+ if isinstance(condition, FulltextFilter):
1157
+ return condition.compile()
1158
+
1159
+ # Handle and_() expressions
1160
+ if hasattr(condition, 'clauses') and hasattr(condition, 'operator'):
1161
+ parts = []
1162
+ for clause in condition.clauses:
1163
+ if isinstance(clause, FulltextFilter):
1164
+ parts.append(clause.compile())
1165
+ elif hasattr(clause, 'compile'):
1166
+ # Regular SQLAlchemy expression
1167
+ compiled = clause.compile(compile_kwargs={"literal_binds": True})
1168
+ formatted = str(compiled)
1169
+ # Fix quoted column names and table prefixes
1170
+ formatted = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", formatted)
1171
+ formatted = re.sub(r"\w+\.(\w+)", r"\1", formatted)
1172
+ parts.append(formatted)
1173
+ else:
1174
+ parts.append(str(clause))
1175
+
1176
+ # Determine operator
1177
+ if str(condition.operator).upper() == 'AND':
1178
+ return f"({' AND '.join(parts)})"
1179
+ elif str(condition.operator).upper() == 'OR':
1180
+ return f"({' OR '.join(parts)})"
1181
+ else:
1182
+ return f"({f' {condition.operator} '.join(parts)})"
1183
+
1184
+ # Fallback for other types
1185
+ return str(condition)
1186
+
1187
+ def filter_by(self, **kwargs) -> "BaseMatrixOneQuery":
1188
+ """Add WHERE conditions from keyword arguments - SQLAlchemy style"""
1189
+ for key, value in kwargs.items():
1190
+ if key in self._columns:
1191
+ if isinstance(value, str):
1192
+ self._where_conditions.append(f"{key} = '{value}'")
1193
+ elif isinstance(value, (int, float)):
1194
+ self._where_conditions.append(f"{key} = {value}")
1195
+ else:
1196
+ # For other types, use parameterized query
1197
+ self._where_conditions.append(f"{key} = ?")
1198
+ self._where_params.append(value)
1199
+ return self
1200
+
1201
+ def where(self, condition: str, *params) -> "BaseMatrixOneQuery":
1202
+ """Add WHERE condition - alias for filter method"""
1203
+ return self.filter(condition, *params)
1204
+
1205
+ def logical_in(self, column, values) -> "BaseMatrixOneQuery":
1206
+ """
1207
+ Add IN condition with support for various value types.
1208
+
1209
+ This method provides enhanced IN functionality that can handle:
1210
+ - Lists of values: [1, 2, 3]
1211
+ - SQLAlchemy expressions: func.count(User.id)
1212
+ - FulltextFilter objects: boolean_match("title", "content").must("python")
1213
+ - Subqueries: client.query(User).select(User.id)
1214
+
1215
+ Args::
1216
+
1217
+ column: Column to check (can be string or SQLAlchemy column)
1218
+ values: Values to check against. Can be:
1219
+ - List of values: [1, 2, 3, "a", "b"]
1220
+ - SQLAlchemy expression: func.count(User.id)
1221
+ - FulltextFilter object: boolean_match("title", "content").must("python")
1222
+ - Subquery object: client.query(User).select(User.id)
1223
+
1224
+ Returns::
1225
+
1226
+ BaseMatrixOneQuery: Self for method chaining.
1227
+
1228
+ Examples::
1229
+
1230
+ # List of values
1231
+ query.logical_in("city", ["北京", "上海", "广州"])
1232
+ query.logical_in(User.id, [1, 2, 3, 4])
1233
+
1234
+ # SQLAlchemy expression
1235
+ query.logical_in("id", func.count(User.id))
1236
+
1237
+ # FulltextFilter
1238
+ query.logical_in("id", boolean_match("title", "content").must("python"))
1239
+
1240
+ # Subquery
1241
+ subquery = client.query(User).select(User.id).filter(User.active == True)
1242
+ query.logical_in("author_id", subquery)
1243
+
1244
+ Notes:
1245
+ - This method automatically handles different value types
1246
+ - For FulltextFilter objects, it creates a subquery using the fulltext search
1247
+ - For SQLAlchemy expressions, it compiles them to SQL
1248
+ - For lists, it creates standard IN clauses with proper parameter binding
1249
+ """
1250
+ # Handle column name
1251
+ if hasattr(column, "name"):
1252
+ column_name = column.name
1253
+ else:
1254
+ column_name = str(column)
1255
+
1256
+ # Handle different types of values
1257
+ if hasattr(values, "compile") and hasattr(values, "columns"):
1258
+ # This is a FulltextFilter object
1259
+ if hasattr(values, "compile") and hasattr(values, "query_builder"):
1260
+ # Convert FulltextFilter to subquery
1261
+ fulltext_sql = values.compile()
1262
+ # Create a subquery that selects IDs from the fulltext search
1263
+ # We need to determine the table name from the context
1264
+ table_name = self._table_name or "table"
1265
+ subquery_sql = f"SELECT id FROM {table_name} WHERE {fulltext_sql}"
1266
+ condition = f"{column_name} IN ({subquery_sql})"
1267
+ self._where_conditions.append(condition)
1268
+ else:
1269
+ # Handle other SQLAlchemy expressions
1270
+ compiled = values.compile(compile_kwargs={"literal_binds": True})
1271
+ sql_str = str(compiled)
1272
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
1273
+ import re
1274
+
1275
+ sql_str = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", sql_str)
1276
+ sql_str = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", sql_str)
1277
+ condition = f"{column_name} IN ({sql_str})"
1278
+ self._where_conditions.append(condition)
1279
+ elif hasattr(values, "_build_sql"):
1280
+ # This is a subquery object
1281
+ subquery_sql, subquery_params = values._build_sql()
1282
+ condition = f"{column_name} IN ({subquery_sql})"
1283
+ self._where_conditions.append(condition)
1284
+ self._where_params.extend(subquery_params)
1285
+ elif isinstance(values, (list, tuple)):
1286
+ # Handle list of values
1287
+ if not values:
1288
+ # Empty list means no matches
1289
+ condition = "1=0" # Always false
1290
+ self._where_conditions.append(condition)
1291
+ else:
1292
+ # Create placeholders for each value
1293
+ placeholders = ",".join(["?" for _ in values])
1294
+ condition = f"{column_name} IN ({placeholders})"
1295
+ self._where_conditions.append(condition)
1296
+ self._where_params.extend(values)
1297
+ else:
1298
+ # Single value
1299
+ condition = f"{column_name} IN (?)"
1300
+ self._where_conditions.append(condition)
1301
+ self._where_params.append(values)
1302
+
1303
+ return self
1304
+
1305
+ def order_by(self, *columns) -> "BaseMatrixOneQuery":
1306
+ """
1307
+ Add ORDER BY clause to the query - SQLAlchemy style compatibility.
1308
+
1309
+ The ORDER BY clause is used to sort the result set by one or more columns,
1310
+ either in ascending (ASC) or descending (DESC) order.
1311
+
1312
+ Args::
1313
+
1314
+ *columns: Columns to order by. Can be:
1315
+ - SQLAlchemy column expressions (e.g., User.name, User.created_at.desc())
1316
+ - String column names (e.g., "name", "created_at DESC")
1317
+ - SQLAlchemy function expressions (e.g., func.count(User.id))
1318
+ - SQLAlchemy desc/asc expressions (e.g., desc(User.name), asc(User.age))
1319
+
1320
+ Returns::
1321
+
1322
+ BaseMatrixOneQuery: Self for method chaining.
1323
+
1324
+ Examples::
1325
+
1326
+ # SQLAlchemy column expressions (recommended)
1327
+ query.order_by(User.name)
1328
+ query.order_by(User.created_at.desc())
1329
+ query.order_by(User.department, User.name.asc())
1330
+
1331
+ # String column names
1332
+ query.order_by("name")
1333
+ query.order_by("created_at DESC")
1334
+ query.order_by("department ASC", "name DESC")
1335
+
1336
+ # SQLAlchemy desc/asc functions
1337
+ from sqlalchemy import desc, asc
1338
+ query.order_by(desc(User.created_at))
1339
+ query.order_by(asc(User.name), desc(User.age))
1340
+
1341
+ # Function expressions
1342
+ query.order_by(func.count(User.id).desc())
1343
+ query.order_by(func.avg(User.salary).asc())
1344
+
1345
+ # Mixed expressions
1346
+ query.order_by(User.department, "name DESC")
1347
+ query.order_by(func.count(User.id).desc(), User.name.asc())
1348
+
1349
+ # Complex expressions
1350
+ query.order_by(
1351
+ User.department.asc(),
1352
+ func.count(User.id).desc(),
1353
+ User.name.asc()
1354
+ )
1355
+
1356
+ Notes:
1357
+ - ORDER BY sorts the result set in ascending order by default
1358
+ - Use .desc() or desc() for descending order
1359
+ - Use .asc() or asc() for explicit ascending order
1360
+ - Multiple columns are ordered from left to right
1361
+ - SQLAlchemy expressions provide better type safety and integration
1362
+ - Column references in SQLAlchemy expressions are automatically
1363
+ converted to MatrixOne-compatible format
1364
+
1365
+ Raises::
1366
+
1367
+ ValueError: If invalid column type is provided
1368
+ SQLAlchemyError: If SQLAlchemy expression compilation fails
1369
+ """
1370
+ for col in columns:
1371
+ if isinstance(col, str):
1372
+ self._order_by_columns.append(col)
1373
+ elif hasattr(col, "compile"): # SQLAlchemy expression
1374
+ # Compile the expression to SQL string
1375
+ compiled = col.compile(compile_kwargs={"literal_binds": True})
1376
+ sql_str = str(compiled)
1377
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
1378
+ import re
1379
+
1380
+ sql_str = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", sql_str)
1381
+ # Handle SQLAlchemy's table prefixes (e.g., "users.name" -> "name")
1382
+ sql_str = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", sql_str)
1383
+ self._order_by_columns.append(sql_str)
1384
+ else:
1385
+ self._order_by_columns.append(str(col))
1386
+ return self
1387
+
1388
+ def limit(self, count: int) -> "BaseMatrixOneQuery":
1389
+ """Add LIMIT clause - SQLAlchemy style"""
1390
+ self._limit_count = count
1391
+ return self
1392
+
1393
+ def offset(self, count: int) -> "BaseMatrixOneQuery":
1394
+ """Add OFFSET clause - SQLAlchemy style"""
1395
+ self._offset_count = count
1396
+ return self
1397
+
1398
+ def _build_sql(self) -> tuple[str, List[Any]]:
1399
+ """Build SQL query using unified SQL builder"""
1400
+ from .sql_builder import MatrixOneSQLBuilder
1401
+
1402
+ builder = MatrixOneSQLBuilder()
1403
+
1404
+ # Build CTE clause if any CTEs are defined
1405
+ if self._ctes:
1406
+ for cte in self._ctes:
1407
+ if isinstance(cte, CTE):
1408
+ cte_sql, cte_params = cte.as_sql()
1409
+ builder._ctes.append({'name': cte.name, 'sql': cte_sql, 'params': cte_params})
1410
+ else:
1411
+ # Handle legacy CTE format
1412
+ builder._ctes.append(cte)
1413
+
1414
+ # Build SELECT clause with SQLAlchemy function support
1415
+ if self._select_columns:
1416
+ # Convert SQLAlchemy function objects to strings
1417
+ select_parts = []
1418
+ for col in self._select_columns:
1419
+ if hasattr(col, "compile"): # SQLAlchemy function object
1420
+ # Check if this is a FulltextLabel (which already has AS in compile())
1421
+ if hasattr(col, "_compiler_dispatch") and hasattr(col, "name") and "FulltextLabel" in str(type(col)):
1422
+ # For FulltextLabel, use compile() which already includes AS
1423
+ sql_str = col.compile(compile_kwargs={"literal_binds": True})
1424
+ else:
1425
+ # For regular SQLAlchemy objects
1426
+ compiled = col.compile(compile_kwargs={"literal_binds": True})
1427
+ sql_str = str(compiled)
1428
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
1429
+ # Convert avg('column') to avg(column)
1430
+ import re
1431
+
1432
+ sql_str = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", sql_str)
1433
+
1434
+ # Handle SQLAlchemy label() method - add AS alias if present
1435
+ # But avoid using SQL reserved keywords as aliases
1436
+ if hasattr(col, "name") and col.name and col.name.upper() not in ["DISTINCT"]:
1437
+ sql_str = f"{sql_str} AS {col.name}"
1438
+
1439
+ select_parts.append(sql_str)
1440
+ else:
1441
+ select_parts.append(str(col))
1442
+ builder._select_columns = select_parts
1443
+ else:
1444
+ builder.select_all()
1445
+
1446
+ # Build FROM clause with optional table alias and snapshot
1447
+ if not self._table_name:
1448
+ raise ValueError("Table name is required. Provide a model class or set table name manually.")
1449
+
1450
+ if self._table_alias:
1451
+ builder._from_table = f"{self._table_name} AS {self._table_alias}"
1452
+ else:
1453
+ builder._from_table = self._table_name
1454
+
1455
+ builder._from_snapshot = self._snapshot_name
1456
+
1457
+ # Add JOIN clauses
1458
+ builder._joins = self._joins.copy()
1459
+
1460
+ # Add WHERE conditions and parameters
1461
+ builder._where_conditions = self._where_conditions.copy()
1462
+ builder._where_params = self._where_params.copy()
1463
+
1464
+ # Add GROUP BY columns
1465
+ if self._group_by_columns:
1466
+ builder.group_by(*self._group_by_columns)
1467
+
1468
+ # Add HAVING conditions and parameters
1469
+ builder._having_conditions = self._having_conditions.copy()
1470
+ builder._having_params = self._having_params.copy()
1471
+
1472
+ # Add ORDER BY columns
1473
+ if self._order_by_columns:
1474
+ builder.order_by(*self._order_by_columns)
1475
+
1476
+ # Add LIMIT and OFFSET
1477
+ if self._limit_count is not None:
1478
+ builder.limit(self._limit_count)
1479
+ if self._offset_count is not None:
1480
+ builder.offset(self._offset_count)
1481
+
1482
+ # Build the final SQL and combine parameters
1483
+ sql, params = builder.build()
1484
+
1485
+ # Parameters are already combined in builder.build() since we added CTEs to builder._ctes
1486
+ return sql, params
1487
+
1488
+ def _create_row_data(self, row, select_cols):
1489
+ """Create RowData object for aggregate queries"""
1490
+
1491
+ class RowData:
1492
+ def __init__(self, values, columns):
1493
+ for i, col in enumerate(columns):
1494
+ if i < len(values):
1495
+ # Replace spaces with underscores for valid Python attribute names
1496
+ attr_name = col.replace(" ", "_")
1497
+ setattr(self, attr_name, values[i])
1498
+ # Also support indexing for backward compatibility
1499
+ self._values = values
1500
+
1501
+ def __getitem__(self, index):
1502
+ return self._values[index]
1503
+
1504
+ def __len__(self):
1505
+ return len(self._values)
1506
+
1507
+ return RowData(row, select_cols)
1508
+
1509
+ def _extract_select_columns(self):
1510
+ """Extract column names from select columns"""
1511
+ select_cols = []
1512
+ for col in self._select_columns:
1513
+ # Check if this is a SQLAlchemy function with label
1514
+ if hasattr(col, "compile") and hasattr(col, "name") and col.name:
1515
+ # Special handling for DISTINCT function to avoid reserved keyword issues
1516
+ if hasattr(col, "name") and col.name.upper() == "DISTINCT":
1517
+ # For DISTINCT functions, use the original logic to extract column name
1518
+ col_str = str(col.compile(compile_kwargs={"literal_binds": True}))
1519
+ if "(" in col_str and ")" in col_str:
1520
+ func_name = col_str.split("(")[0].strip()
1521
+ col_name = col_str.split("(")[1].split(")")[0].strip()
1522
+ col_name = col_name.strip("'\"")
1523
+ select_cols.append(f"DISTINCT_{col_name}")
1524
+ else:
1525
+ select_cols.append(col.name)
1526
+ else:
1527
+ # SQLAlchemy function with label - use the label name
1528
+ select_cols.append(col.name)
1529
+ else:
1530
+ # Convert SQLAlchemy function objects to strings first
1531
+ if hasattr(col, "compile"):
1532
+ col_str = str(col.compile(compile_kwargs={"literal_binds": True}))
1533
+ else:
1534
+ col_str = str(col)
1535
+
1536
+ if " as " in col_str.lower():
1537
+ # Handle "column as alias" syntax - use case-insensitive split
1538
+ parts = col_str.lower().split(" as ")
1539
+ if len(parts) == 2:
1540
+ # Find the actual alias in the original string
1541
+ as_index = col_str.lower().find(" as ")
1542
+ alias = col_str[as_index + 4 :].strip()
1543
+ # Remove quotes from alias if present
1544
+ alias = alias.strip("'\"")
1545
+ select_cols.append(alias)
1546
+ else:
1547
+ # Fallback to original logic
1548
+ alias = col_str.split(" as ")[-1].strip()
1549
+ alias = alias.strip("'\"")
1550
+ select_cols.append(alias)
1551
+ else:
1552
+ # Handle function calls like "COUNT(id)" or "DISTINCT category"
1553
+ if "(" in col_str and ")" in col_str:
1554
+ # Extract the function name and column
1555
+ func_name = col_str.split("(")[0].strip()
1556
+ col_name = col_str.split("(")[1].split(")")[0].strip()
1557
+ # Remove quotes from column name
1558
+ col_name = col_name.strip("'\"")
1559
+ # Handle special cases
1560
+ if func_name.upper() == "DISTINCT":
1561
+ attr_name = f"DISTINCT_{col_name}"
1562
+ else:
1563
+ attr_name = f"{func_name.upper()}_{col_name}"
1564
+ select_cols.append(attr_name)
1565
+ else:
1566
+ # Handle table aliases in column names (e.g., "u.name" -> "name")
1567
+ if "." in col_str and not col_str.startswith("("):
1568
+ # Extract column name after the dot for attribute access
1569
+ col_name = col_str.split(".")[-1]
1570
+ select_cols.append(col_name)
1571
+ else:
1572
+ # For simple column names, use as-is
1573
+ select_cols.append(col_str)
1574
+ return select_cols
1575
+
1576
+ def update(self, **kwargs) -> "BaseMatrixOneQuery":
1577
+ """
1578
+ Start UPDATE operation - SQLAlchemy style
1579
+
1580
+ This method allows you to update records in the database using a fluent interface
1581
+ similar to SQLAlchemy's update() method. It supports both SQLAlchemy expressions
1582
+ and simple key-value pairs for setting column values.
1583
+
1584
+ Args::
1585
+
1586
+ **kwargs: Column names and their new values to set
1587
+
1588
+ Returns::
1589
+
1590
+ Self for method chaining
1591
+
1592
+ Examples::
1593
+
1594
+ # Update with simple key-value pairs
1595
+ query = client.query(User)
1596
+ query.update(name="New Name", email="new@example.com").filter(User.id == 1).execute()
1597
+
1598
+ # Update with SQLAlchemy expressions
1599
+ from sqlalchemy import func
1600
+ query = client.query(User)
1601
+ query.update(
1602
+ last_login=func.now(),
1603
+ login_count=User.login_count + 1
1604
+ ).filter(User.id == 1).execute()
1605
+
1606
+ # Update multiple records with conditions
1607
+ query = client.query(User)
1608
+ query.update(status="inactive").filter(User.last_login < "2023-01-01").execute()
1609
+
1610
+ # Update with complex conditions
1611
+ query = client.query(User)
1612
+ query.update(
1613
+ status="premium",
1614
+ premium_until=func.date_add(func.now(), func.interval(1, "YEAR"))
1615
+ ).filter(
1616
+ User.subscription_type == "paid",
1617
+ User.payment_status == "active"
1618
+ ).execute()
1619
+ """
1620
+ self._query_type = "UPDATE"
1621
+
1622
+ # Handle both SQLAlchemy expressions and simple values
1623
+ for key, value in kwargs.items():
1624
+ if hasattr(value, "compile"): # SQLAlchemy expression
1625
+ # Compile the expression to SQL
1626
+ compiled = value.compile(compile_kwargs={"literal_binds": True})
1627
+ sql_str = str(compiled)
1628
+
1629
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
1630
+ import re
1631
+
1632
+ sql_str = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", sql_str)
1633
+
1634
+ self._update_set_columns.append(f"{key} = {sql_str}")
1635
+ else: # Simple value
1636
+ self._update_set_columns.append(f"{key} = ?")
1637
+ self._update_set_values.append(value)
1638
+
1639
+ return self
1640
+
1641
+ def _build_update_sql(self) -> tuple[str, List[Any]]:
1642
+ """
1643
+ Build UPDATE SQL query directly to handle SQLAlchemy expressions
1644
+
1645
+ Returns::
1646
+
1647
+ Tuple of (SQL string, parameters list)
1648
+
1649
+ Raises::
1650
+
1651
+ ValueError: If no SET clauses are provided for UPDATE
1652
+ """
1653
+ if not self._update_set_columns:
1654
+ raise ValueError("No SET clauses provided for UPDATE")
1655
+
1656
+ # Build SET clause
1657
+ set_clauses = []
1658
+ params = []
1659
+
1660
+ value_index = 0
1661
+ for col in self._update_set_columns:
1662
+ if " = ?" in col:
1663
+ # Simple value assignment
1664
+ col_name = col.split(" = ?")[0]
1665
+ set_clauses.append(f"{col_name} = ?")
1666
+ params.append(self._update_set_values[value_index])
1667
+ value_index += 1
1668
+ else:
1669
+ # SQLAlchemy expression (already compiled to SQL)
1670
+ set_clauses.append(col)
1671
+
1672
+ # Build WHERE clause
1673
+ where_clause = ""
1674
+ if self._where_conditions:
1675
+ where_clause = " WHERE " + " AND ".join(self._where_conditions)
1676
+ params.extend(self._where_params)
1677
+
1678
+ # Build final SQL
1679
+ sql = f"UPDATE {self._table_name} SET {', '.join(set_clauses)}{where_clause}"
1680
+
1681
+ return sql, params
1682
+
1683
+ def delete(self) -> Any:
1684
+ """Execute DELETE operation"""
1685
+ self._query_type = "DELETE"
1686
+ sql, params = self._build_delete_sql()
1687
+ return self._execute(sql, params)
1688
+
1689
+ def _build_delete_sql(self) -> tuple[str, List[Any]]:
1690
+ """Build DELETE SQL query using unified SQL builder"""
1691
+ from .sql_builder import build_delete_query
1692
+
1693
+ return build_delete_query(
1694
+ table_name=self._table_name,
1695
+ where_conditions=self._where_conditions,
1696
+ where_params=self._where_params,
1697
+ )
1698
+
1699
+
1700
+ # MatrixOne Snapshot Query Builder - SQLAlchemy style
1701
+ class MatrixOneQuery(BaseMatrixOneQuery):
1702
+ """
1703
+ MatrixOne Query builder that mimics SQLAlchemy Query interface.
1704
+
1705
+ This class provides full SQLAlchemy compatibility including:
1706
+ - SQLAlchemy expression support in having(), filter(), and other methods
1707
+ - Type-safe column references and function calls
1708
+ - Automatic SQL generation and parameter binding
1709
+ - Full integration with SQLAlchemy models and functions
1710
+
1711
+ Key Features:
1712
+
1713
+ - Supports SQLAlchemy expressions (e.g., func.count(User.id) > 5)
1714
+ - Supports traditional string conditions (e.g., "COUNT(*) > ?", 5)
1715
+ - Automatic column name resolution and SQL generation
1716
+ - Method chaining for fluent query building
1717
+ - Full compatibility with SQLAlchemy 1.4+ and 2.0+
1718
+
1719
+ Examples
1720
+ # SQLAlchemy expression syntax (recommended)
1721
+ query = client.query(User)
1722
+ query.group_by(User.department)
1723
+ query.having(func.count(User.id) > 5)
1724
+ query.having(func.avg(User.age) > 25)
1725
+
1726
+ # String-based syntax (also supported)
1727
+ query.having("COUNT(*) > ?", 5)
1728
+ query.having("AVG(age) > ?", 25)
1729
+
1730
+ # Mixed syntax
1731
+ query.having(func.count(User.id) > 5) # Expression
1732
+ query.having("AVG(age) > ?", 25) # String
1733
+ """
1734
+
1735
+ def __init__(self, model_class, client, transaction_wrapper=None, snapshot=None):
1736
+ super().__init__(model_class, client, transaction_wrapper, snapshot)
1737
+
1738
+ def with_cte(self, *ctes) -> "MatrixOneQuery":
1739
+ """Add CTEs to this query - SQLAlchemy style
1740
+
1741
+ Args::
1742
+
1743
+ *ctes: CTE objects to add to the query
1744
+
1745
+ Returns::
1746
+
1747
+ Self for method chaining
1748
+
1749
+ Examples::
1750
+
1751
+ # Add a single CTE
1752
+ user_stats = client.query(User).filter(User.active == True).cte("user_stats")
1753
+ result = client.query(Article).with_cte(user_stats).join(user_stats, Article.user_id == user_stats.id).all()
1754
+
1755
+ # Add multiple CTEs
1756
+ result = client.query(Article).with_cte(user_stats, category_stats).all()
1757
+ """
1758
+ for cte in ctes:
1759
+ if isinstance(cte, CTE):
1760
+ self._ctes.append(cte)
1761
+ else:
1762
+ raise ValueError("All arguments must be CTE objects")
1763
+ return self
1764
+
1765
+ def all(self) -> List:
1766
+ """Execute query and return all results - SQLAlchemy style"""
1767
+ sql, params = self._build_sql()
1768
+ result = self._execute(sql, params)
1769
+
1770
+ models = []
1771
+ for row in result.rows:
1772
+ # Check if this is an aggregate query (has custom select columns)
1773
+ if self._select_columns:
1774
+ # For aggregate queries, return raw row data as a simple object
1775
+ select_cols = self._extract_select_columns()
1776
+ row_data = self._create_row_data(row, select_cols)
1777
+ models.append(row_data)
1778
+ else:
1779
+ # Regular model query
1780
+ if self._is_sqlalchemy_model:
1781
+ # For SQLAlchemy models, create instance directly
1782
+ row_dict = {}
1783
+ for i, col_name in enumerate(self._columns.keys()):
1784
+ if i < len(row):
1785
+ row_dict[col_name] = row[i]
1786
+
1787
+ # Create SQLAlchemy model instance
1788
+ model = self.model_class(**row_dict)
1789
+ models.append(model)
1790
+ else:
1791
+ # For non-SQLAlchemy models, create instance directly
1792
+ row_dict = {}
1793
+ for i, col_name in enumerate(self._columns.keys()):
1794
+ if i < len(row):
1795
+ row_dict[col_name] = row[i]
1796
+
1797
+ # Create model instance
1798
+ model = self.model_class(**row_dict)
1799
+ models.append(model)
1800
+
1801
+ return models
1802
+
1803
+ def first(self) -> Optional:
1804
+ """Execute query and return first result - SQLAlchemy style"""
1805
+ self._limit_count = 1
1806
+ results = self.all()
1807
+ return results[0] if results else None
1808
+
1809
+ def one(self):
1810
+ """
1811
+ Execute query and return exactly one result - SQLAlchemy style.
1812
+
1813
+ This method executes the query and expects exactly one row to be returned.
1814
+ If no rows are found or multiple rows are found, it raises appropriate exceptions.
1815
+ This method provides SQLAlchemy-compatible behavior for MatrixOne queries.
1816
+
1817
+ Returns::
1818
+ Model instance: The single result row as a model instance.
1819
+
1820
+ Raises::
1821
+ NoResultFound: If no results are found.
1822
+ MultipleResultsFound: If more than one result is found.
1823
+
1824
+ Examples::
1825
+
1826
+ # Get a user by unique ID using SQLAlchemy expressions
1827
+ from sqlalchemy import and_
1828
+ user = client.query(User).filter(and_(User.id == 1, User.active == True)).one()
1829
+
1830
+ # Get a user by unique email with complex conditions
1831
+ user = client.query(User).filter(User.email == "admin@example.com").one()
1832
+
1833
+ Notes::
1834
+ - Use this method when you expect exactly one result
1835
+ - For cases where zero or one result is acceptable, use one_or_none()
1836
+ - For cases where multiple results are acceptable, use all() or first()
1837
+ - This method supports SQLAlchemy expressions and operators
1838
+ """
1839
+ results = self.all()
1840
+ if len(results) == 0:
1841
+ from sqlalchemy.exc import NoResultFound
1842
+
1843
+ raise NoResultFound("No row was found for one()")
1844
+ elif len(results) > 1:
1845
+ from sqlalchemy.exc import MultipleResultsFound
1846
+
1847
+ raise MultipleResultsFound("Multiple rows were found for one()")
1848
+ return results[0]
1849
+
1850
+ def one_or_none(self):
1851
+ """
1852
+ Execute query and return exactly one result or None - SQLAlchemy style.
1853
+
1854
+ This method executes the query and returns exactly one row if found,
1855
+ or None if no rows are found. If multiple rows are found, it raises an exception.
1856
+ This method provides SQLAlchemy-compatible behavior for MatrixOne queries.
1857
+
1858
+ Returns::
1859
+ Model instance or None: The single result row as a model instance,
1860
+ or None if no results are found.
1861
+
1862
+ Raises::
1863
+ MultipleResultsFound: If more than one result is found.
1864
+
1865
+ Examples::
1866
+
1867
+ # Get a user by ID, return None if not found
1868
+ from sqlalchemy import and_
1869
+ user = client.query(User).filter(and_(User.id == 999, User.active == True)).one_or_none()
1870
+ if user:
1871
+ print(f"Found user: {user.name}")
1872
+
1873
+ # Get a user by email with complex conditions
1874
+ user = client.query(User).filter(User.email == "nonexistent@example.com").one_or_none()
1875
+ if user is None:
1876
+ print("User not found")
1877
+
1878
+ Notes::
1879
+ - Use this method when zero or one result is acceptable
1880
+ - For cases where exactly one result is required, use one()
1881
+ - For cases where multiple results are acceptable, use all() or first()
1882
+ - This method supports SQLAlchemy expressions and operators
1883
+ """
1884
+ results = self.all()
1885
+ if len(results) == 0:
1886
+ return None
1887
+ elif len(results) > 1:
1888
+ from sqlalchemy.exc import MultipleResultsFound
1889
+
1890
+ raise MultipleResultsFound("Multiple rows were found for one_or_none()")
1891
+ return results[0]
1892
+
1893
+ def scalar(self):
1894
+ """
1895
+ Execute query and return the first column of the first result - SQLAlchemy style.
1896
+
1897
+ This method executes the query and returns the value of the first column
1898
+ from the first row, or None if no results are found. This is useful for
1899
+ getting single values like counts, sums, or specific column values.
1900
+ This method provides SQLAlchemy-compatible behavior for MatrixOne queries.
1901
+
1902
+ Returns::
1903
+ Any or None: The value of the first column from the first row,
1904
+ or None if no results are found.
1905
+
1906
+ Examples::
1907
+
1908
+ # Get the count of all users using SQLAlchemy functions
1909
+ from sqlalchemy import func
1910
+ count = client.query(User).select(func.count(User.id)).scalar()
1911
+
1912
+ # Get the name of the first user
1913
+ name = client.query(User).select(User.name).first().scalar()
1914
+
1915
+ # Get the maximum age with complex conditions
1916
+ max_age = client.query(User).select(func.max(User.age)).filter(User.active == True).scalar()
1917
+
1918
+ # Get a specific user's name by ID
1919
+ name = client.query(User).select(User.name).filter(User.id == 1).scalar()
1920
+
1921
+ Notes::
1922
+ - This method is particularly useful with aggregate functions
1923
+ - For custom select queries, returns the first selected column value
1924
+ - For model queries, returns the first column value from the model
1925
+ - Returns None if no results are found
1926
+ - This method supports SQLAlchemy expressions and operators
1927
+ """
1928
+ result = self.first()
1929
+ if result is None:
1930
+ return None
1931
+
1932
+ # If result is a model instance, return the first column value
1933
+ if hasattr(result, '__dict__'):
1934
+ # For custom select queries, check if we have select columns
1935
+ if self._select_columns:
1936
+ # For custom select, return the first selected column value
1937
+ select_cols = self._extract_select_columns()
1938
+ if select_cols:
1939
+ first_col_name = select_cols[0]
1940
+ return getattr(result, first_col_name)
1941
+
1942
+ # Get the first column value from the model
1943
+ if hasattr(self.model_class, "__table__"):
1944
+ first_column = list(self.model_class.__table__.columns)[0]
1945
+ return getattr(result, first_column.name)
1946
+ else:
1947
+ # For raw queries, return the first attribute
1948
+ attrs = [attr for attr in dir(result) if not attr.startswith('_')]
1949
+ if attrs:
1950
+ return getattr(result, attrs[0])
1951
+ return None
1952
+ else:
1953
+ # For raw data, return the first element
1954
+ if isinstance(result, (list, tuple)) and len(result) > 0:
1955
+ return result[0]
1956
+ return result
1957
+
1958
+ def count(self) -> int:
1959
+ """Execute query and return count of results - SQLAlchemy style"""
1960
+ sql, params = self._build_sql()
1961
+ # Replace SELECT * with COUNT(*)
1962
+ sql = sql.replace("SELECT *", "SELECT COUNT(*)")
1963
+
1964
+ result = self._execute(sql, params)
1965
+ return result.rows[0][0] if result.rows else 0
1966
+
1967
+ def execute(self) -> Any:
1968
+ """Execute the query based on its type"""
1969
+ if self._query_type == "SELECT":
1970
+ sql, params = self._build_sql()
1971
+ return self._execute(sql, params)
1972
+ elif self._query_type == "INSERT":
1973
+ sql, params = self._build_insert_sql()
1974
+ return self._execute(sql, params)
1975
+ elif self._query_type == "UPDATE":
1976
+ sql, params = self._build_update_sql()
1977
+ return self._execute(sql, params)
1978
+ elif self._query_type == "DELETE":
1979
+ sql, params = self._build_delete_sql()
1980
+ return self._execute(sql, params)
1981
+ else:
1982
+ raise ValueError(f"Unknown query type: {self._query_type}")
1983
+
1984
+ def to_sql(self) -> str:
1985
+ """
1986
+ Generate the complete SQL statement for this query.
1987
+
1988
+ Returns the SQL string that would be executed, with parameters
1989
+ properly substituted for better readability.
1990
+
1991
+ Returns::
1992
+
1993
+ str: The complete SQL statement as a string.
1994
+
1995
+ Examples
1996
+
1997
+ query = client.query(User).filter(User.age > 25).order_by(User.name)
1998
+ sql = query.to_sql()
1999
+ print(sql) # "SELECT * FROM users WHERE age > 25 ORDER BY name"
2000
+
2001
+ query = client.query(User).update(name="New Name").filter(User.id == 1)
2002
+ sql = query.to_sql()
2003
+ print(sql) # "UPDATE users SET name = 'New Name' WHERE id = 1"
2004
+
2005
+ Notes:
2006
+ - This method returns the SQL with parameters substituted
2007
+ - Use this for debugging or logging purposes
2008
+ - The returned SQL is ready to be executed directly
2009
+ """
2010
+ # Build SQL based on query type
2011
+ if self._query_type == "UPDATE":
2012
+ sql, params = self._build_update_sql()
2013
+ elif self._query_type == "INSERT":
2014
+ sql, params = self._build_insert_sql()
2015
+ elif self._query_type == "DELETE":
2016
+ sql, params = self._build_delete_sql()
2017
+ else:
2018
+ sql, params = self._build_sql()
2019
+
2020
+ # Substitute parameters for better readability
2021
+ if params:
2022
+ formatted_sql = sql
2023
+ for param in params:
2024
+ if isinstance(param, str):
2025
+ formatted_sql = formatted_sql.replace("?", f"'{param}'", 1)
2026
+ else:
2027
+ formatted_sql = formatted_sql.replace("?", str(param), 1)
2028
+ return formatted_sql
2029
+ else:
2030
+ return sql
2031
+
2032
+ def explain(self, verbose: bool = False) -> Any:
2033
+ """
2034
+ Execute EXPLAIN statement for this query.
2035
+
2036
+ Shows the query execution plan without actually executing the query.
2037
+ Useful for understanding how MatrixOne will execute the query and
2038
+ optimizing query performance.
2039
+
2040
+ Args::
2041
+
2042
+ verbose (bool): Whether to include verbose output.
2043
+ Defaults to False.
2044
+
2045
+ Returns::
2046
+
2047
+ Any: The result set containing the execution plan.
2048
+
2049
+ Examples::
2050
+
2051
+ # Basic EXPLAIN
2052
+ plan = client.query(User).filter(User.age > 25).explain()
2053
+
2054
+ # EXPLAIN with verbose output
2055
+ plan = client.query(User).filter(User.age > 25).explain(verbose=True)
2056
+
2057
+ # EXPLAIN for complex queries
2058
+ plan = (client.query(User)
2059
+ .filter(User.department == 'Engineering')
2060
+ .order_by(User.salary.desc())
2061
+ .explain(verbose=True))
2062
+
2063
+ Notes:
2064
+ - EXPLAIN shows the execution plan without executing the query
2065
+ - Use verbose=True for more detailed information
2066
+ - Helpful for query optimization and performance tuning
2067
+ """
2068
+ sql, params = self._build_sql()
2069
+
2070
+ # Build EXPLAIN statement
2071
+ if verbose:
2072
+ explain_sql = f"EXPLAIN VERBOSE {sql}"
2073
+ else:
2074
+ explain_sql = f"EXPLAIN {sql}"
2075
+
2076
+ return self._execute(explain_sql, params)
2077
+
2078
+ def explain_analyze(self, verbose: bool = False) -> Any:
2079
+ """
2080
+ Execute EXPLAIN ANALYZE statement for this query.
2081
+
2082
+ Shows the query execution plan and actually executes the query,
2083
+ providing both the plan and actual execution statistics.
2084
+ Useful for understanding query performance with real data.
2085
+
2086
+ Args::
2087
+
2088
+ verbose (bool): Whether to include verbose output.
2089
+ Defaults to False.
2090
+
2091
+ Returns::
2092
+
2093
+ Any: The result set containing the execution plan and statistics.
2094
+
2095
+ Examples::
2096
+
2097
+ # Basic EXPLAIN ANALYZE
2098
+ result = client.query(User).filter(User.age > 25).explain_analyze()
2099
+
2100
+ # EXPLAIN ANALYZE with verbose output
2101
+ result = client.query(User).filter(User.age > 25).explain_analyze(verbose=True)
2102
+
2103
+ # EXPLAIN ANALYZE for complex queries
2104
+ result = (client.query(User)
2105
+ .filter(User.department == 'Engineering')
2106
+ .order_by(User.salary.desc())
2107
+ .explain_analyze(verbose=True))
2108
+
2109
+ Notes:
2110
+ - EXPLAIN ANALYZE actually executes the query and shows statistics
2111
+ - Use verbose=True for more detailed information
2112
+ - Provides actual execution time and row counts
2113
+ - Use with caution on large datasets as it executes the full query
2114
+ """
2115
+ sql, params = self._build_sql()
2116
+
2117
+ # Build EXPLAIN ANALYZE statement
2118
+ if verbose:
2119
+ explain_sql = f"EXPLAIN ANALYZE VERBOSE {sql}"
2120
+ else:
2121
+ explain_sql = f"EXPLAIN ANALYZE {sql}"
2122
+
2123
+ return self._execute(explain_sql, params)
2124
+
2125
+
2126
+ # Helper functions for ORDER BY
2127
+ def desc(column: str) -> str:
2128
+ """Create descending order clause"""
2129
+ return f"{column} DESC"
2130
+
2131
+
2132
+ def asc(column: str) -> str:
2133
+ """Create ascending order clause"""
2134
+ return f"{column} ASC"
2135
+
2136
+
2137
+ class LogicalIn:
2138
+ """
2139
+ Helper class for creating IN conditions that can be used in filter() method.
2140
+
2141
+ This class provides a way to create IN conditions with support for various
2142
+ value types including FulltextFilter objects, lists, and SQLAlchemy expressions.
2143
+
2144
+ Usage
2145
+ # List of values
2146
+ query.filter(logical_in("city", ["北京", "上海", "广州"]))
2147
+ query.filter(logical_in(User.id, [1, 2, 3, 4]))
2148
+
2149
+ # FulltextFilter
2150
+ query.filter(logical_in("id", boolean_match("title", "content").must("python")))
2151
+
2152
+ # Subquery
2153
+ subquery = client.query(User).select(User.id).filter(User.active == True)
2154
+ query.filter(logical_in("author_id", subquery))
2155
+ """
2156
+
2157
+ def __init__(self, column, values):
2158
+ self.column = column
2159
+ self.values = values
2160
+
2161
+ def compile(self, compile_kwargs=None):
2162
+ """Compile to SQL expression for use in filter() method"""
2163
+ # Handle column name
2164
+ if hasattr(self.column, "name"):
2165
+ column_name = self.column.name
2166
+ else:
2167
+ column_name = str(self.column)
2168
+
2169
+ # Handle different types of values
2170
+ if hasattr(self.values, "compile") and hasattr(self.values, "columns"):
2171
+ # This is a FulltextFilter object
2172
+ if hasattr(self.values, "compile") and hasattr(self.values, "query_builder"):
2173
+ # Convert FulltextFilter to subquery
2174
+ fulltext_sql = self.values.compile()
2175
+ # Create a subquery that selects IDs from the fulltext search
2176
+ # We need to determine the table name from the context
2177
+ # For now, use a generic approach that works with most cases
2178
+ table_name = "table" # Default table name - will be handled by the query context
2179
+ subquery_sql = f"SELECT id FROM {table_name} WHERE {fulltext_sql}"
2180
+ return f"{column_name} IN ({subquery_sql})"
2181
+ else:
2182
+ # Handle other SQLAlchemy expressions
2183
+ compiled = self.values.compile(compile_kwargs={"literal_binds": True})
2184
+ sql_str = str(compiled)
2185
+ # Fix SQLAlchemy's quoted column names for MatrixOne compatibility
2186
+ import re
2187
+
2188
+ sql_str = re.sub(r"(\w+)\('([^']+)'\)", r"\1(\2)", sql_str)
2189
+ sql_str = re.sub(r"\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b", r"\2", sql_str)
2190
+ return f"{column_name} IN ({sql_str})"
2191
+ elif hasattr(self.values, "_build_sql"):
2192
+ # This is a subquery object
2193
+ subquery_sql, subquery_params = self.values._build_sql()
2194
+ return f"{column_name} IN ({subquery_sql})"
2195
+ elif isinstance(self.values, (list, tuple)):
2196
+ # Handle list of values
2197
+ if not self.values:
2198
+ # Empty list means no matches
2199
+ return "1=0" # Always false
2200
+ else:
2201
+ # Create SQL with actual values for LogicalIn
2202
+ formatted_values = []
2203
+ for value in self.values:
2204
+ if isinstance(value, str):
2205
+ formatted_values.append(f"'{value}'")
2206
+ else:
2207
+ formatted_values.append(str(value))
2208
+ return f"{column_name} IN ({','.join(formatted_values)})"
2209
+ else:
2210
+ # Single value
2211
+ if isinstance(self.values, str):
2212
+ return f"{column_name} IN ('{self.values}')"
2213
+ else:
2214
+ return f"{column_name} IN ({self.values})"
2215
+
2216
+
2217
+ def logical_in(column, values):
2218
+ """
2219
+ Create a logical IN condition for use in filter() method.
2220
+
2221
+ This function provides enhanced IN functionality that can handle various
2222
+ types of values and expressions, making it more flexible than standard
2223
+ SQLAlchemy IN operations. It automatically generates appropriate SQL
2224
+ based on the input type.
2225
+
2226
+ Key Features:
2227
+
2228
+ - Support for lists of values with automatic SQL generation
2229
+ - Integration with SQLAlchemy expressions
2230
+ - FulltextFilter support for complex search conditions
2231
+ - Subquery support for dynamic value sets
2232
+ - Automatic parameter binding and SQL injection prevention
2233
+
2234
+ Args::
2235
+
2236
+ column: Column to check against. Can be:
2237
+ - String column name: "user_id"
2238
+ - SQLAlchemy column: User.id
2239
+ - Column expression: func.upper(User.name)
2240
+ values: Values to check against. Can be:
2241
+ - List of values: [1, 2, 3, "a", "b"]
2242
+ - Single value: 42 or "test"
2243
+ - SQLAlchemy expression: func.count(User.id)
2244
+ - FulltextFilter object: boolean_match("title", "content").must("python")
2245
+ - Subquery object: client.query(User).select(User.id)
2246
+ - None: Creates "column IN (NULL)" condition
2247
+
2248
+ Returns::
2249
+
2250
+ LogicalIn: A logical IN condition object that can be used in filter().
2251
+ The object automatically compiles to appropriate SQL when used.
2252
+
2253
+ Examples
2254
+ # List of values - generates: WHERE city IN ('北京', '上海', '广州')
2255
+ query.filter(logical_in("city", ["北京", "上海", "广州"]))
2256
+ query.filter(logical_in(User.id, [1, 2, 3, 4]))
2257
+
2258
+ # Single value - generates: WHERE id IN (42)
2259
+ query.filter(logical_in("id", 42))
2260
+
2261
+ # SQLAlchemy expression - generates: WHERE id IN (SELECT COUNT(*) FROM users)
2262
+ query.filter(logical_in("id", func.count(User.id)))
2263
+
2264
+ # FulltextFilter - generates: WHERE id IN (SELECT id FROM table WHERE MATCH(...))
2265
+ query.filter(logical_in(User.id, boolean_match("title", "content").must("python")))
2266
+
2267
+ # Subquery - generates: WHERE user_id IN (SELECT id FROM active_users)
2268
+ active_user_ids = client.query(User).select(User.id).filter(User.active == True)
2269
+ query.filter(logical_in("user_id", active_user_ids))
2270
+
2271
+ # NULL value - generates: WHERE id IN (NULL)
2272
+ query.filter(logical_in("id", None))
2273
+
2274
+ Note: This function is designed to work seamlessly with MatrixOne's query
2275
+ builder and provides better integration than standard SQLAlchemy IN operations.
2276
+ """
2277
+ return LogicalIn(column, values)