matrixone-python-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
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)
|