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,269 @@
|
|
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 CTE (Common Table Expression) integration with MatrixOneQuery
|
17
|
+
|
18
|
+
This test verifies that the new SQLAlchemy-style CTE support works correctly
|
19
|
+
with MatrixOneQuery, including:
|
20
|
+
- Creating CTEs from queries
|
21
|
+
- Using CTEs in other queries
|
22
|
+
- Multiple CTEs in a single query
|
23
|
+
- CTE parameter handling
|
24
|
+
"""
|
25
|
+
|
26
|
+
import pytest
|
27
|
+
from sqlalchemy import Column, Integer, String, create_engine, text
|
28
|
+
from sqlalchemy.orm import sessionmaker
|
29
|
+
|
30
|
+
from matrixone.client import Client
|
31
|
+
from matrixone.orm import CTE, declarative_base
|
32
|
+
|
33
|
+
Base = declarative_base()
|
34
|
+
|
35
|
+
|
36
|
+
class User(Base):
|
37
|
+
__tablename__ = "cte_users"
|
38
|
+
id = Column(Integer, primary_key=True)
|
39
|
+
name = Column(String(50))
|
40
|
+
department = Column(String(50))
|
41
|
+
salary = Column(Integer)
|
42
|
+
|
43
|
+
|
44
|
+
class Article(Base):
|
45
|
+
__tablename__ = "cte_articles"
|
46
|
+
id = Column(Integer, primary_key=True)
|
47
|
+
title = Column(String(100))
|
48
|
+
author_id = Column(Integer)
|
49
|
+
views = Column(Integer)
|
50
|
+
|
51
|
+
|
52
|
+
class TestORMCTEIntegration:
|
53
|
+
"""Test CTE integration with MatrixOneQuery"""
|
54
|
+
|
55
|
+
@pytest.fixture(autouse=True)
|
56
|
+
def setup_and_cleanup(self, connection_params, test_database):
|
57
|
+
"""Setup test database and tables"""
|
58
|
+
host, port, user, password, database = connection_params
|
59
|
+
self.client = Client(host=host, port=port, user=user, password=password, database=database)
|
60
|
+
self.test_db = f"cte_test_db_{test_database}"
|
61
|
+
|
62
|
+
# Create test database
|
63
|
+
self.client.execute(f"CREATE DATABASE IF NOT EXISTS {self.test_db}")
|
64
|
+
self.client.execute(f"USE {self.test_db}")
|
65
|
+
|
66
|
+
# Create tables
|
67
|
+
self.client.execute(
|
68
|
+
f"""
|
69
|
+
CREATE TABLE IF NOT EXISTS {self.test_db}.cte_users (
|
70
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
71
|
+
name VARCHAR(50),
|
72
|
+
department VARCHAR(50),
|
73
|
+
salary INT
|
74
|
+
)
|
75
|
+
"""
|
76
|
+
)
|
77
|
+
|
78
|
+
self.client.execute(
|
79
|
+
f"""
|
80
|
+
CREATE TABLE IF NOT EXISTS {self.test_db}.cte_articles (
|
81
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
82
|
+
title VARCHAR(100),
|
83
|
+
author_id INT,
|
84
|
+
views INT
|
85
|
+
)
|
86
|
+
"""
|
87
|
+
)
|
88
|
+
|
89
|
+
# Insert test data
|
90
|
+
self.client.execute(
|
91
|
+
f"""
|
92
|
+
INSERT INTO {self.test_db}.cte_users (name, department, salary) VALUES
|
93
|
+
('Alice', 'Engineering', 80000),
|
94
|
+
('Bob', 'Engineering', 90000),
|
95
|
+
('Charlie', 'Marketing', 70000),
|
96
|
+
('Diana', 'Marketing', 75000),
|
97
|
+
('Eve', 'Sales', 60000)
|
98
|
+
"""
|
99
|
+
)
|
100
|
+
|
101
|
+
self.client.execute(
|
102
|
+
f"""
|
103
|
+
INSERT INTO {self.test_db}.cte_articles (title, author_id, views) VALUES
|
104
|
+
('Python Tutorial', 1, 1000),
|
105
|
+
('Database Design', 1, 800),
|
106
|
+
('Marketing Strategy', 3, 600),
|
107
|
+
('Sales Tips', 5, 400),
|
108
|
+
('Advanced Python', 2, 1200)
|
109
|
+
"""
|
110
|
+
)
|
111
|
+
|
112
|
+
yield
|
113
|
+
|
114
|
+
# Cleanup
|
115
|
+
try:
|
116
|
+
self.client.execute(f"DROP DATABASE IF EXISTS {self.test_db}")
|
117
|
+
except:
|
118
|
+
pass
|
119
|
+
# Client doesn't have close method, it's automatically managed
|
120
|
+
|
121
|
+
def test_basic_cte_creation(self):
|
122
|
+
"""Test basic CTE creation from a query"""
|
123
|
+
# Create a CTE for high-salary users
|
124
|
+
high_salary_users = self.client.query(User).filter(User.salary > 75000).cte("high_salary_users")
|
125
|
+
|
126
|
+
# Verify CTE object properties
|
127
|
+
assert isinstance(high_salary_users, CTE)
|
128
|
+
assert high_salary_users.name == "high_salary_users"
|
129
|
+
assert high_salary_users.recursive == False
|
130
|
+
|
131
|
+
# Get SQL from CTE
|
132
|
+
cte_sql, cte_params = high_salary_users.as_sql()
|
133
|
+
assert "SELECT" in cte_sql
|
134
|
+
assert "cte_users" in cte_sql
|
135
|
+
assert "salary > 75000" in cte_sql
|
136
|
+
|
137
|
+
def test_cte_usage_in_query(self):
|
138
|
+
"""Test using a CTE in another query"""
|
139
|
+
# Create CTE for engineering users
|
140
|
+
engineering_users = self.client.query(User).filter(User.department == "Engineering").cte("engineering_users")
|
141
|
+
|
142
|
+
# Use CTE in another query
|
143
|
+
result = self.client.query(engineering_users).all()
|
144
|
+
|
145
|
+
# Should return 2 users (Alice and Bob)
|
146
|
+
assert len(result) == 2
|
147
|
+
# Check that we got engineering users (Alice and Bob)
|
148
|
+
# Since we're querying from CTE, we get RowData objects with '*' attribute
|
149
|
+
# The actual data is in _values tuple: (id, name, department, salary)
|
150
|
+
names = [row._values[1] for row in result] # name is at index 1
|
151
|
+
assert "Alice" in names
|
152
|
+
assert "Bob" in names
|
153
|
+
|
154
|
+
def test_multiple_ctes_in_query(self):
|
155
|
+
"""Test using multiple CTEs in a single query"""
|
156
|
+
# Create first CTE: high-salary users
|
157
|
+
high_salary = self.client.query(User).filter(User.salary > 70000).cte("high_salary")
|
158
|
+
|
159
|
+
# Create second CTE: popular articles
|
160
|
+
popular_articles = self.client.query(Article).filter(Article.views > 500).cte("popular_articles")
|
161
|
+
|
162
|
+
# Use both CTEs in a query with with_cte method
|
163
|
+
result = (
|
164
|
+
self.client.query(high_salary)
|
165
|
+
.with_cte(popular_articles)
|
166
|
+
.join(popular_articles, onclause="high_salary.id = popular_articles.author_id")
|
167
|
+
.all()
|
168
|
+
)
|
169
|
+
|
170
|
+
# Should return users who have popular articles and high salary
|
171
|
+
assert len(result) >= 0 # At least Alice (high salary + popular articles)
|
172
|
+
|
173
|
+
def test_cte_with_aggregation(self):
|
174
|
+
"""Test CTE with aggregation functions"""
|
175
|
+
# Create CTE with department statistics
|
176
|
+
dept_stats = self.client.query(User.department, User.salary).cte("dept_stats")
|
177
|
+
|
178
|
+
# Use CTE in aggregation query
|
179
|
+
result = self.client.query(dept_stats).all()
|
180
|
+
|
181
|
+
# Should return all department-salary combinations
|
182
|
+
assert len(result) == 5 # 5 users total
|
183
|
+
|
184
|
+
def test_recursive_cte_flag(self):
|
185
|
+
"""Test recursive CTE flag"""
|
186
|
+
# Create a recursive CTE (even though we won't use recursion in this test)
|
187
|
+
recursive_cte = self.client.query(User).filter(User.id == 1).cte("recursive_users", recursive=True)
|
188
|
+
|
189
|
+
assert recursive_cte.recursive == True
|
190
|
+
assert recursive_cte.name == "recursive_users"
|
191
|
+
|
192
|
+
def test_cte_in_transaction(self):
|
193
|
+
"""Test CTE usage within a transaction"""
|
194
|
+
with self.client.transaction() as tx:
|
195
|
+
# Create CTE within transaction
|
196
|
+
tx_users = tx.query(User).filter(User.department == "Marketing").cte("tx_users")
|
197
|
+
|
198
|
+
# Use CTE within transaction
|
199
|
+
result = tx.query(tx_users).all()
|
200
|
+
|
201
|
+
# Should return 2 marketing users
|
202
|
+
assert len(result) == 2
|
203
|
+
names = [row._values[1] for row in result] # name is at index 1
|
204
|
+
assert all(name in ["Charlie", "Diana"] for name in names)
|
205
|
+
|
206
|
+
def test_cte_with_join(self):
|
207
|
+
"""Test CTE with JOIN operations"""
|
208
|
+
# Create CTE for users with articles - select specific columns to avoid ambiguity
|
209
|
+
users_with_articles = (
|
210
|
+
self.client.query(User.id, User.name, User.department, User.salary)
|
211
|
+
.join("cte_articles", onclause="cte_users.id = cte_articles.author_id")
|
212
|
+
.cte("users_with_articles")
|
213
|
+
)
|
214
|
+
|
215
|
+
# Query from the CTE
|
216
|
+
result = self.client.query(users_with_articles).all()
|
217
|
+
|
218
|
+
# Should return users who have written articles
|
219
|
+
assert len(result) >= 0
|
220
|
+
# All returned users should have written at least one article
|
221
|
+
|
222
|
+
def test_cte_sql_generation(self):
|
223
|
+
"""Test that CTE generates correct SQL"""
|
224
|
+
# Create a simple CTE
|
225
|
+
simple_cte = self.client.query(User.id, User.name).filter(User.salary > 80000).cte("simple_cte")
|
226
|
+
|
227
|
+
# Get the SQL
|
228
|
+
cte_sql, params = simple_cte.as_sql()
|
229
|
+
|
230
|
+
# Verify SQL structure
|
231
|
+
assert "SELECT" in cte_sql
|
232
|
+
assert "cte_users" in cte_sql
|
233
|
+
assert "id" in cte_sql
|
234
|
+
assert "name" in cte_sql
|
235
|
+
assert "salary > 80000" in cte_sql
|
236
|
+
|
237
|
+
# Use CTE in a query and check final SQL
|
238
|
+
query = self.client.query(simple_cte)
|
239
|
+
final_sql, final_params = query._build_sql()
|
240
|
+
|
241
|
+
# Should have WITH clause
|
242
|
+
assert "WITH simple_cte AS" in final_sql
|
243
|
+
assert "SELECT * FROM simple_cte" in final_sql
|
244
|
+
|
245
|
+
def test_cte_parameter_handling(self):
|
246
|
+
"""Test that CTE parameters are handled correctly"""
|
247
|
+
# Create CTE with parameterized query
|
248
|
+
param_cte = self.client.query(User).filter(User.salary > 75000).cte("param_cte") # This should generate a parameter
|
249
|
+
|
250
|
+
# Use CTE in query
|
251
|
+
result = self.client.query(param_cte).all()
|
252
|
+
|
253
|
+
# Should return high-salary users
|
254
|
+
assert len(result) == 2 # Alice and Bob
|
255
|
+
salaries = [row._values[3] for row in result] # salary is at index 3
|
256
|
+
assert all(salary > 75000 for salary in salaries)
|
257
|
+
|
258
|
+
def test_cte_with_raw_sql(self):
|
259
|
+
"""Test CTE creation with raw SQL string"""
|
260
|
+
# Create CTE with raw SQL
|
261
|
+
raw_cte = CTE("raw_cte", f"SELECT * FROM {self.test_db}.cte_users WHERE salary > 80000")
|
262
|
+
|
263
|
+
# Use in query
|
264
|
+
result = self.client.query(raw_cte).all()
|
265
|
+
|
266
|
+
# Should return high-salary users
|
267
|
+
assert len(result) == 1 # Only Bob
|
268
|
+
assert result[0]._values[1] == "Bob" # name is at index 1
|
269
|
+
assert result[0]._values[3] > 80000 # salary is at index 3
|
@@ -0,0 +1,270 @@
|
|
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
|
+
Online tests for ORM functionality - tests actual database operations
|
17
|
+
"""
|
18
|
+
|
19
|
+
import pytest
|
20
|
+
import os
|
21
|
+
import sys
|
22
|
+
from datetime import datetime
|
23
|
+
|
24
|
+
# Add the matrixone package to the path
|
25
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
26
|
+
|
27
|
+
from matrixone import Client
|
28
|
+
from sqlalchemy import Column, Integer, String, DECIMAL
|
29
|
+
|
30
|
+
# SQLAlchemy compatibility import
|
31
|
+
try:
|
32
|
+
from sqlalchemy.orm import declarative_base
|
33
|
+
except ImportError:
|
34
|
+
from matrixone.orm import declarative_base
|
35
|
+
|
36
|
+
Base = declarative_base()
|
37
|
+
from matrixone.exceptions import QueryError
|
38
|
+
from .test_config import online_config
|
39
|
+
|
40
|
+
|
41
|
+
class User(Base):
|
42
|
+
"""Test user model"""
|
43
|
+
|
44
|
+
__tablename__ = "test_users"
|
45
|
+
|
46
|
+
id = Column(Integer, primary_key=True)
|
47
|
+
name = Column(String(100))
|
48
|
+
email = Column(String(100))
|
49
|
+
age = Column(Integer)
|
50
|
+
|
51
|
+
|
52
|
+
class Product(Base):
|
53
|
+
"""Test product model"""
|
54
|
+
|
55
|
+
__tablename__ = "test_products"
|
56
|
+
|
57
|
+
id = Column(Integer, primary_key=True)
|
58
|
+
name = Column(String(100))
|
59
|
+
price = Column(DECIMAL(10, 2))
|
60
|
+
category = Column(String(50))
|
61
|
+
|
62
|
+
|
63
|
+
class TestORMOnline:
|
64
|
+
"""Online tests for ORM functionality"""
|
65
|
+
|
66
|
+
@pytest.fixture(scope="class")
|
67
|
+
def test_client(self):
|
68
|
+
"""Create and connect Client for testing"""
|
69
|
+
host, port, user, password, database = online_config.get_connection_params()
|
70
|
+
client = Client()
|
71
|
+
client.connect(host=host, port=port, user=user, password=password, database=database)
|
72
|
+
try:
|
73
|
+
yield client
|
74
|
+
finally:
|
75
|
+
try:
|
76
|
+
client.disconnect()
|
77
|
+
except Exception as e:
|
78
|
+
print(f"Warning: Failed to disconnect client: {e}")
|
79
|
+
|
80
|
+
@pytest.fixture(scope="class")
|
81
|
+
def test_database(self, test_client):
|
82
|
+
"""Set up test database and tables"""
|
83
|
+
test_db = "test_orm_db"
|
84
|
+
|
85
|
+
try:
|
86
|
+
test_client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
|
87
|
+
test_client.execute(f"USE {test_db}")
|
88
|
+
|
89
|
+
# Create test tables using SQLAlchemy models
|
90
|
+
Base.metadata.create_all(test_client._engine)
|
91
|
+
|
92
|
+
# Insert test data
|
93
|
+
test_client.execute(
|
94
|
+
"""
|
95
|
+
INSERT INTO test_users VALUES
|
96
|
+
(1, 'John Doe', 'john@example.com', 30),
|
97
|
+
(2, 'Jane Smith', 'jane@example.com', 25),
|
98
|
+
(3, 'Bob Johnson', 'bob@example.com', 35)
|
99
|
+
"""
|
100
|
+
)
|
101
|
+
|
102
|
+
test_client.execute(
|
103
|
+
"""
|
104
|
+
INSERT INTO test_products VALUES
|
105
|
+
(1, 'Laptop', 999.99, 'Electronics'),
|
106
|
+
(2, 'Book', 19.99, 'Education'),
|
107
|
+
(3, 'Phone', 699.99, 'Electronics')
|
108
|
+
"""
|
109
|
+
)
|
110
|
+
|
111
|
+
yield test_db
|
112
|
+
|
113
|
+
finally:
|
114
|
+
# Clean up
|
115
|
+
try:
|
116
|
+
test_client.execute(f"DROP DATABASE IF EXISTS {test_db}")
|
117
|
+
except Exception as e:
|
118
|
+
print(f"Cleanup failed: {e}")
|
119
|
+
|
120
|
+
def test_model_creation_and_attributes(self):
|
121
|
+
"""Test model creation and attribute access"""
|
122
|
+
user = User(id=1, name="Test User", email="test@example.com", age=30)
|
123
|
+
|
124
|
+
assert user.id == 1
|
125
|
+
assert user.name == "Test User"
|
126
|
+
assert user.email == "test@example.com"
|
127
|
+
assert user.age == 30
|
128
|
+
|
129
|
+
# Test attribute access (SQLAlchemy models don't have to_dict by default)
|
130
|
+
assert hasattr(user, 'id')
|
131
|
+
assert hasattr(user, 'name')
|
132
|
+
assert hasattr(user, 'email')
|
133
|
+
assert hasattr(user, 'age')
|
134
|
+
|
135
|
+
def test_model_table_creation(self):
|
136
|
+
"""Test creating tables from models"""
|
137
|
+
# This test verifies that the model metadata is correctly set up
|
138
|
+
assert User.__tablename__ == "test_users"
|
139
|
+
assert Product.__tablename__ == "test_products"
|
140
|
+
|
141
|
+
# Check that columns are properly defined
|
142
|
+
assert hasattr(User, '__table__')
|
143
|
+
assert 'id' in [col.name for col in User.__table__.columns]
|
144
|
+
assert 'name' in [col.name for col in User.__table__.columns]
|
145
|
+
assert 'email' in [col.name for col in User.__table__.columns]
|
146
|
+
assert 'age' in [col.name for col in User.__table__.columns]
|
147
|
+
|
148
|
+
def test_orm_query_basic_operations(self, test_client, test_database):
|
149
|
+
"""Test basic ORM query operations"""
|
150
|
+
# Test all() method
|
151
|
+
users = test_client.query(User).all()
|
152
|
+
assert len(users) == 3
|
153
|
+
|
154
|
+
# Test first() method
|
155
|
+
first_user = test_client.query(User).first()
|
156
|
+
assert first_user is not None
|
157
|
+
assert isinstance(first_user, User)
|
158
|
+
|
159
|
+
# Test filter_by() method
|
160
|
+
john = test_client.query(User).filter_by(name="John Doe").first()
|
161
|
+
assert john is not None
|
162
|
+
assert john.name == "John Doe"
|
163
|
+
assert john.email == "john@example.com"
|
164
|
+
|
165
|
+
def test_orm_query_filtering(self, test_client, test_database):
|
166
|
+
"""Test ORM query filtering"""
|
167
|
+
# Test filter_by with multiple conditions
|
168
|
+
electronics = test_client.query(Product).filter_by(category="Electronics").all()
|
169
|
+
assert len(electronics) == 2
|
170
|
+
|
171
|
+
# Test filter() method with conditions
|
172
|
+
expensive_products = test_client.query(Product).filter("price > ?", 500).all()
|
173
|
+
assert len(expensive_products) == 2
|
174
|
+
|
175
|
+
# Test combined filtering
|
176
|
+
expensive_electronics = test_client.query(Product).filter_by(category="Electronics").filter("price > ?", 500).all()
|
177
|
+
assert len(expensive_electronics) == 2
|
178
|
+
|
179
|
+
def test_orm_query_counting(self, test_client, test_database):
|
180
|
+
"""Test ORM query counting"""
|
181
|
+
# Test count() method
|
182
|
+
total_users = test_client.query(User).count()
|
183
|
+
assert total_users == 3
|
184
|
+
|
185
|
+
# Test count() with filter
|
186
|
+
electronics_count = test_client.query(Product).filter_by(category="Electronics").count()
|
187
|
+
assert electronics_count == 2
|
188
|
+
|
189
|
+
# Test count() with complex filter
|
190
|
+
expensive_count = test_client.query(Product).filter("price > ?", 500).count()
|
191
|
+
assert expensive_count == 2
|
192
|
+
|
193
|
+
def test_orm_query_ordering(self, test_client, test_database):
|
194
|
+
"""Test ORM query ordering"""
|
195
|
+
from matrixone.orm import desc
|
196
|
+
|
197
|
+
# Test order_by ascending (default)
|
198
|
+
users_asc = test_client.query(User).order_by("age").all()
|
199
|
+
assert len(users_asc) == 3
|
200
|
+
assert users_asc[0].age <= users_asc[1].age <= users_asc[2].age
|
201
|
+
|
202
|
+
# Test order_by descending
|
203
|
+
users_desc = test_client.query(User).order_by(desc("age")).all()
|
204
|
+
assert len(users_desc) == 3
|
205
|
+
assert users_desc[0].age >= users_desc[1].age >= users_desc[2].age
|
206
|
+
|
207
|
+
def test_orm_query_limiting(self, test_client, test_database):
|
208
|
+
"""Test ORM query limiting"""
|
209
|
+
# Test limit()
|
210
|
+
limited_users = test_client.query(User).limit(2).all()
|
211
|
+
assert len(limited_users) == 2
|
212
|
+
|
213
|
+
# Test limit() with order_by
|
214
|
+
top_users = test_client.query(User).order_by("age").limit(1).all()
|
215
|
+
assert len(top_users) == 1
|
216
|
+
assert top_users[0].age == 25 # Youngest user
|
217
|
+
|
218
|
+
def test_orm_model_instantiation(self, test_client, test_database):
|
219
|
+
"""Test model instantiation from query results"""
|
220
|
+
# Get a user from database
|
221
|
+
user = test_client.query(User).filter_by(name="John Doe").first()
|
222
|
+
assert user is not None
|
223
|
+
assert isinstance(user, User)
|
224
|
+
assert user.id == 1
|
225
|
+
assert user.name == "John Doe"
|
226
|
+
assert user.email == "john@example.com"
|
227
|
+
assert user.age == 30
|
228
|
+
|
229
|
+
# Test that we can access all attributes
|
230
|
+
assert hasattr(user, 'id')
|
231
|
+
assert hasattr(user, 'name')
|
232
|
+
assert hasattr(user, 'email')
|
233
|
+
assert hasattr(user, 'age')
|
234
|
+
|
235
|
+
def test_orm_error_handling(self, test_client, test_database):
|
236
|
+
"""Test ORM error handling"""
|
237
|
+
# Test querying non-existent table
|
238
|
+
with pytest.raises(QueryError):
|
239
|
+
test_client.query(User).filter("invalid_column = ?", "value").all()
|
240
|
+
|
241
|
+
# Test invalid filter syntax
|
242
|
+
with pytest.raises(QueryError):
|
243
|
+
test_client.query(User).filter("invalid_syntax").all()
|
244
|
+
|
245
|
+
def test_orm_complex_queries(self, test_client, test_database):
|
246
|
+
"""Test complex ORM queries"""
|
247
|
+
# Test multiple filters
|
248
|
+
result = (
|
249
|
+
test_client.query(Product)
|
250
|
+
.filter_by(category="Electronics")
|
251
|
+
.filter("price > ?", 500)
|
252
|
+
.order_by("price")
|
253
|
+
.limit(1)
|
254
|
+
.all()
|
255
|
+
)
|
256
|
+
|
257
|
+
assert len(result) == 1
|
258
|
+
assert result[0].category == "Electronics"
|
259
|
+
assert result[0].price > 500
|
260
|
+
|
261
|
+
# Test chaining multiple operations
|
262
|
+
users = test_client.query(User).filter("age > ?", 25).order_by("age").limit(2).all()
|
263
|
+
|
264
|
+
assert len(users) == 2
|
265
|
+
assert all(user.age > 25 for user in users)
|
266
|
+
assert users[0].age <= users[1].age # Should be ordered by age
|
267
|
+
|
268
|
+
|
269
|
+
if __name__ == "__main__":
|
270
|
+
pytest.main([__file__])
|