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
@@ -0,0 +1,346 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
# Copyright 2021 - 2022 Matrix Origin
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
"""
|
18
|
+
Test: EXPLAIN, EXPLAIN ANALYZE, and to_sql() methods
|
19
|
+
|
20
|
+
This test verifies that the explain methods in MatrixOneQuery work correctly
|
21
|
+
for analyzing query execution plans and generating SQL statements.
|
22
|
+
"""
|
23
|
+
|
24
|
+
import pytest
|
25
|
+
import sys
|
26
|
+
import os
|
27
|
+
from datetime import datetime
|
28
|
+
|
29
|
+
# Add the current directory to Python path
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
31
|
+
|
32
|
+
from matrixone import Client
|
33
|
+
from sqlalchemy import Column, Integer, String, DateTime, func, desc, asc
|
34
|
+
from matrixone.orm import declarative_base
|
35
|
+
|
36
|
+
# Create SQLAlchemy models
|
37
|
+
Base = declarative_base()
|
38
|
+
|
39
|
+
|
40
|
+
class User(Base):
|
41
|
+
__tablename__ = 'users'
|
42
|
+
id = Column(Integer, primary_key=True)
|
43
|
+
name = Column(String(50))
|
44
|
+
age = Column(Integer)
|
45
|
+
department = Column(String(50))
|
46
|
+
salary = Column(Integer)
|
47
|
+
created_at = Column(DateTime)
|
48
|
+
active = Column(Integer, default=1) # Add active column for CTE test
|
49
|
+
|
50
|
+
|
51
|
+
class Product(Base):
|
52
|
+
__tablename__ = 'products'
|
53
|
+
id = Column(Integer, primary_key=True)
|
54
|
+
name = Column(String(100))
|
55
|
+
price = Column(Integer)
|
56
|
+
category = Column(String(50))
|
57
|
+
created_at = Column(DateTime)
|
58
|
+
user_id = Column(Integer) # Add user_id for CTE test
|
59
|
+
|
60
|
+
|
61
|
+
class Order(Base):
|
62
|
+
__tablename__ = 'orders'
|
63
|
+
id = Column(Integer, primary_key=True)
|
64
|
+
user_id = Column(Integer)
|
65
|
+
product_id = Column(Integer)
|
66
|
+
quantity = Column(Integer)
|
67
|
+
total_amount = Column(Integer)
|
68
|
+
created_at = Column(DateTime)
|
69
|
+
|
70
|
+
|
71
|
+
class TestExplainMethods:
|
72
|
+
"""Test class for explain methods functionality"""
|
73
|
+
|
74
|
+
def setup_method(self):
|
75
|
+
"""Set up test client"""
|
76
|
+
self.client = Client("test://localhost:6001/test")
|
77
|
+
|
78
|
+
def test_to_sql_basic_query(self):
|
79
|
+
"""Test to_sql() method with basic query"""
|
80
|
+
query = self.client.query(User).filter(User.age > 25).order_by(User.name)
|
81
|
+
sql = query.to_sql()
|
82
|
+
|
83
|
+
assert "SELECT" in sql
|
84
|
+
assert "FROM users" in sql
|
85
|
+
assert "WHERE age > 25" in sql
|
86
|
+
assert "ORDER BY name" in sql
|
87
|
+
|
88
|
+
def test_to_sql_with_parameters(self):
|
89
|
+
"""Test to_sql() method with parameters"""
|
90
|
+
query = self.client.query(User).filter("age > ?", 30).filter("department = ?", "Engineering")
|
91
|
+
sql = query.to_sql()
|
92
|
+
|
93
|
+
assert "WHERE age > 30" in sql
|
94
|
+
assert "department = 'Engineering'" in sql
|
95
|
+
|
96
|
+
def test_to_sql_complex_query(self):
|
97
|
+
"""Test to_sql() method with complex query"""
|
98
|
+
query = (
|
99
|
+
self.client.query(User)
|
100
|
+
.select(User.department, func.count(User.id), func.avg(User.salary))
|
101
|
+
.filter(User.age > 25)
|
102
|
+
.group_by(User.department)
|
103
|
+
.having(func.count(User.id) > 1)
|
104
|
+
.order_by(func.avg(User.salary).desc())
|
105
|
+
.limit(10)
|
106
|
+
)
|
107
|
+
|
108
|
+
sql = query.to_sql()
|
109
|
+
|
110
|
+
assert "SELECT" in sql
|
111
|
+
assert "FROM users" in sql
|
112
|
+
assert "WHERE age > 25" in sql
|
113
|
+
assert "GROUP BY department" in sql
|
114
|
+
assert "HAVING count(id) > 1" in sql
|
115
|
+
assert "ORDER BY avg(salary) DESC" in sql
|
116
|
+
assert "LIMIT 10" in sql
|
117
|
+
|
118
|
+
def test_to_sql_with_joins(self):
|
119
|
+
"""Test to_sql() method with joins"""
|
120
|
+
query = (
|
121
|
+
self.client.query(User)
|
122
|
+
.join(Order, User.id == Order.user_id)
|
123
|
+
.select(User.name, func.sum(Order.total_amount))
|
124
|
+
.filter(User.department == 'Sales')
|
125
|
+
.group_by(User.id, User.name)
|
126
|
+
.having(func.sum(Order.total_amount) > 10000)
|
127
|
+
.order_by(func.sum(Order.total_amount).desc())
|
128
|
+
)
|
129
|
+
|
130
|
+
sql = query.to_sql()
|
131
|
+
|
132
|
+
assert "JOIN orders" in sql
|
133
|
+
assert "WHERE department = 'Sales'" in sql
|
134
|
+
assert "GROUP BY id, name" in sql
|
135
|
+
assert "HAVING sum(total_amount) > 10000" in sql
|
136
|
+
|
137
|
+
def test_explain_sql_generation(self):
|
138
|
+
"""Test explain() method SQL generation"""
|
139
|
+
query = self.client.query(User).filter(User.department == 'Engineering').order_by(User.salary.desc())
|
140
|
+
sql, params = query._build_sql()
|
141
|
+
|
142
|
+
# Test basic EXPLAIN
|
143
|
+
explain_sql = f"EXPLAIN {sql}"
|
144
|
+
assert explain_sql.startswith("EXPLAIN")
|
145
|
+
assert "SELECT" in explain_sql
|
146
|
+
assert "FROM users" in explain_sql
|
147
|
+
assert "WHERE department = 'Engineering'" in explain_sql
|
148
|
+
assert "ORDER BY salary DESC" in explain_sql
|
149
|
+
|
150
|
+
def test_explain_verbose_sql_generation(self):
|
151
|
+
"""Test explain() method with verbose SQL generation"""
|
152
|
+
query = (
|
153
|
+
self.client.query(User)
|
154
|
+
.select(User.department, func.count(User.id), func.avg(User.salary))
|
155
|
+
.filter(User.age > 25)
|
156
|
+
.group_by(User.department)
|
157
|
+
.having(func.count(User.id) > 1)
|
158
|
+
)
|
159
|
+
|
160
|
+
sql, params = query._build_sql()
|
161
|
+
|
162
|
+
# Test EXPLAIN VERBOSE
|
163
|
+
explain_verbose_sql = f"EXPLAIN VERBOSE {sql}"
|
164
|
+
assert explain_verbose_sql.startswith("EXPLAIN VERBOSE")
|
165
|
+
assert "SELECT" in explain_verbose_sql
|
166
|
+
assert "GROUP BY department" in explain_verbose_sql
|
167
|
+
assert "HAVING count(id) > 1" in explain_verbose_sql
|
168
|
+
|
169
|
+
def test_explain_analyze_sql_generation(self):
|
170
|
+
"""Test explain_analyze() method SQL generation"""
|
171
|
+
query = self.client.query(User).filter(User.salary > 70000).order_by(User.age.desc())
|
172
|
+
sql, params = query._build_sql()
|
173
|
+
|
174
|
+
# Test EXPLAIN ANALYZE
|
175
|
+
explain_analyze_sql = f"EXPLAIN ANALYZE {sql}"
|
176
|
+
assert explain_analyze_sql.startswith("EXPLAIN ANALYZE")
|
177
|
+
assert "SELECT" in explain_analyze_sql
|
178
|
+
assert "WHERE salary > 70000" in explain_analyze_sql
|
179
|
+
assert "ORDER BY age DESC" in explain_analyze_sql
|
180
|
+
|
181
|
+
def test_explain_analyze_verbose_sql_generation(self):
|
182
|
+
"""Test explain_analyze() method with verbose SQL generation"""
|
183
|
+
query = (
|
184
|
+
self.client.query(User)
|
185
|
+
.join(Order, User.id == Order.user_id)
|
186
|
+
.select(User.name, func.sum(Order.total_amount))
|
187
|
+
.filter(User.department == 'Sales')
|
188
|
+
.group_by(User.id, User.name)
|
189
|
+
.having(func.sum(Order.total_amount) > 10000)
|
190
|
+
.order_by(func.sum(Order.total_amount).desc())
|
191
|
+
)
|
192
|
+
|
193
|
+
sql, params = query._build_sql()
|
194
|
+
|
195
|
+
# Test EXPLAIN ANALYZE VERBOSE
|
196
|
+
explain_analyze_verbose_sql = f"EXPLAIN ANALYZE VERBOSE {sql}"
|
197
|
+
assert explain_analyze_verbose_sql.startswith("EXPLAIN ANALYZE VERBOSE")
|
198
|
+
assert "SELECT" in explain_analyze_verbose_sql
|
199
|
+
assert "JOIN orders" in explain_analyze_verbose_sql
|
200
|
+
assert "GROUP BY id, name" in explain_analyze_verbose_sql
|
201
|
+
|
202
|
+
def test_explain_methods_with_cte(self):
|
203
|
+
"""Test explain methods with CTE queries"""
|
204
|
+
# Use string-based CTE instead of SQLAlchemy CTE object
|
205
|
+
query = (
|
206
|
+
self.client.query(Product)
|
207
|
+
.select(Product.name, "user_stats.department")
|
208
|
+
.filter("user_id IN (SELECT id FROM users WHERE active = 1)")
|
209
|
+
)
|
210
|
+
|
211
|
+
sql = query.to_sql()
|
212
|
+
|
213
|
+
assert "SELECT" in sql
|
214
|
+
assert "FROM products" in sql
|
215
|
+
assert "WHERE user_id IN" in sql
|
216
|
+
|
217
|
+
# Test EXPLAIN with CTE-like query
|
218
|
+
explain_sql = f"EXPLAIN {sql}"
|
219
|
+
assert explain_sql.startswith("EXPLAIN")
|
220
|
+
|
221
|
+
def test_explain_methods_with_subquery(self):
|
222
|
+
"""Test explain methods with subqueries"""
|
223
|
+
# Use string-based subquery instead of SQLAlchemy expression
|
224
|
+
subquery_sql = "SELECT id FROM users WHERE salary > 80000 ORDER BY salary DESC LIMIT 5"
|
225
|
+
|
226
|
+
query = (
|
227
|
+
self.client.query(User)
|
228
|
+
.select(User.name, User.salary)
|
229
|
+
.filter(f"id IN ({subquery_sql})")
|
230
|
+
.order_by(User.salary.desc())
|
231
|
+
)
|
232
|
+
|
233
|
+
sql = query.to_sql()
|
234
|
+
|
235
|
+
assert "SELECT" in sql
|
236
|
+
assert "WHERE id IN" in sql
|
237
|
+
|
238
|
+
# Test EXPLAIN ANALYZE with subquery
|
239
|
+
explain_analyze_sql = f"EXPLAIN ANALYZE {sql}"
|
240
|
+
assert explain_analyze_sql.startswith("EXPLAIN ANALYZE")
|
241
|
+
|
242
|
+
def test_explain_methods_parameter_handling(self):
|
243
|
+
"""Test that explain methods handle parameters correctly"""
|
244
|
+
# Test with parameters
|
245
|
+
query = self.client.query(User).filter("age > ?", 25).filter("department = ?", "Engineering")
|
246
|
+
sql, params = query._build_sql()
|
247
|
+
|
248
|
+
# Test that the SQL is generated correctly
|
249
|
+
assert "SELECT" in sql
|
250
|
+
assert "FROM users" in sql
|
251
|
+
assert "age > 25" in sql
|
252
|
+
assert "department = 'Engineering'" in sql
|
253
|
+
|
254
|
+
# Test to_sql() method (should be the same as _build_sql() with substitution)
|
255
|
+
formatted_sql = query.to_sql()
|
256
|
+
assert "age > 25" in formatted_sql
|
257
|
+
assert "department = 'Engineering'" in formatted_sql
|
258
|
+
assert formatted_sql == sql # Should be identical
|
259
|
+
|
260
|
+
def test_explain_methods_edge_cases(self):
|
261
|
+
"""Test explain methods with edge cases"""
|
262
|
+
# Empty query
|
263
|
+
query = self.client.query(User)
|
264
|
+
sql = query.to_sql()
|
265
|
+
assert "SELECT * FROM users" in sql
|
266
|
+
|
267
|
+
# Query with only LIMIT
|
268
|
+
query = self.client.query(User).limit(10)
|
269
|
+
sql = query.to_sql()
|
270
|
+
assert "LIMIT 10" in sql
|
271
|
+
|
272
|
+
# Query with only ORDER BY
|
273
|
+
query = self.client.query(User).order_by(User.name)
|
274
|
+
sql = query.to_sql()
|
275
|
+
assert "ORDER BY name" in sql
|
276
|
+
|
277
|
+
# Query with multiple ORDER BY columns
|
278
|
+
query = self.client.query(User).order_by(User.department, User.name.desc())
|
279
|
+
sql = query.to_sql()
|
280
|
+
assert "ORDER BY department, name DESC" in sql
|
281
|
+
|
282
|
+
def test_explain_methods_performance_queries(self):
|
283
|
+
"""Test explain methods with performance-focused queries"""
|
284
|
+
# Complex aggregation query
|
285
|
+
query = (
|
286
|
+
self.client.query(User)
|
287
|
+
.select(
|
288
|
+
User.department,
|
289
|
+
func.count(User.id).label('user_count'),
|
290
|
+
func.avg(User.salary).label('avg_salary'),
|
291
|
+
func.max(User.salary).label('max_salary'),
|
292
|
+
func.min(User.salary).label('min_salary'),
|
293
|
+
)
|
294
|
+
.filter(User.age > 25)
|
295
|
+
.group_by(User.department)
|
296
|
+
.having(func.count(User.id) > 1)
|
297
|
+
.order_by(func.avg(User.salary).desc())
|
298
|
+
.limit(20)
|
299
|
+
)
|
300
|
+
|
301
|
+
sql = query.to_sql()
|
302
|
+
|
303
|
+
assert "count(users.id) AS user_count" in sql
|
304
|
+
assert "avg(users.salary) AS avg_salary" in sql
|
305
|
+
assert "max(users.salary) AS max_salary" in sql
|
306
|
+
assert "min(users.salary) AS min_salary" in sql
|
307
|
+
assert "GROUP BY department" in sql
|
308
|
+
assert "HAVING count(id) > 1" in sql
|
309
|
+
assert "ORDER BY avg(salary) DESC" in sql
|
310
|
+
assert "LIMIT 20" in sql
|
311
|
+
|
312
|
+
# Test EXPLAIN ANALYZE for performance analysis
|
313
|
+
explain_analyze_sql = f"EXPLAIN ANALYZE {sql}"
|
314
|
+
assert explain_analyze_sql.startswith("EXPLAIN ANALYZE")
|
315
|
+
|
316
|
+
def test_explain_methods_with_functions(self):
|
317
|
+
"""Test explain methods with various SQL functions"""
|
318
|
+
query = (
|
319
|
+
self.client.query(User)
|
320
|
+
.select(
|
321
|
+
func.year(User.created_at).label('year'),
|
322
|
+
func.month(User.created_at).label('month'),
|
323
|
+
func.count(User.id).label('user_count'),
|
324
|
+
)
|
325
|
+
.filter(func.year(User.created_at) > 2020)
|
326
|
+
.group_by(func.year(User.created_at), func.month(User.created_at))
|
327
|
+
.order_by(func.year(User.created_at).desc(), func.month(User.created_at).desc())
|
328
|
+
)
|
329
|
+
|
330
|
+
sql = query.to_sql()
|
331
|
+
|
332
|
+
assert "year(users.created_at) AS year" in sql
|
333
|
+
assert "month(users.created_at) AS month" in sql
|
334
|
+
assert "count(users.id) AS user_count" in sql
|
335
|
+
assert "WHERE year(created_at) > 2020" in sql
|
336
|
+
assert "GROUP BY year(created_at), month(created_at)" in sql
|
337
|
+
assert "ORDER BY year(created_at) DESC, month(created_at) DESC" in sql
|
338
|
+
|
339
|
+
# Test EXPLAIN VERBOSE for function analysis
|
340
|
+
explain_verbose_sql = f"EXPLAIN VERBOSE {sql}"
|
341
|
+
assert explain_verbose_sql.startswith("EXPLAIN VERBOSE")
|
342
|
+
|
343
|
+
|
344
|
+
if __name__ == "__main__":
|
345
|
+
# Run tests if executed directly
|
346
|
+
pytest.main([__file__, "-v"])
|
@@ -0,0 +1,237 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
# Copyright 2021 - 2022 Matrix Origin
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
"""
|
18
|
+
Test: filter(logical_in(...)) syntax for enhanced IN functionality
|
19
|
+
|
20
|
+
This test verifies that the filter(logical_in(...)) syntax works correctly with various
|
21
|
+
value types including FulltextFilter objects, lists, and SQLAlchemy expressions.
|
22
|
+
"""
|
23
|
+
|
24
|
+
import pytest
|
25
|
+
import sys
|
26
|
+
import os
|
27
|
+
from datetime import datetime
|
28
|
+
|
29
|
+
# Add the current directory to Python path
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
31
|
+
|
32
|
+
from matrixone import Client
|
33
|
+
from sqlalchemy import Column, Integer, String, DateTime, func
|
34
|
+
from matrixone.orm import declarative_base, logical_in
|
35
|
+
from matrixone.sqlalchemy_ext.fulltext_search import boolean_match
|
36
|
+
|
37
|
+
# Create SQLAlchemy models
|
38
|
+
Base = declarative_base()
|
39
|
+
|
40
|
+
|
41
|
+
class User(Base):
|
42
|
+
__tablename__ = 'users'
|
43
|
+
id = Column(Integer, primary_key=True)
|
44
|
+
name = Column(String(50))
|
45
|
+
age = Column(Integer)
|
46
|
+
city = Column(String(50))
|
47
|
+
department = Column(String(50))
|
48
|
+
salary = Column(Integer)
|
49
|
+
created_at = Column(DateTime)
|
50
|
+
|
51
|
+
|
52
|
+
class Article(Base):
|
53
|
+
__tablename__ = 'articles'
|
54
|
+
id = Column(Integer, primary_key=True)
|
55
|
+
title = Column(String(100))
|
56
|
+
content = Column(String(500))
|
57
|
+
author = Column(String(50))
|
58
|
+
created_at = Column(DateTime)
|
59
|
+
|
60
|
+
|
61
|
+
class TestFilterLogicalIn:
|
62
|
+
"""Test class for filter(logical_in(...)) syntax functionality"""
|
63
|
+
|
64
|
+
def setup_method(self):
|
65
|
+
"""Set up test client"""
|
66
|
+
self.client = Client("test://localhost:6001/test")
|
67
|
+
|
68
|
+
def test_filter_logical_in_with_list_values(self):
|
69
|
+
"""Test filter(logical_in(...)) with list of values"""
|
70
|
+
# Test with integer list
|
71
|
+
query1 = self.client.query(User).filter(logical_in(User.id, [1, 2, 3, 4]))
|
72
|
+
sql1 = query1.to_sql()
|
73
|
+
|
74
|
+
assert "SELECT" in sql1
|
75
|
+
assert "FROM users" in sql1
|
76
|
+
assert "WHERE id IN (1,2,3,4)" in sql1
|
77
|
+
|
78
|
+
# Test with string list
|
79
|
+
query2 = self.client.query(User).filter(logical_in("city", ["北京", "上海", "广州"]))
|
80
|
+
sql2 = query2.to_sql()
|
81
|
+
|
82
|
+
assert "WHERE city IN ('北京','上海','广州')" in sql2
|
83
|
+
|
84
|
+
# Test with empty list
|
85
|
+
query3 = self.client.query(User).filter(logical_in(User.id, []))
|
86
|
+
sql3 = query3.to_sql()
|
87
|
+
|
88
|
+
assert "WHERE 1=0" in sql3 # Empty list should result in always false condition
|
89
|
+
|
90
|
+
def test_filter_logical_in_with_single_value(self):
|
91
|
+
"""Test filter(logical_in(...)) with single value"""
|
92
|
+
query = self.client.query(User).filter(logical_in(User.id, 5))
|
93
|
+
sql = query.to_sql()
|
94
|
+
|
95
|
+
assert "WHERE id IN (5)" in sql
|
96
|
+
|
97
|
+
def test_filter_logical_in_with_fulltext_filter(self):
|
98
|
+
"""Test filter(logical_in(...)) with FulltextFilter objects"""
|
99
|
+
# Basic boolean_match
|
100
|
+
query1 = self.client.query(Article).filter(logical_in(Article.id, boolean_match("title", "content").must("python")))
|
101
|
+
sql1 = query1.to_sql()
|
102
|
+
|
103
|
+
assert "SELECT" in sql1
|
104
|
+
assert "FROM articles" in sql1
|
105
|
+
assert "WHERE id IN (SELECT id FROM table WHERE MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE))" in sql1
|
106
|
+
|
107
|
+
# Complex boolean_match
|
108
|
+
query2 = self.client.query(Article).filter(
|
109
|
+
logical_in(
|
110
|
+
Article.id, boolean_match("title", "content").must("python").encourage("programming").must_not("java")
|
111
|
+
)
|
112
|
+
)
|
113
|
+
sql2 = query2.to_sql()
|
114
|
+
|
115
|
+
assert (
|
116
|
+
"WHERE id IN (SELECT id FROM table WHERE MATCH(title, content) AGAINST('+python programming -java' IN BOOLEAN MODE))"
|
117
|
+
in sql2
|
118
|
+
)
|
119
|
+
|
120
|
+
def test_filter_logical_in_with_subquery(self):
|
121
|
+
"""Test filter(logical_in(...)) with subquery objects"""
|
122
|
+
# Create a subquery
|
123
|
+
subquery = self.client.query(User).select(User.id).filter(User.age > 25)
|
124
|
+
|
125
|
+
query = self.client.query(Article).filter(logical_in(Article.author, subquery))
|
126
|
+
sql = query.to_sql()
|
127
|
+
|
128
|
+
assert "WHERE author IN (SELECT users.id AS id FROM users WHERE age > 25)" in sql
|
129
|
+
|
130
|
+
def test_filter_logical_in_with_sqlalchemy_expressions(self):
|
131
|
+
"""Test filter(logical_in(...)) with SQLAlchemy expressions"""
|
132
|
+
# Test with function expression
|
133
|
+
query = self.client.query(User).filter(logical_in("id", func.count(User.id)))
|
134
|
+
sql = query.to_sql()
|
135
|
+
|
136
|
+
assert "WHERE id IN (count(id))" in sql
|
137
|
+
|
138
|
+
def test_filter_logical_in_with_string_column(self):
|
139
|
+
"""Test filter(logical_in(...)) with string column names"""
|
140
|
+
query = self.client.query(User).filter(logical_in("department", ["Engineering", "Sales", "Marketing"]))
|
141
|
+
sql = query.to_sql()
|
142
|
+
|
143
|
+
assert "WHERE department IN ('Engineering','Sales','Marketing')" in sql
|
144
|
+
|
145
|
+
def test_filter_logical_in_with_sqlalchemy_column(self):
|
146
|
+
"""Test filter(logical_in(...)) with SQLAlchemy column objects"""
|
147
|
+
query = self.client.query(User).filter(logical_in(User.department, ["Engineering", "Sales", "Marketing"]))
|
148
|
+
sql = query.to_sql()
|
149
|
+
|
150
|
+
assert "WHERE department IN ('Engineering','Sales','Marketing')" in sql
|
151
|
+
|
152
|
+
def test_filter_logical_in_combined_with_other_conditions(self):
|
153
|
+
"""Test filter(logical_in(...)) combined with other filter conditions"""
|
154
|
+
query = (
|
155
|
+
self.client.query(Article)
|
156
|
+
.filter(Article.author == "张三")
|
157
|
+
.filter(logical_in(Article.id, boolean_match("title", "content").must("python")))
|
158
|
+
.filter("created_at > ?", "2023-01-01")
|
159
|
+
)
|
160
|
+
|
161
|
+
sql = query.to_sql()
|
162
|
+
|
163
|
+
assert "WHERE author = '张三'" in sql
|
164
|
+
assert "AND id IN (SELECT id FROM table WHERE MATCH(title, content) AGAINST('+python' IN BOOLEAN MODE))" in sql
|
165
|
+
assert "AND created_at > '2023-01-01'" in sql
|
166
|
+
|
167
|
+
def test_filter_logical_in_with_tuple_values(self):
|
168
|
+
"""Test filter(logical_in(...)) with tuple values"""
|
169
|
+
query = self.client.query(User).filter(logical_in(User.id, (1, 2, 3, 4, 5)))
|
170
|
+
sql = query.to_sql()
|
171
|
+
|
172
|
+
assert "WHERE id IN (1,2,3,4,5)" in sql
|
173
|
+
|
174
|
+
def test_filter_logical_in_edge_cases(self):
|
175
|
+
"""Test filter(logical_in(...)) with edge cases"""
|
176
|
+
# Test with None value
|
177
|
+
query1 = self.client.query(User).filter(logical_in(User.id, None))
|
178
|
+
sql1 = query1.to_sql()
|
179
|
+
|
180
|
+
assert "WHERE id IN (None)" in sql1
|
181
|
+
|
182
|
+
# Test with mixed types in list
|
183
|
+
query2 = self.client.query(User).filter(logical_in("id", [1, "2", 3.0]))
|
184
|
+
sql2 = query2.to_sql()
|
185
|
+
|
186
|
+
assert "WHERE id IN (1,'2',3.0)" in sql2
|
187
|
+
|
188
|
+
def test_filter_logical_in_method_chaining(self):
|
189
|
+
"""Test filter(logical_in(...)) with method chaining"""
|
190
|
+
query = (
|
191
|
+
self.client.query(User)
|
192
|
+
.filter(logical_in(User.city, ["北京", "上海"]))
|
193
|
+
.filter(logical_in(User.department, ["Engineering", "Sales"]))
|
194
|
+
.filter(User.age > 25)
|
195
|
+
.order_by(User.name)
|
196
|
+
.limit(10)
|
197
|
+
)
|
198
|
+
|
199
|
+
sql = query.to_sql()
|
200
|
+
|
201
|
+
assert "WHERE city IN ('北京','上海')" in sql
|
202
|
+
assert "AND department IN ('Engineering','Sales')" in sql
|
203
|
+
assert "AND age > 25" in sql
|
204
|
+
assert "ORDER BY name" in sql
|
205
|
+
assert "LIMIT 10" in sql
|
206
|
+
|
207
|
+
def test_filter_logical_in_performance_queries(self):
|
208
|
+
"""Test filter(logical_in(...)) with performance-focused queries"""
|
209
|
+
# Complex query with multiple logical_in conditions
|
210
|
+
query = (
|
211
|
+
self.client.query(User)
|
212
|
+
.select(User.name, User.department, func.count(User.id))
|
213
|
+
.filter(logical_in(User.city, ["北京", "上海", "广州", "深圳"]))
|
214
|
+
.filter(logical_in(User.department, ["Engineering", "Sales", "Marketing"]))
|
215
|
+
.filter(User.age > 25)
|
216
|
+
.group_by(User.department)
|
217
|
+
.having(func.count(User.id) > 1)
|
218
|
+
.order_by(func.count(User.id).desc())
|
219
|
+
.limit(20)
|
220
|
+
)
|
221
|
+
|
222
|
+
sql = query.to_sql()
|
223
|
+
|
224
|
+
assert "SELECT" in sql
|
225
|
+
assert "FROM users" in sql
|
226
|
+
assert "WHERE city IN ('北京','上海','广州','深圳')" in sql
|
227
|
+
assert "AND department IN ('Engineering','Sales','Marketing')" in sql
|
228
|
+
assert "AND age > 25" in sql
|
229
|
+
assert "GROUP BY department" in sql
|
230
|
+
assert "HAVING count(id) > 1" in sql
|
231
|
+
assert "ORDER BY count(id) DESC" in sql
|
232
|
+
assert "LIMIT 20" in sql
|
233
|
+
|
234
|
+
|
235
|
+
if __name__ == "__main__":
|
236
|
+
# Run tests if executed directly
|
237
|
+
pytest.main([__file__, "-v"])
|