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,540 @@
|
|
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
|
+
Test MatrixOneQuery and AsyncMatrixOneQuery ORM functionality.
|
17
|
+
Comprehensive online tests for SQLAlchemy-style ORM operations.
|
18
|
+
"""
|
19
|
+
|
20
|
+
import pytest
|
21
|
+
import pytest_asyncio
|
22
|
+
import asyncio
|
23
|
+
import sys
|
24
|
+
import os
|
25
|
+
from datetime import datetime
|
26
|
+
from typing import List, Optional
|
27
|
+
|
28
|
+
# Add the project root to Python path
|
29
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
30
|
+
|
31
|
+
from matrixone import Client, AsyncClient, SnapshotManager, SnapshotLevel
|
32
|
+
from sqlalchemy import Column, Integer, String, DECIMAL, TIMESTAMP
|
33
|
+
from matrixone.orm import desc, declarative_base
|
34
|
+
|
35
|
+
Base = declarative_base()
|
36
|
+
from .test_config import online_config
|
37
|
+
|
38
|
+
|
39
|
+
# Define test models
|
40
|
+
class User(Base):
|
41
|
+
"""User model for testing"""
|
42
|
+
|
43
|
+
__tablename__ = "test_users"
|
44
|
+
|
45
|
+
id = Column(Integer, primary_key=True)
|
46
|
+
name = Column(String(100))
|
47
|
+
email = Column(String(100))
|
48
|
+
age = Column(Integer)
|
49
|
+
department = Column(String(50))
|
50
|
+
salary = Column(DECIMAL(10, 2))
|
51
|
+
created_at = Column(TIMESTAMP)
|
52
|
+
|
53
|
+
|
54
|
+
class Product(Base):
|
55
|
+
"""Product model for testing"""
|
56
|
+
|
57
|
+
__tablename__ = "test_products"
|
58
|
+
|
59
|
+
id = Column(Integer, primary_key=True)
|
60
|
+
name = Column(String(100))
|
61
|
+
price = Column(DECIMAL(10, 2))
|
62
|
+
category = Column(String(50))
|
63
|
+
stock = Column(Integer)
|
64
|
+
created_at = Column(TIMESTAMP)
|
65
|
+
|
66
|
+
|
67
|
+
class Order(Base):
|
68
|
+
"""Order model for testing"""
|
69
|
+
|
70
|
+
__tablename__ = "test_orders"
|
71
|
+
|
72
|
+
id = Column(Integer, primary_key=True)
|
73
|
+
user_id = Column(Integer)
|
74
|
+
product_id = Column(Integer)
|
75
|
+
quantity = Column(Integer)
|
76
|
+
total_amount = Column(DECIMAL(10, 2))
|
77
|
+
order_date = Column(TIMESTAMP)
|
78
|
+
|
79
|
+
|
80
|
+
class TestMatrixOneQueryORM:
|
81
|
+
"""Test MatrixOneQuery ORM functionality"""
|
82
|
+
|
83
|
+
@pytest.fixture(scope="class")
|
84
|
+
def client(self):
|
85
|
+
"""Create and connect MatrixOne client."""
|
86
|
+
client = Client()
|
87
|
+
host, port, user, password, database = online_config.get_connection_params()
|
88
|
+
client.connect(host=host, port=port, user=user, password=password, database=database)
|
89
|
+
yield client
|
90
|
+
if hasattr(client, 'disconnect'):
|
91
|
+
client.disconnect()
|
92
|
+
|
93
|
+
@pytest.fixture(scope="class")
|
94
|
+
def test_data_setup(self, client):
|
95
|
+
"""Set up test data"""
|
96
|
+
# Clean up any existing test data
|
97
|
+
try:
|
98
|
+
client.execute("DROP TABLE IF EXISTS test_orders")
|
99
|
+
client.execute("DROP TABLE IF EXISTS test_products")
|
100
|
+
client.execute("DROP TABLE IF EXISTS test_users")
|
101
|
+
except:
|
102
|
+
pass
|
103
|
+
|
104
|
+
# Create test tables
|
105
|
+
client.execute(
|
106
|
+
"""
|
107
|
+
CREATE TABLE test_users (
|
108
|
+
id INT PRIMARY KEY,
|
109
|
+
name VARCHAR(100),
|
110
|
+
email VARCHAR(100),
|
111
|
+
age INT,
|
112
|
+
department VARCHAR(50),
|
113
|
+
salary DECIMAL(10,2),
|
114
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
115
|
+
)
|
116
|
+
"""
|
117
|
+
)
|
118
|
+
|
119
|
+
client.execute(
|
120
|
+
"""
|
121
|
+
CREATE TABLE test_products (
|
122
|
+
id INT PRIMARY KEY,
|
123
|
+
name VARCHAR(100),
|
124
|
+
price DECIMAL(10,2),
|
125
|
+
category VARCHAR(50),
|
126
|
+
stock INT,
|
127
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
128
|
+
)
|
129
|
+
"""
|
130
|
+
)
|
131
|
+
|
132
|
+
client.execute(
|
133
|
+
"""
|
134
|
+
CREATE TABLE test_orders (
|
135
|
+
id INT PRIMARY KEY,
|
136
|
+
user_id INT,
|
137
|
+
product_id INT,
|
138
|
+
quantity INT,
|
139
|
+
total_amount DECIMAL(10,2),
|
140
|
+
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
141
|
+
)
|
142
|
+
"""
|
143
|
+
)
|
144
|
+
|
145
|
+
# Insert test data
|
146
|
+
client.execute(
|
147
|
+
"""
|
148
|
+
INSERT INTO test_users (id, name, email, age, department, salary) VALUES
|
149
|
+
(1, 'Alice Johnson', 'alice@example.com', 28, 'Engineering', 75000.00),
|
150
|
+
(2, 'Bob Smith', 'bob@example.com', 32, 'Marketing', 65000.00),
|
151
|
+
(3, 'Charlie Brown', 'charlie@example.com', 25, 'Engineering', 70000.00),
|
152
|
+
(4, 'Diana Prince', 'diana@example.com', 30, 'Sales', 60000.00),
|
153
|
+
(5, 'Eve Wilson', 'eve@example.com', 27, 'Engineering', 80000.00)
|
154
|
+
"""
|
155
|
+
)
|
156
|
+
|
157
|
+
client.execute(
|
158
|
+
"""
|
159
|
+
INSERT INTO test_products (id, name, price, category, stock) VALUES
|
160
|
+
(1, 'Laptop Pro', 1299.99, 'Electronics', 50),
|
161
|
+
(2, 'Wireless Mouse', 29.99, 'Electronics', 200),
|
162
|
+
(3, 'Office Chair', 199.99, 'Furniture', 30),
|
163
|
+
(4, 'Coffee Mug', 12.99, 'Accessories', 100),
|
164
|
+
(5, 'Monitor 4K', 399.99, 'Electronics', 25)
|
165
|
+
"""
|
166
|
+
)
|
167
|
+
|
168
|
+
client.execute(
|
169
|
+
"""
|
170
|
+
INSERT INTO test_orders (id, user_id, product_id, quantity, total_amount) VALUES
|
171
|
+
(1, 1, 1, 1, 1299.99),
|
172
|
+
(2, 2, 2, 2, 59.98),
|
173
|
+
(3, 3, 3, 1, 199.99),
|
174
|
+
(4, 1, 4, 3, 38.97),
|
175
|
+
(5, 4, 5, 1, 399.99)
|
176
|
+
"""
|
177
|
+
)
|
178
|
+
|
179
|
+
yield
|
180
|
+
|
181
|
+
# Cleanup
|
182
|
+
try:
|
183
|
+
client.execute("DROP TABLE test_orders")
|
184
|
+
client.execute("DROP TABLE test_products")
|
185
|
+
client.execute("DROP TABLE test_users")
|
186
|
+
except:
|
187
|
+
pass
|
188
|
+
|
189
|
+
def test_basic_query_all(self, client, test_data_setup):
|
190
|
+
"""Test basic query all functionality"""
|
191
|
+
users = client.query(User).all()
|
192
|
+
assert len(users) == 5
|
193
|
+
assert users[0].name == 'Alice Johnson'
|
194
|
+
assert users[0].email == 'alice@example.com'
|
195
|
+
|
196
|
+
def test_filter_by(self, client, test_data_setup):
|
197
|
+
"""Test filter_by functionality"""
|
198
|
+
# Test single filter
|
199
|
+
engineering_users = client.query(User).filter_by(department='Engineering').all()
|
200
|
+
assert len(engineering_users) == 3
|
201
|
+
assert all(user.department == 'Engineering' for user in engineering_users)
|
202
|
+
|
203
|
+
# Test multiple filters
|
204
|
+
young_engineers = client.query(User).filter_by(department='Engineering', age=25).all()
|
205
|
+
assert len(young_engineers) == 1
|
206
|
+
assert young_engineers[0].name == 'Charlie Brown'
|
207
|
+
|
208
|
+
def test_filter(self, client, test_data_setup):
|
209
|
+
"""Test filter functionality with custom conditions"""
|
210
|
+
# Test numeric comparison
|
211
|
+
high_salary_users = client.query(User).filter("salary > ?", 70000.0).all()
|
212
|
+
assert len(high_salary_users) == 2
|
213
|
+
assert all(user.salary > 70000.0 for user in high_salary_users)
|
214
|
+
|
215
|
+
# Test string comparison
|
216
|
+
electronics_products = client.query(Product).filter("category = ?", 'Electronics').all()
|
217
|
+
assert len(electronics_products) == 3
|
218
|
+
assert all(product.category == 'Electronics' for product in electronics_products)
|
219
|
+
|
220
|
+
def test_order_by(self, client, test_data_setup):
|
221
|
+
"""Test order_by functionality"""
|
222
|
+
# Test ascending order
|
223
|
+
users_by_age = client.query(User).order_by('age').all()
|
224
|
+
assert users_by_age[0].age == 25
|
225
|
+
assert users_by_age[-1].age == 32
|
226
|
+
|
227
|
+
# Test descending order
|
228
|
+
users_by_salary_desc = client.query(User).order_by(desc('salary')).all()
|
229
|
+
assert users_by_salary_desc[0].salary == 80000.0
|
230
|
+
assert users_by_salary_desc[-1].salary == 60000.0
|
231
|
+
|
232
|
+
def test_limit(self, client, test_data_setup):
|
233
|
+
"""Test limit functionality"""
|
234
|
+
top_users = client.query(User).order_by(desc('salary')).limit(3).all()
|
235
|
+
assert len(top_users) == 3
|
236
|
+
assert top_users[0].salary == 80000.0
|
237
|
+
|
238
|
+
def test_offset(self, client, test_data_setup):
|
239
|
+
"""Test offset functionality"""
|
240
|
+
# MatrixOne requires LIMIT with OFFSET, so we use a large limit
|
241
|
+
users_skip_first = client.query(User).order_by('id').limit(100).offset(2).all()
|
242
|
+
assert len(users_skip_first) == 3
|
243
|
+
assert users_skip_first[0].id == 3
|
244
|
+
|
245
|
+
def test_limit_offset_combination(self, client, test_data_setup):
|
246
|
+
"""Test limit and offset combination"""
|
247
|
+
users_page2 = client.query(User).order_by('id').limit(2).offset(2).all()
|
248
|
+
assert len(users_page2) == 2
|
249
|
+
assert users_page2[0].id == 3
|
250
|
+
assert users_page2[1].id == 4
|
251
|
+
|
252
|
+
def test_count(self, client, test_data_setup):
|
253
|
+
"""Test count functionality"""
|
254
|
+
total_users = client.query(User).count()
|
255
|
+
assert total_users == 5
|
256
|
+
|
257
|
+
engineering_count = client.query(User).filter_by(department='Engineering').count()
|
258
|
+
assert engineering_count == 3
|
259
|
+
|
260
|
+
def test_first(self, client, test_data_setup):
|
261
|
+
"""Test first functionality"""
|
262
|
+
first_user = client.query(User).order_by('id').first()
|
263
|
+
assert first_user is not None
|
264
|
+
assert first_user.id == 1
|
265
|
+
assert first_user.name == 'Alice Johnson'
|
266
|
+
|
267
|
+
# Test first with filter
|
268
|
+
engineering_user = client.query(User).filter_by(department='Engineering').first()
|
269
|
+
assert engineering_user is not None
|
270
|
+
assert engineering_user.department == 'Engineering'
|
271
|
+
|
272
|
+
def test_complex_queries(self, client, test_data_setup):
|
273
|
+
"""Test complex query combinations"""
|
274
|
+
# Complex query: high-salary engineering users ordered by age
|
275
|
+
result = client.query(User).filter_by(department='Engineering').filter("salary > ?", 70000.0).order_by('age').all()
|
276
|
+
|
277
|
+
assert len(result) == 2
|
278
|
+
assert result[0].name == 'Eve Wilson' # age 27 (younger of the two)
|
279
|
+
assert result[1].name == 'Alice Johnson' # age 28 (older of the two)
|
280
|
+
|
281
|
+
def test_model_attributes(self, client, test_data_setup):
|
282
|
+
"""Test model attribute access"""
|
283
|
+
user = client.query(User).first()
|
284
|
+
assert hasattr(user, 'id')
|
285
|
+
assert hasattr(user, 'name')
|
286
|
+
assert hasattr(user, 'email')
|
287
|
+
assert hasattr(user, 'age')
|
288
|
+
assert hasattr(user, 'department')
|
289
|
+
assert hasattr(user, 'salary')
|
290
|
+
assert hasattr(user, 'created_at')
|
291
|
+
|
292
|
+
def test_empty_results(self, client, test_data_setup):
|
293
|
+
"""Test queries that return empty results"""
|
294
|
+
# Non-existent department
|
295
|
+
empty_result = client.query(User).filter_by(department='NonExistent').all()
|
296
|
+
assert len(empty_result) == 0
|
297
|
+
|
298
|
+
# Non-existent user
|
299
|
+
empty_first = client.query(User).filter_by(department='NonExistent').first()
|
300
|
+
assert empty_first is None
|
301
|
+
|
302
|
+
# Count of non-existent records
|
303
|
+
empty_count = client.query(User).filter_by(department='NonExistent').count()
|
304
|
+
assert empty_count == 0
|
305
|
+
|
306
|
+
|
307
|
+
# Define a global AsyncUser class for all async tests
|
308
|
+
class AsyncUser(Base):
|
309
|
+
__tablename__ = "async_test_users"
|
310
|
+
id = Column(Integer, primary_key=True)
|
311
|
+
name = Column(String(100))
|
312
|
+
email = Column(String(100))
|
313
|
+
age = Column(Integer)
|
314
|
+
department = Column(String(50))
|
315
|
+
salary = Column(DECIMAL(10, 2))
|
316
|
+
created_at = Column(TIMESTAMP)
|
317
|
+
|
318
|
+
|
319
|
+
# Define a global TestModel class for edge case tests
|
320
|
+
class TempTable1(Base):
|
321
|
+
__tablename__ = "test_malformed_filter"
|
322
|
+
id = Column(Integer, primary_key=True)
|
323
|
+
name = Column(String(50))
|
324
|
+
|
325
|
+
|
326
|
+
class TestAsyncMatrixOneQueryORM:
|
327
|
+
"""Test AsyncMatrixOneQuery ORM functionality"""
|
328
|
+
|
329
|
+
@pytest_asyncio.fixture(scope="function")
|
330
|
+
async def async_client(self):
|
331
|
+
"""Create and connect async MatrixOne client."""
|
332
|
+
client = AsyncClient()
|
333
|
+
host, port, user, password, database = online_config.get_connection_params()
|
334
|
+
await client.connect(host=host, port=port, user=user, password=password, database=database)
|
335
|
+
yield client
|
336
|
+
if hasattr(client, 'disconnect'):
|
337
|
+
await client.disconnect()
|
338
|
+
|
339
|
+
@pytest_asyncio.fixture(scope="function")
|
340
|
+
async def async_test_data_setup(self, async_client):
|
341
|
+
"""Set up test data for async tests"""
|
342
|
+
# Clean up any existing test data
|
343
|
+
try:
|
344
|
+
await async_client.execute("DROP TABLE IF EXISTS async_test_orders")
|
345
|
+
await async_client.execute("DROP TABLE IF EXISTS async_test_products")
|
346
|
+
await async_client.execute("DROP TABLE IF EXISTS async_test_users")
|
347
|
+
except:
|
348
|
+
pass
|
349
|
+
|
350
|
+
# Create test tables
|
351
|
+
await async_client.execute(
|
352
|
+
"""
|
353
|
+
CREATE TABLE async_test_users (
|
354
|
+
id INT PRIMARY KEY,
|
355
|
+
name VARCHAR(100),
|
356
|
+
email VARCHAR(100),
|
357
|
+
age INT,
|
358
|
+
department VARCHAR(50),
|
359
|
+
salary DECIMAL(10,2),
|
360
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
361
|
+
)
|
362
|
+
"""
|
363
|
+
)
|
364
|
+
|
365
|
+
await async_client.execute(
|
366
|
+
"""
|
367
|
+
CREATE TABLE async_test_products (
|
368
|
+
id INT PRIMARY KEY,
|
369
|
+
name VARCHAR(100),
|
370
|
+
price DECIMAL(10,2),
|
371
|
+
category VARCHAR(50),
|
372
|
+
stock INT,
|
373
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
374
|
+
)
|
375
|
+
"""
|
376
|
+
)
|
377
|
+
|
378
|
+
# Insert test data
|
379
|
+
await async_client.execute(
|
380
|
+
"""
|
381
|
+
INSERT INTO async_test_users (id, name, email, age, department, salary) VALUES
|
382
|
+
(1, 'Async Alice', 'async.alice@example.com', 28, 'Engineering', 75000.00),
|
383
|
+
(2, 'Async Bob', 'async.bob@example.com', 32, 'Marketing', 65000.00),
|
384
|
+
(3, 'Async Charlie', 'async.charlie@example.com', 25, 'Engineering', 70000.00),
|
385
|
+
(4, 'Async Diana', 'async.diana@example.com', 30, 'Sales', 60000.00),
|
386
|
+
(5, 'Async Eve', 'async.eve@example.com', 27, 'Engineering', 80000.00)
|
387
|
+
"""
|
388
|
+
)
|
389
|
+
|
390
|
+
await async_client.execute(
|
391
|
+
"""
|
392
|
+
INSERT INTO async_test_products (id, name, price, category, stock) VALUES
|
393
|
+
(1, 'Async Laptop', 1299.99, 'Electronics', 50),
|
394
|
+
(2, 'Async Mouse', 29.99, 'Electronics', 200),
|
395
|
+
(3, 'Async Chair', 199.99, 'Furniture', 30),
|
396
|
+
(4, 'Async Mug', 12.99, 'Accessories', 100),
|
397
|
+
(5, 'Async Monitor', 399.99, 'Electronics', 25)
|
398
|
+
"""
|
399
|
+
)
|
400
|
+
|
401
|
+
yield
|
402
|
+
|
403
|
+
# Cleanup
|
404
|
+
try:
|
405
|
+
await async_client.execute("DROP TABLE async_test_products")
|
406
|
+
await async_client.execute("DROP TABLE async_test_users")
|
407
|
+
except:
|
408
|
+
pass
|
409
|
+
|
410
|
+
@pytest.mark.asyncio
|
411
|
+
async def test_async_basic_query_all(self, async_client, async_test_data_setup):
|
412
|
+
"""Test async basic query all functionality"""
|
413
|
+
users = await async_client.query(AsyncUser).all()
|
414
|
+
assert len(users) == 5
|
415
|
+
assert users[0].name == 'Async Alice'
|
416
|
+
|
417
|
+
@pytest.mark.asyncio
|
418
|
+
async def test_async_filter_by(self, async_client, async_test_data_setup):
|
419
|
+
"""Test async filter_by functionality"""
|
420
|
+
engineering_users = await async_client.query(AsyncUser).filter_by(department='Engineering').all()
|
421
|
+
assert len(engineering_users) == 3
|
422
|
+
assert all(user.department == 'Engineering' for user in engineering_users)
|
423
|
+
|
424
|
+
@pytest.mark.asyncio
|
425
|
+
async def test_async_filter(self, async_client, async_test_data_setup):
|
426
|
+
"""Test async filter functionality"""
|
427
|
+
high_salary_users = await async_client.query(AsyncUser).filter("salary > ?", 70000.0).all()
|
428
|
+
assert len(high_salary_users) == 2
|
429
|
+
assert all(user.salary > 70000.0 for user in high_salary_users)
|
430
|
+
|
431
|
+
@pytest.mark.asyncio
|
432
|
+
async def test_async_order_by(self, async_client, async_test_data_setup):
|
433
|
+
"""Test async order_by functionality"""
|
434
|
+
users_by_salary_desc = await async_client.query(AsyncUser).order_by(desc('salary')).all()
|
435
|
+
assert users_by_salary_desc[0].salary == 80000.0
|
436
|
+
assert users_by_salary_desc[-1].salary == 60000.0
|
437
|
+
|
438
|
+
@pytest.mark.asyncio
|
439
|
+
async def test_async_limit_offset(self, async_client, async_test_data_setup):
|
440
|
+
"""Test async limit and offset functionality"""
|
441
|
+
top_users = await async_client.query(AsyncUser).order_by(desc('salary')).limit(3).all()
|
442
|
+
assert len(top_users) == 3
|
443
|
+
assert top_users[0].salary == 80000.0
|
444
|
+
|
445
|
+
users_skip_first = await async_client.query(AsyncUser).order_by('id').limit(100).offset(2).all()
|
446
|
+
assert len(users_skip_first) == 3
|
447
|
+
assert users_skip_first[0].id == 3
|
448
|
+
|
449
|
+
@pytest.mark.asyncio
|
450
|
+
async def test_async_count(self, async_client, async_test_data_setup):
|
451
|
+
"""Test async count functionality"""
|
452
|
+
total_users = await async_client.query(AsyncUser).count()
|
453
|
+
assert total_users == 5
|
454
|
+
|
455
|
+
engineering_count = await async_client.query(AsyncUser).filter_by(department='Engineering').count()
|
456
|
+
assert engineering_count == 3
|
457
|
+
|
458
|
+
@pytest.mark.asyncio
|
459
|
+
async def test_async_first(self, async_client, async_test_data_setup):
|
460
|
+
"""Test async first functionality"""
|
461
|
+
first_user = await async_client.query(AsyncUser).order_by('id').first()
|
462
|
+
assert first_user is not None
|
463
|
+
assert first_user.id == 1
|
464
|
+
assert first_user.name == 'Async Alice'
|
465
|
+
|
466
|
+
@pytest.mark.asyncio
|
467
|
+
async def test_async_complex_queries(self, async_client, async_test_data_setup):
|
468
|
+
"""Test async complex query combinations"""
|
469
|
+
result = (
|
470
|
+
await async_client.query(AsyncUser)
|
471
|
+
.filter_by(department='Engineering')
|
472
|
+
.filter("salary > ?", 70000.0)
|
473
|
+
.order_by('age')
|
474
|
+
.all()
|
475
|
+
)
|
476
|
+
|
477
|
+
assert len(result) == 2
|
478
|
+
assert result[0].name == 'Async Eve' # age 27 (younger of the two)
|
479
|
+
assert result[1].name == 'Async Alice' # age 28 (older of the two)
|
480
|
+
|
481
|
+
@pytest.mark.asyncio
|
482
|
+
async def test_async_empty_results(self, async_client, async_test_data_setup):
|
483
|
+
"""Test async queries that return empty results"""
|
484
|
+
# Non-existent department
|
485
|
+
empty_result = await async_client.query(AsyncUser).filter_by(department='NonExistent').all()
|
486
|
+
assert len(empty_result) == 0
|
487
|
+
|
488
|
+
# Non-existent user
|
489
|
+
empty_first = await async_client.query(AsyncUser).filter_by(department='NonExistent').first()
|
490
|
+
assert empty_first is None
|
491
|
+
|
492
|
+
# Count of non-existent records
|
493
|
+
empty_count = await async_client.query(AsyncUser).filter_by(department='NonExistent').count()
|
494
|
+
assert empty_count == 0
|
495
|
+
|
496
|
+
|
497
|
+
class TestMatrixOneQueryEdgeCases:
|
498
|
+
"""Test edge cases and error conditions"""
|
499
|
+
|
500
|
+
@pytest.fixture(scope="class")
|
501
|
+
def client(self):
|
502
|
+
"""Create and connect MatrixOne client."""
|
503
|
+
client = Client()
|
504
|
+
host, port, user, password, database = online_config.get_connection_params()
|
505
|
+
client.connect(host=host, port=port, user=user, password=password, database=database)
|
506
|
+
yield client
|
507
|
+
if hasattr(client, 'disconnect'):
|
508
|
+
client.disconnect()
|
509
|
+
|
510
|
+
def test_invalid_model(self, client):
|
511
|
+
"""Test query with invalid model"""
|
512
|
+
|
513
|
+
class InvalidModel(Base):
|
514
|
+
__tablename__ = "non_existent_table"
|
515
|
+
id = Column(Integer, primary_key=True)
|
516
|
+
|
517
|
+
with pytest.raises(Exception):
|
518
|
+
client.query(InvalidModel).all()
|
519
|
+
|
520
|
+
def test_malformed_filter(self, client):
|
521
|
+
"""Test malformed filter conditions"""
|
522
|
+
# Create a simple test table first
|
523
|
+
try:
|
524
|
+
client.execute("DROP TABLE IF EXISTS test_malformed")
|
525
|
+
client.execute("CREATE TABLE test_malformed (id INT PRIMARY KEY, name VARCHAR(50))")
|
526
|
+
client.execute("INSERT INTO test_malformed (id, name) VALUES (1, 'test')")
|
527
|
+
|
528
|
+
# This should raise an exception due to invalid SQL syntax
|
529
|
+
with pytest.raises(Exception):
|
530
|
+
client.query(TempTable1).filter("invalid_syntax").all()
|
531
|
+
|
532
|
+
finally:
|
533
|
+
try:
|
534
|
+
client.execute("DROP TABLE test_malformed")
|
535
|
+
except:
|
536
|
+
pass
|
537
|
+
|
538
|
+
|
539
|
+
if __name__ == "__main__":
|
540
|
+
pytest.main([__file__])
|