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,433 @@
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 query update functionality
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 matrixone.orm import declarative_base
29
+ from sqlalchemy import Column, Integer, String, DECIMAL, TIMESTAMP, func
30
+
31
+ Base = declarative_base()
32
+
33
+ try:
34
+ from .test_config import online_config
35
+ except ImportError:
36
+ # Fallback for when running as standalone script
37
+ import test_config
38
+
39
+ online_config = test_config.online_config
40
+
41
+
42
+ class User(Base):
43
+ """User model for testing update operations"""
44
+
45
+ __tablename__ = "test_users_update"
46
+
47
+ id = Column(Integer, primary_key=True)
48
+ name = Column(String(100))
49
+ email = Column(String(100))
50
+ age = Column(Integer)
51
+ salary = Column(DECIMAL(10, 2))
52
+ status = Column(String(50))
53
+ login_count = Column(Integer, default=0)
54
+ last_login = Column(TIMESTAMP)
55
+ created_at = Column(TIMESTAMP, server_default=func.current_timestamp())
56
+
57
+
58
+ class Product(Base):
59
+ """Product model for testing update operations"""
60
+
61
+ __tablename__ = "test_products_update"
62
+
63
+ id = Column(Integer, primary_key=True)
64
+ name = Column(String(200))
65
+ price = Column(DECIMAL(10, 2))
66
+ category = Column(String(50))
67
+ stock = Column(Integer)
68
+ active = Column(Integer, default=1) # Using Integer for boolean-like field
69
+
70
+
71
+ class TestQueryUpdateOnline:
72
+ """Online tests for query update functionality"""
73
+
74
+ @pytest.fixture(scope="class")
75
+ def test_client(self):
76
+ """Create and connect Client for testing"""
77
+ host, port, user, password, database = online_config.get_connection_params()
78
+ client = Client()
79
+ client.connect(host=host, port=port, user=user, password=password, database=database)
80
+ try:
81
+ yield client
82
+ finally:
83
+ try:
84
+ client.disconnect()
85
+ except Exception as e:
86
+ print(f"Warning: Failed to disconnect client: {e}")
87
+
88
+ @pytest.fixture(scope="class")
89
+ def test_database(self, test_client):
90
+ """Set up test database and tables"""
91
+ test_db = "test_query_update_db"
92
+
93
+ # Create test database
94
+ test_client.execute(f"CREATE DATABASE IF NOT EXISTS {test_db}")
95
+ test_client.execute(f"USE {test_db}")
96
+
97
+ # Create tables using ORM
98
+ test_client.create_all(Base)
99
+
100
+ try:
101
+ yield test_db
102
+ finally:
103
+ # Clean up
104
+ try:
105
+ test_client.execute(f"DROP DATABASE IF EXISTS {test_db}")
106
+ except Exception as e:
107
+ print(f"Warning: Failed to drop test database: {e}")
108
+
109
+ @pytest.fixture(autouse=True, scope="class")
110
+ def setup_test_data(self, test_client, test_database):
111
+ """Set up test data before each test"""
112
+ # Ensure we're using the test database
113
+ test_client.execute(f"USE {test_database}")
114
+
115
+ # Clear existing data (ignore errors if tables don't exist)
116
+ try:
117
+ test_client.query(User).delete()
118
+ except Exception:
119
+ pass
120
+ try:
121
+ test_client.query(Product).delete()
122
+ except Exception:
123
+ pass
124
+
125
+ # Insert test users
126
+ users_data = [
127
+ {
128
+ "id": 1,
129
+ "name": "Alice",
130
+ "email": "alice@example.com",
131
+ "age": 25,
132
+ "salary": 50000.00,
133
+ "status": "active",
134
+ "login_count": 5,
135
+ },
136
+ {
137
+ "id": 2,
138
+ "name": "Bob",
139
+ "email": "bob@example.com",
140
+ "age": 30,
141
+ "salary": 60000.00,
142
+ "status": "active",
143
+ "login_count": 10,
144
+ },
145
+ {
146
+ "id": 3,
147
+ "name": "Charlie",
148
+ "email": "charlie@example.com",
149
+ "age": 35,
150
+ "salary": 70000.00,
151
+ "status": "inactive",
152
+ "login_count": 2,
153
+ },
154
+ {
155
+ "id": 4,
156
+ "name": "David",
157
+ "email": "david@example.com",
158
+ "age": 28,
159
+ "salary": 55000.00,
160
+ "status": "active",
161
+ "login_count": 8,
162
+ },
163
+ ]
164
+ test_client.batch_insert("test_users_update", users_data)
165
+
166
+ # Insert test products
167
+ products_data = [
168
+ {"id": 1, "name": "Laptop", "price": 999.99, "category": "Electronics", "stock": 10, "active": 1},
169
+ {"id": 2, "name": "Phone", "price": 699.99, "category": "Electronics", "stock": 20, "active": 1},
170
+ {"id": 3, "name": "Book", "price": 29.99, "category": "Education", "stock": 50, "active": 0},
171
+ {"id": 4, "name": "Tablet", "price": 499.99, "category": "Electronics", "stock": 15, "active": 1},
172
+ ]
173
+ test_client.batch_insert("test_products_update", products_data)
174
+
175
+ def test_simple_update_single_record(self, test_client):
176
+ """Test updating a single record with simple values"""
177
+ # Update Alice's name and email
178
+ query = test_client.query(User)
179
+ result = query.update(name="Alice Updated", email="alice.updated@example.com").filter(User.id == 1).execute()
180
+
181
+ # Verify the update
182
+ updated_user = test_client.query(User).filter(User.id == 1).first()
183
+ assert updated_user.name == "Alice Updated"
184
+ assert updated_user.email == "alice.updated@example.com"
185
+ assert updated_user.age == 25 # Should remain unchanged
186
+
187
+ def test_update_multiple_records_with_condition(self, test_client):
188
+ """Test updating multiple records with a condition"""
189
+ # Update all active users' status to "premium"
190
+ query = test_client.query(User)
191
+ result = query.update(status="premium").filter(User.status == "active").execute()
192
+
193
+ # Verify the updates
194
+ premium_users = test_client.query(User).filter(User.status == "premium").all()
195
+ assert len(premium_users) == 3 # Alice, Bob, and David
196
+
197
+ # Check that inactive user (Charlie) was not updated
198
+ charlie = test_client.query(User).filter(User.id == 3).first()
199
+ assert charlie.status == "inactive"
200
+
201
+ def test_update_with_sqlalchemy_expressions(self, test_client):
202
+ """Test updating with SQLAlchemy expressions"""
203
+ # Update login count using SQLAlchemy expression
204
+ query = test_client.query(User)
205
+ result = query.update(login_count=User.login_count + 1).filter(User.id == 1).execute()
206
+
207
+ # Verify the update
208
+ updated_user = test_client.query(User).filter(User.id == 1).first()
209
+ assert updated_user.login_count == 6 # Was 5, now 6
210
+
211
+ def test_update_with_complex_expressions(self, test_client):
212
+ """Test updating with complex SQLAlchemy expressions"""
213
+ # Update salary with a percentage increase
214
+ query = test_client.query(User)
215
+ result = query.update(salary=User.salary * 1.1).filter(User.id == 2).execute()
216
+
217
+ # Verify the update
218
+ updated_user = test_client.query(User).filter(User.id == 2).first()
219
+ assert updated_user.salary == 66000.00 # 60000 * 1.1
220
+
221
+ def test_update_with_multiple_conditions(self, test_client, test_database):
222
+ """Test updating with multiple filter conditions"""
223
+ # Ensure we're using the test database
224
+ test_client.execute(f"USE {test_database}")
225
+
226
+ # Create test records specifically for this test
227
+ test_client.execute(
228
+ """
229
+ INSERT INTO test_users_update (id, name, email, age, salary, status, login_count)
230
+ VALUES (200, 'Test User 1', 'test1@example.com', 25, 50000, 'active', 10),
231
+ (201, 'Test User 2', 'test2@example.com', 30, 60000, 'active', 5),
232
+ (202, 'Test User 3', 'test3@example.com', 35, 70000, 'active', 8)
233
+ """
234
+ )
235
+
236
+ # Update users who are active and have login_count > 5
237
+ query = test_client.query(User)
238
+ result = query.update(status="vip").filter(User.status == "active").filter(User.login_count > 5).execute()
239
+
240
+ # Verify the updates
241
+ vip_users = test_client.query(User).filter(User.status == "vip").all()
242
+ assert len(vip_users) == 2 # User 1 (login_count=10) and User 3 (login_count=8)
243
+
244
+ # Check that User 2 (login_count=5) was not updated
245
+ user2 = test_client.query(User).filter(User.id == 201).first()
246
+ assert user2.status == "active"
247
+
248
+ # Clean up
249
+ test_client.query(User).filter(User.id.in_([200, 201, 202])).delete()
250
+
251
+ def test_update_with_none_values(self, test_client):
252
+ """Test updating with None values"""
253
+ # Update Charlie's last_login to None
254
+ query = test_client.query(User)
255
+ result = query.update(last_login=None).filter(User.id == 3).execute()
256
+
257
+ # Verify the update
258
+ updated_user = test_client.query(User).filter(User.id == 3).first()
259
+ assert updated_user.last_login is None
260
+
261
+ def test_update_with_numeric_values(self, test_client):
262
+ """Test updating with numeric values"""
263
+ # Update product price
264
+ query = test_client.query(Product)
265
+ result = query.update(price=1099.99).filter(Product.id == 1).execute()
266
+
267
+ # Verify the update
268
+ updated_product = test_client.query(Product).filter(Product.id == 1).first()
269
+ assert float(updated_product.price) == 1099.99
270
+
271
+ def test_update_with_boolean_like_values(self, test_client):
272
+ """Test updating with boolean-like values"""
273
+ # Update product active status
274
+ query = test_client.query(Product)
275
+ result = query.update(active=0).filter(Product.id == 2).execute()
276
+
277
+ # Verify the update
278
+ updated_product = test_client.query(Product).filter(Product.id == 2).first()
279
+ assert updated_product.active == 0
280
+
281
+ def test_update_with_string_conditions(self, test_client):
282
+ """Test updating with string-based filter conditions"""
283
+ # Update products in Electronics category
284
+ query = test_client.query(Product)
285
+ result = query.update(stock=Product.stock + 5).filter("category = ?", "Electronics").execute()
286
+
287
+ # Verify the updates
288
+ electronics_products = test_client.query(Product).filter("category = ?", "Electronics").all()
289
+ for product in electronics_products:
290
+ if product.id == 1: # Laptop
291
+ assert product.stock == 15 # Was 10, now 15
292
+ elif product.id == 2: # Phone
293
+ assert product.stock == 25 # Was 20, now 25
294
+ elif product.id == 4: # Tablet
295
+ assert product.stock == 20 # Was 15, now 20
296
+
297
+ def test_update_with_mixed_conditions(self, test_client):
298
+ """Test updating with mixed SQLAlchemy and string conditions"""
299
+ # Update users with mixed conditions
300
+ query = test_client.query(User)
301
+ result = query.update(age=User.age + 1).filter(User.status == "active").filter("age < ?", 30).execute()
302
+
303
+ # Verify the updates
304
+ young_active_users = test_client.query(User).filter(User.status == "active").filter("age < ?", 30).all()
305
+ for user in young_active_users:
306
+ if user.id == 1: # Alice, was 25, now 26
307
+ assert user.age == 26
308
+ elif user.id == 4: # David, was 28, now 29
309
+ assert user.age == 29
310
+
311
+ def test_update_no_matching_records(self, test_client):
312
+ """Test updating when no records match the condition"""
313
+ # Try to update a non-existent user
314
+ query = test_client.query(User)
315
+ result = query.update(name="Non-existent").filter(User.id == 999).execute()
316
+
317
+ # Verify no records were updated
318
+ non_existent = test_client.query(User).filter(User.id == 999).first()
319
+ assert non_existent is None
320
+
321
+ def test_update_with_transaction(self, test_client, test_database):
322
+ """Test updating within a transaction"""
323
+ # Ensure we're using the test database
324
+ test_client.execute(f"USE {test_database}")
325
+
326
+ # Create a test record specifically for this test
327
+ test_client.execute(
328
+ """
329
+ INSERT INTO test_users_update (id, name, email, age, salary, status, login_count)
330
+ VALUES (100, 'Transaction Test User', 'transaction@test.com', 25, 50000, 'active', 0)
331
+ """
332
+ )
333
+
334
+ with test_client.transaction() as tx:
335
+ # Ensure we're using the test database in transaction context
336
+ test_client.execute(f"USE {test_database}")
337
+ # Update the test record in a transaction
338
+ query = test_client.query(User)
339
+ query.update(status="transaction_committed").filter(User.id == 100).execute()
340
+
341
+ # Verify the update was committed
342
+ updated_user = test_client.query(User).filter(User.id == 100).first()
343
+ assert updated_user is not None
344
+ assert updated_user.status == "transaction_committed"
345
+
346
+ # Clean up
347
+ test_client.query(User).filter(User.id == 100).delete()
348
+
349
+ def test_update_with_rollback(self, test_client, test_database):
350
+ """Test updating with transaction rollback"""
351
+ # Ensure we're using the test database
352
+ test_client.execute(f"USE {test_database}")
353
+
354
+ # Create a test record specifically for this test
355
+ test_client.execute(
356
+ """
357
+ INSERT INTO test_users_update (id, name, email, age, salary, status, login_count)
358
+ VALUES (101, 'Rollback Test User', 'rollback@test.com', 30, 60000, 'original_status', 0)
359
+ """
360
+ )
361
+
362
+ # Verify initial state
363
+ user = test_client.query(User).filter(User.id == 101).first()
364
+ assert user is not None
365
+ assert user.status == "original_status"
366
+
367
+ # Test transaction rollback
368
+ try:
369
+ with test_client.transaction() as tx:
370
+ # Ensure we're using the test database in transaction context
371
+ tx.execute(f"USE {test_database}")
372
+ # Update the test record using transaction connection
373
+ tx.execute("UPDATE test_users_update SET status = 'rollback_test' WHERE id = 101")
374
+
375
+ # Verify the update happened within the transaction
376
+ result = tx.execute("SELECT status FROM test_users_update WHERE id = 101")
377
+ user_in_tx = result.fetchone()
378
+ assert user_in_tx[0] == "rollback_test"
379
+
380
+ # Force rollback by raising an exception
381
+ raise Exception("Force rollback")
382
+ except Exception:
383
+ pass
384
+
385
+ # Verify the update was rolled back - status should be back to "original_status"
386
+ user = test_client.query(User).filter(User.id == 101).first()
387
+ assert user is not None
388
+ assert user.status == "original_status"
389
+
390
+ # Clean up
391
+ test_client.query(User).filter(User.id == 101).delete()
392
+
393
+ def test_update_with_explain(self, test_client):
394
+ """Test explaining an update query"""
395
+ query = test_client.query(User)
396
+ explain_result = query.update(name="Explain Test").filter(User.id == 1).explain()
397
+
398
+ # Verify explain returns a result
399
+ assert explain_result is not None
400
+ assert len(explain_result.rows) > 0
401
+
402
+ def test_update_with_to_sql(self, test_client):
403
+ """Test generating SQL for an update query"""
404
+ query = test_client.query(User)
405
+ sql = query.update(name="SQL Test", email="sql@example.com").filter(User.id == 1).to_sql()
406
+
407
+ # Verify SQL structure
408
+ assert "UPDATE test_users_update SET" in sql
409
+ assert "name = 'SQL Test'" in sql
410
+ assert "email = 'sql@example.com'" in sql
411
+ assert "WHERE id = 1" in sql
412
+
413
+ def test_update_with_explain_analyze(self, test_client):
414
+ """Test explaining and analyzing an update query"""
415
+ query = test_client.query(User)
416
+ explain_result = query.update(name="Explain Analyze Test").filter(User.id == 1).explain_analyze()
417
+
418
+ # Verify explain analyze returns a result
419
+ assert explain_result is not None
420
+ assert len(explain_result.rows) > 0
421
+
422
+ def test_update_with_verbose_explain(self, test_client):
423
+ """Test verbose explain for an update query"""
424
+ query = test_client.query(User)
425
+ explain_result = query.update(name="Verbose Explain Test").filter(User.id == 1).explain(verbose=True)
426
+
427
+ # Verify verbose explain returns a result
428
+ assert explain_result is not None
429
+ assert len(explain_result.rows) > 0
430
+
431
+
432
+ if __name__ == "__main__":
433
+ pytest.main([__file__])