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.
Files changed (122) hide show
  1. matrixone/__init__.py +155 -0
  2. matrixone/account.py +723 -0
  3. matrixone/async_client.py +3913 -0
  4. matrixone/async_metadata_manager.py +311 -0
  5. matrixone/async_orm.py +123 -0
  6. matrixone/async_vector_index_manager.py +633 -0
  7. matrixone/base_client.py +208 -0
  8. matrixone/client.py +4672 -0
  9. matrixone/config.py +452 -0
  10. matrixone/connection_hooks.py +286 -0
  11. matrixone/exceptions.py +89 -0
  12. matrixone/logger.py +782 -0
  13. matrixone/metadata.py +820 -0
  14. matrixone/moctl.py +219 -0
  15. matrixone/orm.py +2277 -0
  16. matrixone/pitr.py +646 -0
  17. matrixone/pubsub.py +771 -0
  18. matrixone/restore.py +411 -0
  19. matrixone/search_vector_index.py +1176 -0
  20. matrixone/snapshot.py +550 -0
  21. matrixone/sql_builder.py +844 -0
  22. matrixone/sqlalchemy_ext/__init__.py +161 -0
  23. matrixone/sqlalchemy_ext/adapters.py +163 -0
  24. matrixone/sqlalchemy_ext/dialect.py +534 -0
  25. matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
  26. matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
  27. matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
  28. matrixone/sqlalchemy_ext/ivf_config.py +252 -0
  29. matrixone/sqlalchemy_ext/table_builder.py +351 -0
  30. matrixone/sqlalchemy_ext/vector_index.py +1721 -0
  31. matrixone/sqlalchemy_ext/vector_type.py +948 -0
  32. matrixone/version.py +580 -0
  33. matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
  34. matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
  35. matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
  36. matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
  37. matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
  38. matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
  39. tests/__init__.py +19 -0
  40. tests/offline/__init__.py +20 -0
  41. tests/offline/conftest.py +77 -0
  42. tests/offline/test_account.py +703 -0
  43. tests/offline/test_async_client_query_comprehensive.py +1218 -0
  44. tests/offline/test_basic.py +54 -0
  45. tests/offline/test_case_sensitivity.py +227 -0
  46. tests/offline/test_connection_hooks_offline.py +287 -0
  47. tests/offline/test_dialect_schema_handling.py +609 -0
  48. tests/offline/test_explain_methods.py +346 -0
  49. tests/offline/test_filter_logical_in.py +237 -0
  50. tests/offline/test_fulltext_search_comprehensive.py +795 -0
  51. tests/offline/test_ivf_config.py +249 -0
  52. tests/offline/test_join_methods.py +281 -0
  53. tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
  54. tests/offline/test_logical_in_method.py +237 -0
  55. tests/offline/test_matrixone_version_parsing.py +264 -0
  56. tests/offline/test_metadata_offline.py +557 -0
  57. tests/offline/test_moctl.py +300 -0
  58. tests/offline/test_moctl_simple.py +251 -0
  59. tests/offline/test_model_support_offline.py +359 -0
  60. tests/offline/test_model_support_simple.py +225 -0
  61. tests/offline/test_pinecone_filter_offline.py +377 -0
  62. tests/offline/test_pitr.py +585 -0
  63. tests/offline/test_pubsub.py +712 -0
  64. tests/offline/test_query_update.py +283 -0
  65. tests/offline/test_restore.py +445 -0
  66. tests/offline/test_snapshot_comprehensive.py +384 -0
  67. tests/offline/test_sql_escaping_edge_cases.py +551 -0
  68. tests/offline/test_sqlalchemy_integration.py +382 -0
  69. tests/offline/test_sqlalchemy_vector_integration.py +434 -0
  70. tests/offline/test_table_builder.py +198 -0
  71. tests/offline/test_unified_filter.py +398 -0
  72. tests/offline/test_unified_transaction.py +495 -0
  73. tests/offline/test_vector_index.py +238 -0
  74. tests/offline/test_vector_operations.py +688 -0
  75. tests/offline/test_vector_type.py +174 -0
  76. tests/offline/test_version_core.py +328 -0
  77. tests/offline/test_version_management.py +372 -0
  78. tests/offline/test_version_standalone.py +652 -0
  79. tests/online/__init__.py +20 -0
  80. tests/online/conftest.py +216 -0
  81. tests/online/test_account_management.py +194 -0
  82. tests/online/test_advanced_features.py +344 -0
  83. tests/online/test_async_client_interfaces.py +330 -0
  84. tests/online/test_async_client_online.py +285 -0
  85. tests/online/test_async_model_insert_online.py +293 -0
  86. tests/online/test_async_orm_online.py +300 -0
  87. tests/online/test_async_simple_query_online.py +802 -0
  88. tests/online/test_async_transaction_simple_query.py +300 -0
  89. tests/online/test_basic_connection.py +130 -0
  90. tests/online/test_client_online.py +238 -0
  91. tests/online/test_config.py +90 -0
  92. tests/online/test_config_validation.py +123 -0
  93. tests/online/test_connection_hooks_new_online.py +217 -0
  94. tests/online/test_dialect_schema_handling_online.py +331 -0
  95. tests/online/test_filter_logical_in_online.py +374 -0
  96. tests/online/test_fulltext_comprehensive.py +1773 -0
  97. tests/online/test_fulltext_label_online.py +433 -0
  98. tests/online/test_fulltext_search_online.py +842 -0
  99. tests/online/test_ivf_stats_online.py +506 -0
  100. tests/online/test_logger_integration.py +311 -0
  101. tests/online/test_matrixone_query_orm.py +540 -0
  102. tests/online/test_metadata_online.py +579 -0
  103. tests/online/test_model_insert_online.py +255 -0
  104. tests/online/test_mysql_driver_validation.py +213 -0
  105. tests/online/test_orm_advanced_features.py +2022 -0
  106. tests/online/test_orm_cte_integration.py +269 -0
  107. tests/online/test_orm_online.py +270 -0
  108. tests/online/test_pinecone_filter.py +708 -0
  109. tests/online/test_pubsub_operations.py +352 -0
  110. tests/online/test_query_methods.py +225 -0
  111. tests/online/test_query_update_online.py +433 -0
  112. tests/online/test_search_vector_index.py +557 -0
  113. tests/online/test_simple_fulltext_online.py +915 -0
  114. tests/online/test_snapshot_comprehensive.py +998 -0
  115. tests/online/test_sqlalchemy_engine_integration.py +336 -0
  116. tests/online/test_sqlalchemy_integration.py +425 -0
  117. tests/online/test_transaction_contexts.py +1219 -0
  118. tests/online/test_transaction_insert_methods.py +356 -0
  119. tests/online/test_transaction_query_methods.py +288 -0
  120. tests/online/test_unified_filter_online.py +529 -0
  121. tests/online/test_vector_comprehensive.py +706 -0
  122. 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}")