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,300 @@
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 async transaction simple_query functionality
17
+ """
18
+
19
+ import pytest
20
+ import pytest_asyncio
21
+ from matrixone import AsyncClient
22
+ from matrixone.sqlalchemy_ext import boolean_match, natural_match
23
+
24
+
25
+ class TestAsyncTransactionSimpleQuery:
26
+ """Test async transaction simple_query operations"""
27
+
28
+ @pytest_asyncio.fixture(scope="function")
29
+ async def async_client_setup(self):
30
+ """Setup async client for testing"""
31
+ client = AsyncClient()
32
+ await client.connect("127.0.0.1", 6001, "root", "111", "test")
33
+
34
+ # Enable fulltext indexing
35
+ await client.execute("SET experimental_fulltext_index=1")
36
+
37
+ # Create test database and table
38
+ test_db = "async_tx_simple_query_test"
39
+ await client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
40
+ await client.execute(f"USE {test_db}")
41
+
42
+ await client.execute("DROP TABLE IF EXISTS async_tx_docs")
43
+ await client.execute(
44
+ """
45
+ CREATE TABLE async_tx_docs (
46
+ id INT AUTO_INCREMENT PRIMARY KEY,
47
+ title VARCHAR(255) NOT NULL,
48
+ content TEXT NOT NULL,
49
+ category VARCHAR(100) NOT NULL
50
+ )
51
+ """
52
+ )
53
+
54
+ # Insert test data
55
+ test_docs = [
56
+ (
57
+ "Python Async Programming",
58
+ "Learn Python async programming with asyncio",
59
+ "Programming",
60
+ ),
61
+ (
62
+ "JavaScript Async Patterns",
63
+ "Modern JavaScript async patterns and promises",
64
+ "Programming",
65
+ ),
66
+ (
67
+ "Database Async Operations",
68
+ "Async database operations and connection pooling",
69
+ "Database",
70
+ ),
71
+ ("Web Async Development", "Async web development with modern frameworks", "Web"),
72
+ (
73
+ "Async Testing Strategies",
74
+ "Testing async code and handling async test cases",
75
+ "Testing",
76
+ ),
77
+ ]
78
+
79
+ for title, content, category in test_docs:
80
+ await client.execute(
81
+ f"""
82
+ INSERT INTO async_tx_docs (title, content, category)
83
+ VALUES ('{title}', '{content}', '{category}')
84
+ """
85
+ )
86
+
87
+ # Create fulltext index
88
+ await client.fulltext_index.create("async_tx_docs", "ftidx_async_tx", ["title", "content"])
89
+
90
+ yield client, test_db
91
+
92
+ # Cleanup
93
+ try:
94
+ await client.fulltext_index.drop("async_tx_docs", "ftidx_async_tx")
95
+ await client.execute("DROP TABLE async_tx_docs")
96
+ await client.execute(f"DROP DATABASE {test_db}")
97
+ except Exception as e:
98
+ print(f"Cleanup warning: {e}")
99
+ finally:
100
+ await client.disconnect()
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_async_transaction_simple_query_basic(self, async_client_setup):
104
+ """Test basic async transaction simple_query functionality"""
105
+ client, test_db = async_client_setup
106
+
107
+ async with client.transaction() as tx:
108
+ # Test basic search within transaction
109
+ result = await tx.query("async_tx_docs").filter(boolean_match("title", "content").encourage("python")).execute()
110
+
111
+ assert result is not None
112
+ rows = result.fetchall()
113
+ assert len(rows) > 0
114
+
115
+ # Verify results contain "python"
116
+ for row in rows:
117
+ title_content = f"{row[0]} {row[1]}".lower() # title and content
118
+ assert "python" in title_content
119
+
120
+ @pytest.mark.asyncio
121
+ async def test_async_transaction_simple_query_with_score(self, async_client_setup):
122
+ """Test async transaction simple_query with score"""
123
+ client, test_db = async_client_setup
124
+
125
+ async with client.transaction() as tx:
126
+ # Test search with score within transaction
127
+ result = await tx.query(
128
+ "async_tx_docs.title",
129
+ "async_tx_docs.content",
130
+ boolean_match("title", "content").encourage("async").label("score"),
131
+ ).execute()
132
+
133
+ assert result is not None
134
+ rows = result.fetchall()
135
+ assert len(rows) > 0
136
+
137
+ # Verify results have score column
138
+ assert "score" in result.columns
139
+ score_column_index = result.columns.index("score")
140
+
141
+ # Verify score values are numeric
142
+ for row in rows:
143
+ score = row[score_column_index]
144
+ assert isinstance(score, (int, float))
145
+ assert score >= 0
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_async_transaction_simple_query_boolean_mode(self, async_client_setup):
149
+ """Test async transaction simple_query with boolean mode"""
150
+ client, test_db = async_client_setup
151
+
152
+ async with client.transaction() as tx:
153
+ # Test boolean mode search within transaction
154
+ result = (
155
+ await tx.query("async_tx_docs.title", "async_tx_docs.content")
156
+ .filter(boolean_match("title", "content").must("async").must_not("basic"))
157
+ .execute()
158
+ )
159
+
160
+ assert result is not None
161
+ rows = result.fetchall()
162
+ assert len(rows) > 0
163
+
164
+ # Verify all results contain "async" but not "basic"
165
+ for row in rows:
166
+ title_content = f"{row[0]} {row[1]}".lower() # title and content
167
+ assert "async" in title_content
168
+ assert "basic" not in title_content
169
+
170
+ @pytest.mark.asyncio
171
+ async def test_async_transaction_simple_query_with_where(self, async_client_setup):
172
+ """Test async transaction simple_query with WHERE conditions"""
173
+ client, test_db = async_client_setup
174
+
175
+ async with client.transaction() as tx:
176
+ # Test search with WHERE conditions within transaction
177
+ result = (
178
+ await tx.query("async_tx_docs.title", "async_tx_docs.content")
179
+ .filter(boolean_match("title", "content").encourage("programming"), "async_tx_docs.category = 'Programming'")
180
+ .execute()
181
+ )
182
+
183
+ assert result is not None
184
+ rows = result.fetchall()
185
+ assert len(rows) > 0
186
+
187
+ # Verify all results contain "programming"
188
+ for row in rows:
189
+ title_content = f"{row[0]} {row[1]}".lower()
190
+ assert "programming" in title_content
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_async_transaction_simple_query_ordering_and_limit(self, async_client_setup):
194
+ """Test async transaction simple_query with ordering and limit"""
195
+ client, test_db = async_client_setup
196
+
197
+ async with client.transaction() as tx:
198
+ # Test search with ordering and limit within transaction
199
+ result = (
200
+ await tx.query("async_tx_docs.title", "async_tx_docs.content")
201
+ .filter(boolean_match("title", "content").encourage("async"))
202
+ .order_by("async_tx_docs.title DESC")
203
+ .limit(2)
204
+ .execute()
205
+ )
206
+
207
+ assert result is not None
208
+ rows = result.fetchall()
209
+ assert len(rows) <= 2
210
+
211
+ # Verify results are ordered by title (descending)
212
+ titles = [row[0] for row in rows] # title column
213
+ assert titles == sorted(titles, reverse=True)
214
+
215
+ @pytest.mark.asyncio
216
+ async def test_async_transaction_simple_query_explain(self, async_client_setup):
217
+ """Test async transaction simple_query explain functionality"""
218
+ client, test_db = async_client_setup
219
+
220
+ async with client.transaction() as tx:
221
+ # Test basic query functionality within transaction
222
+ result = (
223
+ await tx.query("async_tx_docs.title", "async_tx_docs.content")
224
+ .filter(boolean_match("title", "content").encourage("test"), "async_tx_docs.category = 'Testing'")
225
+ .execute()
226
+ )
227
+
228
+ assert result is not None
229
+ rows = result.fetchall()
230
+ # Should find testing-related content
231
+ assert len(rows) > 0, "Should find testing-related content"
232
+
233
+ # Verify all results contain "test" in title or content
234
+ for row in rows:
235
+ title_content = f"{row[0]} {row[1]}".lower()
236
+ assert "test" in title_content, f"Row should contain 'test': {row}"
237
+
238
+ @pytest.mark.asyncio
239
+ async def test_async_transaction_simple_query_multiple_operations(self, async_client_setup):
240
+ """Test multiple async transaction simple_query operations in one transaction"""
241
+ client, test_db = async_client_setup
242
+
243
+ async with client.transaction() as tx:
244
+ # Test multiple searches within the same transaction
245
+ results = []
246
+
247
+ # Search 1: Basic search
248
+ result1 = await tx.query(
249
+ "async_tx_docs.title", "async_tx_docs.content", boolean_match("title", "content").encourage("python")
250
+ ).execute()
251
+ results.append(result1)
252
+
253
+ # Search 2: Boolean mode
254
+ result2 = await tx.query(
255
+ "async_tx_docs.title", "async_tx_docs.content", boolean_match("title", "content").must("javascript")
256
+ ).execute()
257
+ results.append(result2)
258
+
259
+ # Search 3: With score
260
+ result3 = await tx.query(
261
+ "async_tx_docs.title",
262
+ "async_tx_docs.content",
263
+ boolean_match("title", "content").encourage("database").label("score"),
264
+ ).execute()
265
+ results.append(result3)
266
+
267
+ # Verify all searches returned results
268
+ for i, result in enumerate(results):
269
+ assert result is not None
270
+ rows = result.fetchall()
271
+ assert len(rows) > 0, f"Search {i+1} should return results"
272
+
273
+ # Verify content matches search terms
274
+ search_terms = ["python", "javascript", "database"]
275
+ term = search_terms[i]
276
+ for row in rows:
277
+ title_content = f"{row[0]} {row[1]}".lower()
278
+ assert term in title_content, f"Row should contain '{term}': {row}"
279
+
280
+ @pytest.mark.asyncio
281
+ async def test_async_transaction_simple_query_with_database_prefix(self, async_client_setup):
282
+ """Test async transaction simple_query with database prefix"""
283
+ client, test_db = async_client_setup
284
+
285
+ async with client.transaction() as tx:
286
+ # Test search with database prefix within transaction
287
+ result = await tx.query(
288
+ f"{test_db}.async_tx_docs.title",
289
+ f"{test_db}.async_tx_docs.content",
290
+ boolean_match("title", "content").encourage("web"),
291
+ ).execute()
292
+
293
+ assert result is not None
294
+ rows = result.fetchall()
295
+ assert len(rows) > 0
296
+
297
+ # Verify results contain "web"
298
+ for row in rows:
299
+ title_content = f"{row[0]} {row[1]}".lower() # title and content
300
+ assert "web" in title_content
@@ -0,0 +1,130 @@
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 basic connection functionality
17
+
18
+ These tests are inspired by example_01_basic_connection.py
19
+ """
20
+
21
+ import pytest
22
+ from matrixone import Client, AsyncClient
23
+ from matrixone.logger import create_default_logger
24
+
25
+
26
+ @pytest.mark.online
27
+ class TestBasicConnection:
28
+ """Test basic connection functionality"""
29
+
30
+ def test_basic_connection_and_query(self, test_client):
31
+ """Test basic connection and simple query"""
32
+ # Test basic query
33
+ result = test_client.execute("SELECT 1 as test_value, USER() as user_info")
34
+ assert result is not None
35
+ assert len(result.rows) > 0
36
+ assert result.rows[0][0] == 1 # test_value should be 1
37
+
38
+ def test_database_info_queries(self, test_client):
39
+ """Test database information queries"""
40
+ # Test SHOW DATABASES
41
+ result = test_client.execute("SHOW DATABASES")
42
+ assert result is not None
43
+ assert len(result.rows) > 0
44
+
45
+ # Test SHOW TABLES
46
+ result = test_client.execute("SHOW TABLES")
47
+ assert result is not None
48
+
49
+ def test_connection_with_logging(self, connection_params):
50
+ """Test connection with custom logging"""
51
+ host, port, user, password, database = connection_params
52
+
53
+ # Create logger
54
+ logger = create_default_logger()
55
+
56
+ # Create client with logging
57
+ client = Client()
58
+ client.connect(host=host, port=port, user=user, password=password, database=database)
59
+
60
+ try:
61
+ # Test query with logging
62
+ result = client.execute("SELECT 1 as test_value")
63
+ assert result is not None
64
+ assert len(result.rows) > 0
65
+ finally:
66
+ client.disconnect()
67
+
68
+ def test_connection_error_handling(self, connection_params):
69
+ """Test connection error handling"""
70
+ host, port, user, password, database = connection_params
71
+
72
+ # Test with invalid port
73
+ try:
74
+ client = Client()
75
+ client.connect(host=host, port=9999, user=user, password=password, database=database)
76
+ # With the new connection pool architecture, connection is tested on first query
77
+ client.execute("SELECT 1")
78
+ assert False, "Should have failed with invalid port"
79
+ except Exception as e:
80
+ assert "connection" in str(e).lower() or "refused" in str(e).lower()
81
+
82
+ def test_client_context_manager(self, connection_params):
83
+ """Test client context manager"""
84
+ host, port, user, password, database = connection_params
85
+
86
+ with Client() as client:
87
+ client.connect(host=host, port=port, user=user, password=password, database=database)
88
+ result = client.execute("SELECT 1 as test_value")
89
+ assert result is not None
90
+ assert len(result.rows) > 0
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_async_basic_connection(self, test_async_client):
94
+ """Test basic async connection and query"""
95
+ # Test basic query
96
+ result = await test_async_client.execute("SELECT 1 as test_value, USER() as user_info")
97
+ assert result is not None
98
+ assert len(result.rows) > 0
99
+ assert result.rows[0][0] == 1 # test_value should be 1
100
+
101
+ @pytest.mark.asyncio
102
+ async def test_async_connection_with_logging(self, connection_params):
103
+ """Test async connection with custom logging"""
104
+ host, port, user, password, database = connection_params
105
+
106
+ # Create logger
107
+ logger = create_default_logger()
108
+
109
+ # Create async client with logging
110
+ client = AsyncClient()
111
+ await client.connect(host=host, port=port, user=user, password=password, database=database)
112
+
113
+ try:
114
+ # Test query with logging
115
+ result = await client.execute("SELECT 1 as test_value")
116
+ assert result is not None
117
+ assert len(result.rows) > 0
118
+ finally:
119
+ await client.disconnect()
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_async_client_context_manager(self, connection_params):
123
+ """Test async client context manager"""
124
+ host, port, user, password, database = connection_params
125
+
126
+ async with AsyncClient() as client:
127
+ await client.connect(host=host, port=port, user=user, password=password, database=database)
128
+ result = await client.execute("SELECT 1 as test_value")
129
+ assert result is not None
130
+ assert len(result.rows) > 0
@@ -0,0 +1,238 @@
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 Client functionality - tests actual database operations
17
+ """
18
+
19
+ import unittest
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 matrixone.exceptions import ConnectionError, QueryError
29
+
30
+
31
+ class TestClientOnline(unittest.TestCase):
32
+ """Online tests for Client functionality"""
33
+
34
+ @classmethod
35
+ def setUpClass(cls):
36
+ """Set up test database connection"""
37
+ cls.client = Client(
38
+ host=os.getenv('MATRIXONE_HOST', '127.0.0.1'),
39
+ port=int(os.getenv('MATRIXONE_PORT', '6001')),
40
+ user=os.getenv('MATRIXONE_USER', 'root'),
41
+ password=os.getenv('MATRIXONE_PASSWORD', '111'),
42
+ database=os.getenv('MATRIXONE_DATABASE', 'test'),
43
+ )
44
+
45
+ # Create test database and table
46
+ cls.test_db = "test_client_db"
47
+ cls.test_table = "test_client_table"
48
+
49
+ try:
50
+ cls.client.execute(f"CREATE DATABASE IF NOT EXISTS {cls.test_db}")
51
+ cls.client.execute(f"USE {cls.test_db}")
52
+ cls.client.execute(
53
+ f"""
54
+ CREATE TABLE IF NOT EXISTS {cls.test_table} (
55
+ id INT PRIMARY KEY,
56
+ name VARCHAR(100),
57
+ value INT,
58
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
59
+ )
60
+ """
61
+ )
62
+
63
+ # Insert test data
64
+ cls.client.execute(f"INSERT INTO {cls.test_table} VALUES (1, 'test1', 100, NOW())")
65
+ cls.client.execute(f"INSERT INTO {cls.test_table} VALUES (2, 'test2', 200, NOW())")
66
+ cls.client.execute(f"INSERT INTO {cls.test_table} VALUES (3, 'test3', 300, NOW())")
67
+
68
+ except Exception as e:
69
+ print(f"Setup failed: {e}")
70
+ raise
71
+
72
+ @classmethod
73
+ def tearDownClass(cls):
74
+ """Clean up test database"""
75
+ try:
76
+ cls.client.execute(f"DROP DATABASE IF EXISTS {cls.test_db}")
77
+ except Exception as e:
78
+ print(f"Cleanup failed: {e}")
79
+
80
+ def test_basic_connection_and_query(self):
81
+ """Test basic connection and query functionality"""
82
+ # Test simple query
83
+ result = self.client.execute("SELECT 1 as test_value")
84
+ rows = result.fetchall()
85
+ self.assertEqual(len(rows), 1)
86
+ self.assertEqual(rows[0][0], 1)
87
+
88
+ # Test query with parameters
89
+ result = self.client.execute("SELECT ? as param_value", (42,))
90
+ rows = result.fetchall()
91
+ self.assertEqual(len(rows), 1)
92
+ self.assertEqual(rows[0][0], 42)
93
+
94
+ def test_table_operations(self):
95
+ """Test table operations"""
96
+ # Test SELECT
97
+ result = self.client.execute(f"SELECT * FROM {self.test_table} ORDER BY id")
98
+ rows = result.fetchall()
99
+ self.assertEqual(len(rows), 3)
100
+ self.assertEqual(rows[0][0], 1) # id
101
+ self.assertEqual(rows[0][1], 'test1') # name
102
+
103
+ # Test INSERT
104
+ self.client.execute(f"INSERT INTO {self.test_table} VALUES (4, 'test4', 400, NOW())")
105
+
106
+ # Verify INSERT
107
+ result = self.client.execute(f"SELECT COUNT(*) FROM {self.test_table}")
108
+ count = result.fetchone()[0]
109
+ self.assertEqual(count, 4)
110
+
111
+ # Test UPDATE
112
+ self.client.execute(f"UPDATE {self.test_table} SET value = 500 WHERE id = 4")
113
+
114
+ # Verify UPDATE
115
+ result = self.client.execute(f"SELECT value FROM {self.test_table} WHERE id = 4")
116
+ value = result.fetchone()[0]
117
+ self.assertEqual(value, 500)
118
+
119
+ # Test DELETE
120
+ self.client.execute(f"DELETE FROM {self.test_table} WHERE id = 4")
121
+
122
+ # Verify DELETE
123
+ result = self.client.execute(f"SELECT COUNT(*) FROM {self.test_table}")
124
+ count = result.fetchone()[0]
125
+ self.assertEqual(count, 3)
126
+
127
+ def test_transaction_operations(self):
128
+ """Test transaction operations"""
129
+ with self.client.transaction() as tx:
130
+ # Insert within transaction
131
+ tx.execute(f"INSERT INTO {self.test_table} VALUES (5, 'test5', 500, NOW())")
132
+
133
+ # Verify within transaction
134
+ result = tx.execute(f"SELECT COUNT(*) FROM {self.test_table}")
135
+ count = result.fetchone()[0]
136
+ self.assertEqual(count, 4)
137
+
138
+ # Verify transaction commit
139
+ result = self.client.execute(f"SELECT COUNT(*) FROM {self.test_table}")
140
+ count = result.fetchone()[0]
141
+ self.assertEqual(count, 4)
142
+
143
+ # Test transaction rollback
144
+ try:
145
+ with self.client.transaction() as tx:
146
+ tx.execute(f"INSERT INTO {self.test_table} VALUES (6, 'test6', 600, NOW())")
147
+ # Force rollback by raising exception
148
+ raise Exception("Test rollback")
149
+ except Exception:
150
+ pass
151
+
152
+ # Verify rollback
153
+ result = self.client.execute(f"SELECT COUNT(*) FROM {self.test_table}")
154
+ count = result.fetchone()[0]
155
+ self.assertEqual(count, 4) # Should still be 4, not 5
156
+
157
+ # Clean up
158
+ self.client.execute(f"DELETE FROM {self.test_table} WHERE id = 5")
159
+
160
+ def test_error_handling(self):
161
+ """Test error handling"""
162
+ # Test connection error handling
163
+ with self.assertRaises(QueryError):
164
+ self.client.execute("SELECT * FROM non_existent_table")
165
+
166
+ # Test invalid SQL
167
+ with self.assertRaises(QueryError):
168
+ self.client.execute("INVALID SQL STATEMENT")
169
+
170
+ def test_result_set_operations(self):
171
+ """Test result set operations"""
172
+ result = self.client.execute(f"SELECT * FROM {self.test_table} ORDER BY id")
173
+
174
+ # Test fetchone
175
+ first_row = result.fetchone()
176
+ self.assertIsNotNone(first_row)
177
+ self.assertEqual(first_row[0], 1)
178
+
179
+ # Test fetchmany
180
+ next_rows = result.fetchmany(2)
181
+ self.assertEqual(len(next_rows), 2)
182
+ self.assertEqual(next_rows[0][0], 2)
183
+ self.assertEqual(next_rows[1][0], 3)
184
+
185
+ # Test fetchall (should return remaining rows)
186
+ remaining_rows = result.fetchall()
187
+ self.assertEqual(len(remaining_rows), 0) # Should be empty
188
+
189
+ # Test column names
190
+ result = self.client.execute(f"SELECT id, name, value FROM {self.test_table} LIMIT 1")
191
+ columns = result.keys()
192
+ expected_columns = ['id', 'name', 'value']
193
+ self.assertEqual(list(columns), expected_columns)
194
+
195
+ def test_parameter_binding(self):
196
+ """Test parameter binding"""
197
+ # Test string parameters
198
+ result = self.client.execute(f"SELECT * FROM {self.test_table} WHERE name = ?", ('test1',))
199
+ rows = result.fetchall()
200
+ self.assertEqual(len(rows), 1)
201
+ self.assertEqual(rows[0][1], 'test1')
202
+
203
+ # Test numeric parameters
204
+ result = self.client.execute(f"SELECT * FROM {self.test_table} WHERE value = ?", (200,))
205
+ rows = result.fetchall()
206
+ self.assertEqual(len(rows), 1)
207
+ self.assertEqual(rows[0][2], 200)
208
+
209
+ # Test multiple parameters
210
+ result = self.client.execute(f"SELECT * FROM {self.test_table} WHERE name = ? AND value > ?", ('test2', 150))
211
+ rows = result.fetchall()
212
+ self.assertEqual(len(rows), 1)
213
+ self.assertEqual(rows[0][1], 'test2')
214
+ self.assertEqual(rows[0][2], 200)
215
+
216
+ def test_connection_pooling(self):
217
+ """Test connection pooling and reuse"""
218
+ # Execute multiple queries to test connection reuse
219
+ for i in range(5):
220
+ result = self.client.execute("SELECT 1 as test")
221
+ rows = result.fetchall()
222
+ self.assertEqual(len(rows), 1)
223
+ self.assertEqual(rows[0][0], 1)
224
+
225
+ # Test concurrent-like operations
226
+ results = []
227
+ for i in range(3):
228
+ result = self.client.execute(f"SELECT COUNT(*) FROM {self.test_table}")
229
+ count = result.fetchone()[0]
230
+ results.append(count)
231
+
232
+ # All should return the same count
233
+ for count in results:
234
+ self.assertEqual(count, 3)
235
+
236
+
237
+ if __name__ == '__main__':
238
+ unittest.main()