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,276 @@
|
|
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 join methods compatibility with SQLAlchemy ORM
|
19
|
+
|
20
|
+
This test file verifies that MatrixOne's join methods produce the same SQL
|
21
|
+
as SQLAlchemy ORM for equivalent operations.
|
22
|
+
"""
|
23
|
+
|
24
|
+
import sys
|
25
|
+
import os
|
26
|
+
import unittest
|
27
|
+
from unittest.mock import Mock
|
28
|
+
|
29
|
+
# Add the matrixone package to the path
|
30
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
31
|
+
|
32
|
+
from matrixone.orm import MatrixOneQuery, declarative_base
|
33
|
+
from sqlalchemy import Column, Integer, String, create_engine, select
|
34
|
+
from sqlalchemy.orm import sessionmaker
|
35
|
+
|
36
|
+
Base = declarative_base()
|
37
|
+
|
38
|
+
|
39
|
+
class User(Base):
|
40
|
+
__tablename__ = 'users'
|
41
|
+
id = Column(Integer, primary_key=True)
|
42
|
+
name = Column(String(50))
|
43
|
+
department_id = Column(Integer)
|
44
|
+
|
45
|
+
|
46
|
+
class Address(Base):
|
47
|
+
__tablename__ = 'addresses'
|
48
|
+
id = Column(Integer, primary_key=True)
|
49
|
+
user_id = Column(Integer)
|
50
|
+
email = Column(String(100))
|
51
|
+
|
52
|
+
|
53
|
+
class Department(Base):
|
54
|
+
__tablename__ = 'departments'
|
55
|
+
id = Column(Integer, primary_key=True)
|
56
|
+
name = Column(String(50))
|
57
|
+
|
58
|
+
|
59
|
+
class TestJoinSQLAlchemyCompatibility(unittest.TestCase):
|
60
|
+
"""Test join methods compatibility with SQLAlchemy ORM"""
|
61
|
+
|
62
|
+
def setUp(self):
|
63
|
+
"""Set up test fixtures"""
|
64
|
+
self.mock_client = Mock()
|
65
|
+
self.query = MatrixOneQuery(User, self.mock_client)
|
66
|
+
self.query._table_name = 'users'
|
67
|
+
|
68
|
+
def test_join_behavior_matches_sqlalchemy(self):
|
69
|
+
"""Test that join behavior matches SQLAlchemy ORM"""
|
70
|
+
# MatrixOne query
|
71
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
72
|
+
matrixone_query._table_name = 'users'
|
73
|
+
matrixone_query.join(Address, User.id == Address.user_id)
|
74
|
+
|
75
|
+
# SQLAlchemy equivalent
|
76
|
+
sqlalchemy_query = select(User).join(Address, User.id == Address.user_id)
|
77
|
+
|
78
|
+
# Both should produce INNER JOIN
|
79
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
80
|
+
|
81
|
+
# MatrixOne should handle the SQLAlchemy expression correctly
|
82
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
83
|
+
self.assertIn("id = user_id", matrixone_query._joins[0])
|
84
|
+
|
85
|
+
def test_left_join_behavior_matches_sqlalchemy(self):
|
86
|
+
"""Test that left join behavior matches SQLAlchemy ORM"""
|
87
|
+
# MatrixOne query
|
88
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
89
|
+
matrixone_query._table_name = 'users'
|
90
|
+
matrixone_query.join(Address, User.id == Address.user_id, isouter=True)
|
91
|
+
|
92
|
+
# SQLAlchemy equivalent
|
93
|
+
sqlalchemy_query = select(User).outerjoin(Address, User.id == Address.user_id)
|
94
|
+
|
95
|
+
# Both should produce LEFT OUTER JOIN
|
96
|
+
self.assertTrue(matrixone_query._joins[0].startswith("LEFT OUTER JOIN"))
|
97
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
98
|
+
|
99
|
+
def test_join_without_condition_matches_sqlalchemy(self):
|
100
|
+
"""Test that join without condition matches SQLAlchemy ORM"""
|
101
|
+
# MatrixOne query
|
102
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
103
|
+
matrixone_query._table_name = 'users'
|
104
|
+
matrixone_query.join(Address)
|
105
|
+
|
106
|
+
# SQLAlchemy equivalent
|
107
|
+
sqlalchemy_query = select(User).join(Address)
|
108
|
+
|
109
|
+
# Both should produce INNER JOIN without ON condition
|
110
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
111
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
112
|
+
self.assertNotIn("ON", matrixone_query._joins[0])
|
113
|
+
|
114
|
+
def test_multiple_joins_matches_sqlalchemy(self):
|
115
|
+
"""Test that multiple joins match SQLAlchemy ORM"""
|
116
|
+
# MatrixOne query
|
117
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
118
|
+
matrixone_query._table_name = 'users'
|
119
|
+
matrixone_query.join(Address, User.id == Address.user_id)
|
120
|
+
matrixone_query.join(Department, User.department_id == Department.id)
|
121
|
+
|
122
|
+
# SQLAlchemy equivalent
|
123
|
+
sqlalchemy_query = (
|
124
|
+
select(User).join(Address, User.id == Address.user_id).join(Department, User.department_id == Department.id)
|
125
|
+
)
|
126
|
+
|
127
|
+
# Both should have two joins
|
128
|
+
self.assertEqual(len(matrixone_query._joins), 2)
|
129
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
130
|
+
self.assertTrue(matrixone_query._joins[1].startswith("INNER JOIN"))
|
131
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
132
|
+
self.assertIn("departments", matrixone_query._joins[1])
|
133
|
+
|
134
|
+
def test_join_with_string_condition(self):
|
135
|
+
"""Test join with string condition (MatrixOne specific)"""
|
136
|
+
# MatrixOne query with string condition
|
137
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
138
|
+
matrixone_query._table_name = 'users'
|
139
|
+
matrixone_query.join('addresses', 'users.id = addresses.user_id')
|
140
|
+
|
141
|
+
# Should produce INNER JOIN with the exact condition
|
142
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
143
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
144
|
+
self.assertIn("users.id = addresses.user_id", matrixone_query._joins[0])
|
145
|
+
|
146
|
+
def test_join_with_onclause_parameter(self):
|
147
|
+
"""Test join with onclause parameter (SQLAlchemy style)"""
|
148
|
+
# MatrixOne query with onclause parameter
|
149
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
150
|
+
matrixone_query._table_name = 'users'
|
151
|
+
matrixone_query.join('addresses', onclause='users.id = addresses.user_id')
|
152
|
+
|
153
|
+
# Should produce INNER JOIN with the exact condition
|
154
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
155
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
156
|
+
self.assertIn("users.id = addresses.user_id", matrixone_query._joins[0])
|
157
|
+
|
158
|
+
def test_join_method_aliases(self):
|
159
|
+
"""Test that join method aliases work correctly"""
|
160
|
+
# Test innerjoin
|
161
|
+
query1 = MatrixOneQuery(User, self.mock_client)
|
162
|
+
query1._table_name = 'users'
|
163
|
+
query1.innerjoin(Address, User.id == Address.user_id)
|
164
|
+
|
165
|
+
# Test leftjoin
|
166
|
+
query2 = MatrixOneQuery(User, self.mock_client)
|
167
|
+
query2._table_name = 'users'
|
168
|
+
query2.leftjoin(Address, User.id == Address.user_id)
|
169
|
+
|
170
|
+
# Test outerjoin
|
171
|
+
query3 = MatrixOneQuery(User, self.mock_client)
|
172
|
+
query3._table_name = 'users'
|
173
|
+
query3.outerjoin(Address, User.id == Address.user_id)
|
174
|
+
|
175
|
+
# Test fullouterjoin
|
176
|
+
query4 = MatrixOneQuery(User, self.mock_client)
|
177
|
+
query4._table_name = 'users'
|
178
|
+
query4.fullouterjoin(Address, User.id == Address.user_id)
|
179
|
+
|
180
|
+
# Verify join types
|
181
|
+
self.assertTrue(query1._joins[0].startswith("INNER JOIN"))
|
182
|
+
self.assertTrue(query2._joins[0].startswith("LEFT OUTER JOIN"))
|
183
|
+
self.assertTrue(query3._joins[0].startswith("LEFT OUTER JOIN"))
|
184
|
+
self.assertTrue(query4._joins[0].startswith("FULL OUTER JOIN"))
|
185
|
+
|
186
|
+
def test_join_with_complex_expression(self):
|
187
|
+
"""Test join with complex SQLAlchemy expression"""
|
188
|
+
# MatrixOne query with complex expression
|
189
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
190
|
+
matrixone_query._table_name = 'users'
|
191
|
+
matrixone_query.join(Address, (User.id == Address.user_id) & (Address.email.like('%@example.com')))
|
192
|
+
|
193
|
+
# Should handle complex expressions
|
194
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
195
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
196
|
+
self.assertIn("ON", matrixone_query._joins[0])
|
197
|
+
# The complex expression should be compiled to SQL
|
198
|
+
join_clause = matrixone_query._joins[0]
|
199
|
+
self.assertIn("id = user_id", join_clause)
|
200
|
+
|
201
|
+
def test_join_method_chaining_compatibility(self):
|
202
|
+
"""Test that join methods support method chaining like SQLAlchemy"""
|
203
|
+
# MatrixOne query with method chaining
|
204
|
+
result = (
|
205
|
+
MatrixOneQuery(User, self.mock_client)
|
206
|
+
.join(Address, User.id == Address.user_id)
|
207
|
+
.join(Department, User.department_id == Department.id)
|
208
|
+
.filter(User.name == 'John')
|
209
|
+
)
|
210
|
+
|
211
|
+
# Should return self for chaining
|
212
|
+
self.assertEqual(result._table_name, 'users')
|
213
|
+
self.assertEqual(len(result._joins), 2)
|
214
|
+
self.assertEqual(len(result._where_conditions), 1)
|
215
|
+
|
216
|
+
def test_join_with_model_class_target(self):
|
217
|
+
"""Test join with model class as target"""
|
218
|
+
# MatrixOne query with model class
|
219
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
220
|
+
matrixone_query._table_name = 'users'
|
221
|
+
matrixone_query.join(Address, 'users.id = addresses.user_id')
|
222
|
+
|
223
|
+
# Should use the model's table name
|
224
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
225
|
+
self.assertIn("addresses", matrixone_query._joins[0])
|
226
|
+
self.assertIn("users.id = addresses.user_id", matrixone_query._joins[0])
|
227
|
+
|
228
|
+
def test_join_parameter_consistency_with_sqlalchemy(self):
|
229
|
+
"""Test that parameter names are consistent with SQLAlchemy"""
|
230
|
+
# Test that onclause parameter works (SQLAlchemy style)
|
231
|
+
query1 = MatrixOneQuery(User, self.mock_client)
|
232
|
+
query1._table_name = 'users'
|
233
|
+
query1.join('addresses', onclause='users.id = addresses.user_id')
|
234
|
+
|
235
|
+
# Test that positional parameter also works (backward compatibility)
|
236
|
+
query2 = MatrixOneQuery(User, self.mock_client)
|
237
|
+
query2._table_name = 'users'
|
238
|
+
query2.join('addresses', 'users.id = addresses.user_id')
|
239
|
+
|
240
|
+
# Both should produce the same result
|
241
|
+
self.assertEqual(query1._joins[0], query2._joins[0])
|
242
|
+
|
243
|
+
def test_join_default_behavior_is_inner_join(self):
|
244
|
+
"""Test that join() defaults to INNER JOIN like SQLAlchemy"""
|
245
|
+
# MatrixOne query
|
246
|
+
matrixone_query = MatrixOneQuery(User, self.mock_client)
|
247
|
+
matrixone_query._table_name = 'users'
|
248
|
+
matrixone_query.join(Address, User.id == Address.user_id)
|
249
|
+
|
250
|
+
# SQLAlchemy equivalent
|
251
|
+
sqlalchemy_query = select(User).join(Address, User.id == Address.user_id)
|
252
|
+
|
253
|
+
# Both should default to INNER JOIN
|
254
|
+
self.assertTrue(matrixone_query._joins[0].startswith("INNER JOIN"))
|
255
|
+
|
256
|
+
# This matches SQLAlchemy's default behavior where join() creates INNER JOIN
|
257
|
+
|
258
|
+
|
259
|
+
if __name__ == '__main__':
|
260
|
+
# Create test suite
|
261
|
+
test_suite = unittest.TestSuite()
|
262
|
+
|
263
|
+
# Add all test methods
|
264
|
+
test_suite.addTest(unittest.makeSuite(TestJoinSQLAlchemyCompatibility))
|
265
|
+
|
266
|
+
# Run the tests
|
267
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
268
|
+
result = runner.run(test_suite)
|
269
|
+
|
270
|
+
# Print summary
|
271
|
+
print(f"\n{'='*50}")
|
272
|
+
print(f"Tests run: {result.testsRun}")
|
273
|
+
print(f"Failures: {len(result.failures)}")
|
274
|
+
print(f"Errors: {len(result.errors)}")
|
275
|
+
print(f"Success rate: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%")
|
276
|
+
print(f"{'='*50}")
|
@@ -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: logical_in method for enhanced IN functionality
|
19
|
+
|
20
|
+
This test verifies that the logical_in method 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 TestLogicalInMethod:
|
62
|
+
"""Test class for logical_in method functionality"""
|
63
|
+
|
64
|
+
def setup_method(self):
|
65
|
+
"""Set up test client"""
|
66
|
+
self.client = Client("test://localhost:6001/test")
|
67
|
+
|
68
|
+
def test_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_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_logical_in_with_fulltext_filter(self):
|
98
|
+
"""Test 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_logical_in_with_subquery(self):
|
121
|
+
"""Test 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_logical_in_with_sqlalchemy_expressions(self):
|
131
|
+
"""Test 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_logical_in_with_string_column(self):
|
139
|
+
"""Test 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_logical_in_with_sqlalchemy_column(self):
|
146
|
+
"""Test 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_logical_in_combined_with_other_conditions(self):
|
153
|
+
"""Test 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_logical_in_with_tuple_values(self):
|
168
|
+
"""Test 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_logical_in_edge_cases(self):
|
175
|
+
"""Test 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_logical_in_method_chaining(self):
|
189
|
+
"""Test 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_logical_in_performance_queries(self):
|
208
|
+
"""Test 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"])
|