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,249 @@
|
|
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
|
+
Offline tests for IVF configuration functionality.
|
17
|
+
"""
|
18
|
+
|
19
|
+
import pytest
|
20
|
+
from unittest.mock import Mock, patch
|
21
|
+
from sqlalchemy import text
|
22
|
+
|
23
|
+
from matrixone.sqlalchemy_ext import (
|
24
|
+
IVFConfig,
|
25
|
+
create_ivf_config,
|
26
|
+
enable_ivf_indexing,
|
27
|
+
disable_ivf_indexing,
|
28
|
+
set_probe_limit,
|
29
|
+
get_ivf_status,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
class TestIVFConfigOffline:
|
34
|
+
"""Test IVF configuration functionality using mocks."""
|
35
|
+
|
36
|
+
def _create_mock_engine_with_conn(self, mock_conn):
|
37
|
+
"""Helper to create a mock engine with connection."""
|
38
|
+
# Ensure mock_conn has exec_driver_sql if not already set
|
39
|
+
# This allows individual tests to customize the mock before calling this helper
|
40
|
+
if not hasattr(mock_conn, 'exec_driver_sql') or mock_conn.exec_driver_sql is None:
|
41
|
+
mock_conn.exec_driver_sql = Mock(return_value=Mock(returns_rows=False, rowcount=0))
|
42
|
+
|
43
|
+
mock_engine = Mock()
|
44
|
+
mock_context = Mock()
|
45
|
+
mock_context.__enter__ = Mock(return_value=mock_conn)
|
46
|
+
mock_context.__exit__ = Mock(return_value=None)
|
47
|
+
mock_engine.begin.return_value = mock_context
|
48
|
+
return mock_engine
|
49
|
+
|
50
|
+
def test_ivf_config_creation(self):
|
51
|
+
"""Test creating IVF configuration manager."""
|
52
|
+
mock_engine = Mock()
|
53
|
+
config = IVFConfig(mock_engine)
|
54
|
+
|
55
|
+
assert config.engine == mock_engine
|
56
|
+
|
57
|
+
def test_create_ivf_config_helper(self):
|
58
|
+
"""Test create_ivf_config helper function."""
|
59
|
+
mock_engine = Mock()
|
60
|
+
config = create_ivf_config(mock_engine)
|
61
|
+
|
62
|
+
assert isinstance(config, IVFConfig)
|
63
|
+
assert config.engine == mock_engine
|
64
|
+
|
65
|
+
def test_enable_ivf_indexing(self):
|
66
|
+
"""Test enabling IVF indexing."""
|
67
|
+
mock_conn = Mock()
|
68
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
69
|
+
|
70
|
+
config = IVFConfig(mock_engine)
|
71
|
+
result = config.enable_ivf_indexing()
|
72
|
+
|
73
|
+
assert result is True
|
74
|
+
# Check that exec_driver_sql was called with the correct SQL
|
75
|
+
mock_conn.exec_driver_sql.assert_called_once()
|
76
|
+
call_args = mock_conn.exec_driver_sql.call_args[0][0]
|
77
|
+
assert call_args == "SET experimental_ivf_index = 1"
|
78
|
+
|
79
|
+
def test_disable_ivf_indexing(self):
|
80
|
+
"""Test disabling IVF indexing."""
|
81
|
+
mock_conn = Mock()
|
82
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
83
|
+
|
84
|
+
config = IVFConfig(mock_engine)
|
85
|
+
result = config.disable_ivf_indexing()
|
86
|
+
|
87
|
+
assert result is True
|
88
|
+
# Check that execute was called with the correct SQL
|
89
|
+
mock_conn.exec_driver_sql.assert_called_once()
|
90
|
+
call_args = mock_conn.exec_driver_sql.call_args[0][0]
|
91
|
+
assert call_args == "SET experimental_ivf_index = 0"
|
92
|
+
|
93
|
+
def test_set_probe_limit(self):
|
94
|
+
"""Test setting probe limit."""
|
95
|
+
mock_conn = Mock()
|
96
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
97
|
+
|
98
|
+
config = IVFConfig(mock_engine)
|
99
|
+
result = config.set_probe_limit(5)
|
100
|
+
|
101
|
+
assert result is True
|
102
|
+
# Check that execute was called with the correct SQL
|
103
|
+
mock_conn.exec_driver_sql.assert_called_once()
|
104
|
+
call_args = mock_conn.exec_driver_sql.call_args[0][0]
|
105
|
+
assert call_args == "SET probe_limit = 5"
|
106
|
+
|
107
|
+
def test_configure_ivf(self):
|
108
|
+
"""Test configuring IVF with multiple parameters."""
|
109
|
+
mock_conn = Mock()
|
110
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
111
|
+
|
112
|
+
config = IVFConfig(mock_engine)
|
113
|
+
result = config.configure_ivf(enabled=True, probe_limit=3)
|
114
|
+
|
115
|
+
assert result is True
|
116
|
+
# Should call both enable and set probe limit
|
117
|
+
assert mock_conn.exec_driver_sql.call_count == 2
|
118
|
+
|
119
|
+
# Check the SQL calls
|
120
|
+
calls = [call[0][0] for call in mock_conn.exec_driver_sql.call_args_list]
|
121
|
+
assert "SET experimental_ivf_index = 1" in calls
|
122
|
+
assert "SET probe_limit = 3" in calls
|
123
|
+
|
124
|
+
def test_get_ivf_status(self):
|
125
|
+
"""Test getting IVF status."""
|
126
|
+
mock_conn = Mock()
|
127
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
128
|
+
|
129
|
+
# Mock the SHOW VARIABLES results
|
130
|
+
mock_result1 = Mock()
|
131
|
+
mock_result1.fetchone.return_value = ("experimental_ivf_index", "1")
|
132
|
+
mock_result2 = Mock()
|
133
|
+
mock_result2.fetchone.return_value = ("probe_limit", "5")
|
134
|
+
|
135
|
+
mock_conn.exec_driver_sql.side_effect = [mock_result1, mock_result2]
|
136
|
+
|
137
|
+
config = IVFConfig(mock_engine)
|
138
|
+
status = config.get_ivf_status()
|
139
|
+
|
140
|
+
assert status["ivf_enabled"] is True
|
141
|
+
assert status["probe_limit"] == 5
|
142
|
+
assert status["error"] is None
|
143
|
+
|
144
|
+
def test_is_ivf_supported(self):
|
145
|
+
"""Test checking if IVF is supported."""
|
146
|
+
mock_conn = Mock()
|
147
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
148
|
+
|
149
|
+
config = IVFConfig(mock_engine)
|
150
|
+
result = config.is_ivf_supported()
|
151
|
+
|
152
|
+
assert result is True
|
153
|
+
# Check that execute was called with the correct SQL
|
154
|
+
mock_conn.exec_driver_sql.assert_called_once()
|
155
|
+
call_args = mock_conn.exec_driver_sql.call_args[0][0]
|
156
|
+
assert call_args == "SHOW VARIABLES LIKE 'experimental_ivf_index'"
|
157
|
+
|
158
|
+
def test_is_ivf_supported_failure(self):
|
159
|
+
"""Test checking if IVF is supported when it fails."""
|
160
|
+
mock_engine = Mock()
|
161
|
+
mock_engine.begin.side_effect = Exception("Not supported")
|
162
|
+
|
163
|
+
config = IVFConfig(mock_engine)
|
164
|
+
result = config.is_ivf_supported()
|
165
|
+
|
166
|
+
assert result is False
|
167
|
+
|
168
|
+
def test_enable_ivf_indexing_failure(self):
|
169
|
+
"""Test enabling IVF indexing when it fails."""
|
170
|
+
mock_engine = Mock()
|
171
|
+
mock_engine.begin.side_effect = Exception("Connection failed")
|
172
|
+
|
173
|
+
config = IVFConfig(mock_engine)
|
174
|
+
result = config.enable_ivf_indexing()
|
175
|
+
|
176
|
+
assert result is False
|
177
|
+
|
178
|
+
def test_disable_ivf_indexing_failure(self):
|
179
|
+
"""Test disabling IVF indexing when it fails."""
|
180
|
+
mock_engine = Mock()
|
181
|
+
mock_engine.begin.side_effect = Exception("Connection failed")
|
182
|
+
|
183
|
+
config = IVFConfig(mock_engine)
|
184
|
+
result = config.disable_ivf_indexing()
|
185
|
+
|
186
|
+
assert result is False
|
187
|
+
|
188
|
+
def test_set_probe_limit_failure(self):
|
189
|
+
"""Test setting probe limit when it fails."""
|
190
|
+
mock_engine = Mock()
|
191
|
+
mock_engine.begin.side_effect = Exception("Connection failed")
|
192
|
+
|
193
|
+
config = IVFConfig(mock_engine)
|
194
|
+
result = config.set_probe_limit(5)
|
195
|
+
|
196
|
+
assert result is False
|
197
|
+
|
198
|
+
def test_get_ivf_status_with_error(self):
|
199
|
+
"""Test getting IVF status when there's an error."""
|
200
|
+
mock_engine = Mock()
|
201
|
+
mock_engine.begin.side_effect = Exception("Connection failed")
|
202
|
+
|
203
|
+
config = IVFConfig(mock_engine)
|
204
|
+
status = config.get_ivf_status()
|
205
|
+
|
206
|
+
assert status["ivf_enabled"] is None
|
207
|
+
assert status["probe_limit"] is None
|
208
|
+
assert status["error"] == "Connection failed"
|
209
|
+
|
210
|
+
def test_configure_ivf_partial_failure(self):
|
211
|
+
"""Test configuring IVF when some operations fail."""
|
212
|
+
mock_conn = Mock()
|
213
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
214
|
+
|
215
|
+
# First call succeeds, second call fails
|
216
|
+
mock_conn.exec_driver_sql.side_effect = [None, Exception("Failed")]
|
217
|
+
|
218
|
+
config = IVFConfig(mock_engine)
|
219
|
+
result = config.configure_ivf(enabled=True, probe_limit=3)
|
220
|
+
|
221
|
+
assert result is False # Should be False because one operation failed
|
222
|
+
|
223
|
+
def test_convenience_functions(self):
|
224
|
+
"""Test convenience functions."""
|
225
|
+
mock_conn = Mock()
|
226
|
+
mock_engine = self._create_mock_engine_with_conn(mock_conn)
|
227
|
+
|
228
|
+
# Test enable_ivf_indexing
|
229
|
+
result = enable_ivf_indexing(mock_engine)
|
230
|
+
assert result is True
|
231
|
+
|
232
|
+
# Test disable_ivf_indexing
|
233
|
+
result = disable_ivf_indexing(mock_engine)
|
234
|
+
assert result is True
|
235
|
+
|
236
|
+
# Test set_probe_limit
|
237
|
+
result = set_probe_limit(mock_engine, 10)
|
238
|
+
assert result is True
|
239
|
+
|
240
|
+
# Test get_ivf_status
|
241
|
+
mock_result1 = Mock()
|
242
|
+
mock_result1.fetchone.return_value = ("experimental_ivf_index", "0")
|
243
|
+
mock_result2 = Mock()
|
244
|
+
mock_result2.fetchone.return_value = ("probe_limit", "10")
|
245
|
+
mock_conn.exec_driver_sql.side_effect = [mock_result1, mock_result2]
|
246
|
+
|
247
|
+
status = get_ivf_status(mock_engine)
|
248
|
+
assert status["ivf_enabled"] is False
|
249
|
+
assert status["probe_limit"] == 10
|
@@ -0,0 +1,281 @@
|
|
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 alignment with SQLAlchemy ORM
|
19
|
+
|
20
|
+
This test file verifies that MatrixOne's join methods behave exactly like SQLAlchemy ORM.
|
21
|
+
"""
|
22
|
+
|
23
|
+
import sys
|
24
|
+
import os
|
25
|
+
import unittest
|
26
|
+
from unittest.mock import Mock
|
27
|
+
|
28
|
+
# Add the matrixone package to the path
|
29
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
30
|
+
|
31
|
+
from matrixone.orm import BaseMatrixOneQuery, MatrixOneQuery, declarative_base
|
32
|
+
from sqlalchemy import Column, Integer, String, create_engine
|
33
|
+
|
34
|
+
Base = declarative_base()
|
35
|
+
|
36
|
+
|
37
|
+
class User(Base):
|
38
|
+
__tablename__ = 'users'
|
39
|
+
id = Column(Integer, primary_key=True)
|
40
|
+
name = Column(String(50))
|
41
|
+
department_id = Column(Integer)
|
42
|
+
|
43
|
+
|
44
|
+
class Address(Base):
|
45
|
+
__tablename__ = 'addresses'
|
46
|
+
id = Column(Integer, primary_key=True)
|
47
|
+
user_id = Column(Integer)
|
48
|
+
email = Column(String(100))
|
49
|
+
|
50
|
+
|
51
|
+
class Department(Base):
|
52
|
+
__tablename__ = 'departments'
|
53
|
+
id = Column(Integer, primary_key=True)
|
54
|
+
name = Column(String(50))
|
55
|
+
|
56
|
+
|
57
|
+
class TestJoinMethods(unittest.TestCase):
|
58
|
+
"""Test join methods alignment with SQLAlchemy ORM"""
|
59
|
+
|
60
|
+
def setUp(self):
|
61
|
+
"""Set up test fixtures"""
|
62
|
+
self.mock_client = Mock()
|
63
|
+
self.query = MatrixOneQuery(User, self.mock_client)
|
64
|
+
self.query._table_name = 'users'
|
65
|
+
|
66
|
+
def test_join_default_inner_join(self):
|
67
|
+
"""Test that join() defaults to INNER JOIN"""
|
68
|
+
self.query.join(Address, User.id == Address.user_id)
|
69
|
+
|
70
|
+
self.assertEqual(len(self.query._joins), 1)
|
71
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
72
|
+
self.assertIn("addresses", self.query._joins[0])
|
73
|
+
self.assertIn("id = user_id", self.query._joins[0])
|
74
|
+
|
75
|
+
def test_join_with_string_condition(self):
|
76
|
+
"""Test join with string condition"""
|
77
|
+
self.query.join('addresses', 'users.id = addresses.user_id')
|
78
|
+
|
79
|
+
self.assertEqual(len(self.query._joins), 1)
|
80
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
81
|
+
self.assertIn("addresses", self.query._joins[0])
|
82
|
+
self.assertIn("users.id = addresses.user_id", self.query._joins[0])
|
83
|
+
|
84
|
+
def test_join_with_onclause_parameter(self):
|
85
|
+
"""Test join with onclause parameter"""
|
86
|
+
self.query.join('addresses', onclause='users.id = addresses.user_id')
|
87
|
+
|
88
|
+
self.assertEqual(len(self.query._joins), 1)
|
89
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
90
|
+
self.assertIn("addresses", self.query._joins[0])
|
91
|
+
self.assertIn("users.id = addresses.user_id", self.query._joins[0])
|
92
|
+
|
93
|
+
def test_join_with_sqlalchemy_expression(self):
|
94
|
+
"""Test join with SQLAlchemy expression"""
|
95
|
+
self.query.join(Address, User.id == Address.user_id)
|
96
|
+
|
97
|
+
self.assertEqual(len(self.query._joins), 1)
|
98
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
99
|
+
self.assertIn("addresses", self.query._joins[0])
|
100
|
+
# SQLAlchemy expressions get compiled and table prefixes are removed
|
101
|
+
self.assertIn("id = user_id", self.query._joins[0])
|
102
|
+
|
103
|
+
def test_join_with_model_class_target(self):
|
104
|
+
"""Test join with model class as target"""
|
105
|
+
self.query.join(Address, 'users.id = addresses.user_id')
|
106
|
+
|
107
|
+
self.assertEqual(len(self.query._joins), 1)
|
108
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
109
|
+
self.assertIn("addresses", self.query._joins[0])
|
110
|
+
|
111
|
+
def test_join_without_onclause(self):
|
112
|
+
"""Test join without onclause (should still work)"""
|
113
|
+
self.query.join(Address)
|
114
|
+
|
115
|
+
self.assertEqual(len(self.query._joins), 1)
|
116
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
117
|
+
self.assertIn("addresses", self.query._joins[0])
|
118
|
+
self.assertNotIn("ON", self.query._joins[0])
|
119
|
+
|
120
|
+
def test_join_with_isouter_true(self):
|
121
|
+
"""Test join with isouter=True creates LEFT OUTER JOIN"""
|
122
|
+
self.query.join(Address, User.id == Address.user_id, isouter=True)
|
123
|
+
|
124
|
+
self.assertEqual(len(self.query._joins), 1)
|
125
|
+
self.assertTrue(self.query._joins[0].startswith("LEFT OUTER JOIN"))
|
126
|
+
self.assertIn("addresses", self.query._joins[0])
|
127
|
+
|
128
|
+
def test_join_with_full_true(self):
|
129
|
+
"""Test join with full=True creates FULL OUTER JOIN"""
|
130
|
+
self.query.join(Address, User.id == Address.user_id, full=True)
|
131
|
+
|
132
|
+
self.assertEqual(len(self.query._joins), 1)
|
133
|
+
self.assertTrue(self.query._joins[0].startswith("FULL OUTER JOIN"))
|
134
|
+
self.assertIn("addresses", self.query._joins[0])
|
135
|
+
|
136
|
+
def test_innerjoin_method(self):
|
137
|
+
"""Test innerjoin method"""
|
138
|
+
self.query.innerjoin(Address, User.id == Address.user_id)
|
139
|
+
|
140
|
+
self.assertEqual(len(self.query._joins), 1)
|
141
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
142
|
+
self.assertIn("addresses", self.query._joins[0])
|
143
|
+
|
144
|
+
def test_leftjoin_method(self):
|
145
|
+
"""Test leftjoin method"""
|
146
|
+
self.query.leftjoin(Address, User.id == Address.user_id)
|
147
|
+
|
148
|
+
self.assertEqual(len(self.query._joins), 1)
|
149
|
+
self.assertTrue(self.query._joins[0].startswith("LEFT OUTER JOIN"))
|
150
|
+
self.assertIn("addresses", self.query._joins[0])
|
151
|
+
|
152
|
+
def test_outerjoin_method(self):
|
153
|
+
"""Test outerjoin method (should be same as leftjoin)"""
|
154
|
+
self.query.outerjoin(Address, User.id == Address.user_id)
|
155
|
+
|
156
|
+
self.assertEqual(len(self.query._joins), 1)
|
157
|
+
self.assertTrue(self.query._joins[0].startswith("LEFT OUTER JOIN"))
|
158
|
+
self.assertIn("addresses", self.query._joins[0])
|
159
|
+
|
160
|
+
def test_fullouterjoin_method(self):
|
161
|
+
"""Test fullouterjoin method"""
|
162
|
+
self.query.fullouterjoin(Address, User.id == Address.user_id)
|
163
|
+
|
164
|
+
self.assertEqual(len(self.query._joins), 1)
|
165
|
+
self.assertTrue(self.query._joins[0].startswith("FULL OUTER JOIN"))
|
166
|
+
self.assertIn("addresses", self.query._joins[0])
|
167
|
+
|
168
|
+
def test_multiple_joins(self):
|
169
|
+
"""Test multiple joins in one query"""
|
170
|
+
self.query.join(Address, User.id == Address.user_id)
|
171
|
+
self.query.join(Department, User.department_id == Department.id)
|
172
|
+
|
173
|
+
self.assertEqual(len(self.query._joins), 2)
|
174
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
175
|
+
self.assertTrue(self.query._joins[1].startswith("INNER JOIN"))
|
176
|
+
self.assertIn("addresses", self.query._joins[0])
|
177
|
+
self.assertIn("departments", self.query._joins[1])
|
178
|
+
|
179
|
+
def test_join_method_chaining(self):
|
180
|
+
"""Test that join methods support method chaining"""
|
181
|
+
result = (
|
182
|
+
self.query.join(Address, User.id == Address.user_id)
|
183
|
+
.join(Department, User.department_id == Department.id)
|
184
|
+
.filter(User.name == 'John')
|
185
|
+
)
|
186
|
+
|
187
|
+
self.assertEqual(result, self.query) # Should return self for chaining
|
188
|
+
self.assertEqual(len(self.query._joins), 2)
|
189
|
+
self.assertEqual(len(self.query._where_conditions), 1)
|
190
|
+
|
191
|
+
def test_join_with_cte(self):
|
192
|
+
"""Test join with CTE object"""
|
193
|
+
# Create a CTE
|
194
|
+
cte = self.query.filter(User.department_id == 1).cte("engineering_users")
|
195
|
+
|
196
|
+
# Create new query and join with CTE
|
197
|
+
new_query = MatrixOneQuery(User, self.mock_client)
|
198
|
+
new_query._table_name = 'users'
|
199
|
+
new_query.join(cte, 'users.id = engineering_users.id')
|
200
|
+
|
201
|
+
self.assertEqual(len(new_query._joins), 1)
|
202
|
+
self.assertTrue(new_query._joins[0].startswith("INNER JOIN"))
|
203
|
+
self.assertIn("engineering_users", new_query._joins[0])
|
204
|
+
|
205
|
+
def test_join_with_string_table_name(self):
|
206
|
+
"""Test join with string table name"""
|
207
|
+
self.query.join('custom_table', 'users.id = custom_table.user_id')
|
208
|
+
|
209
|
+
self.assertEqual(len(self.query._joins), 1)
|
210
|
+
self.assertTrue(self.query._joins[0].startswith("INNER JOIN"))
|
211
|
+
self.assertIn("custom_table", self.query._joins[0])
|
212
|
+
self.assertIn("users.id = custom_table.user_id", self.query._joins[0])
|
213
|
+
|
214
|
+
def test_join_sqlalchemy_expression_compilation(self):
|
215
|
+
"""Test that SQLAlchemy expressions are properly compiled"""
|
216
|
+
# Test with complex expression
|
217
|
+
self.query.join(Address, (User.id == Address.user_id) & (Address.email.like('%@example.com')))
|
218
|
+
|
219
|
+
self.assertEqual(len(self.query._joins), 1)
|
220
|
+
join_clause = self.query._joins[0]
|
221
|
+
self.assertTrue(join_clause.startswith("INNER JOIN"))
|
222
|
+
self.assertIn("addresses", join_clause)
|
223
|
+
# The complex expression should be compiled to SQL
|
224
|
+
self.assertIn("ON", join_clause)
|
225
|
+
|
226
|
+
def test_join_parameter_consistency(self):
|
227
|
+
"""Test that all join methods use consistent parameter names"""
|
228
|
+
# Test that onclause parameter works consistently across all methods
|
229
|
+
methods_to_test = [
|
230
|
+
('join', lambda q: q.join('addresses', onclause='users.id = addresses.user_id')),
|
231
|
+
(
|
232
|
+
'innerjoin',
|
233
|
+
lambda q: q.innerjoin('addresses', onclause='users.id = addresses.user_id'),
|
234
|
+
),
|
235
|
+
(
|
236
|
+
'leftjoin',
|
237
|
+
lambda q: q.leftjoin('addresses', onclause='users.id = addresses.user_id'),
|
238
|
+
),
|
239
|
+
(
|
240
|
+
'outerjoin',
|
241
|
+
lambda q: q.outerjoin('addresses', onclause='users.id = addresses.user_id'),
|
242
|
+
),
|
243
|
+
(
|
244
|
+
'fullouterjoin',
|
245
|
+
lambda q: q.fullouterjoin('addresses', onclause='users.id = addresses.user_id'),
|
246
|
+
),
|
247
|
+
]
|
248
|
+
|
249
|
+
for method_name, method_call in methods_to_test:
|
250
|
+
with self.subTest(method=method_name):
|
251
|
+
query = MatrixOneQuery(User, self.mock_client)
|
252
|
+
query._table_name = 'users'
|
253
|
+
|
254
|
+
result = method_call(query)
|
255
|
+
|
256
|
+
# Should return self for chaining
|
257
|
+
self.assertEqual(result, query)
|
258
|
+
# Should have one join
|
259
|
+
self.assertEqual(len(query._joins), 1)
|
260
|
+
# Should contain the condition
|
261
|
+
self.assertIn("users.id = addresses.user_id", query._joins[0])
|
262
|
+
|
263
|
+
|
264
|
+
if __name__ == '__main__':
|
265
|
+
# Create test suite
|
266
|
+
test_suite = unittest.TestSuite()
|
267
|
+
|
268
|
+
# Add all test methods
|
269
|
+
test_suite.addTest(unittest.makeSuite(TestJoinMethods))
|
270
|
+
|
271
|
+
# Run the tests
|
272
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
273
|
+
result = runner.run(test_suite)
|
274
|
+
|
275
|
+
# Print summary
|
276
|
+
print(f"\n{'='*50}")
|
277
|
+
print(f"Tests run: {result.testsRun}")
|
278
|
+
print(f"Failures: {len(result.failures)}")
|
279
|
+
print(f"Errors: {len(result.errors)}")
|
280
|
+
print(f"Success rate: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%")
|
281
|
+
print(f"{'='*50}")
|