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
matrixone/account.py ADDED
@@ -0,0 +1,723 @@
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
+ MatrixOne Account Management - Corrected implementation based on actual MatrixOne behavior
17
+
18
+ This module provides proper account, user, and role management for MatrixOne database.
19
+ Based on MatrixOne v25.2.2.2 documentation and actual testing.
20
+ """
21
+
22
+ import re
23
+ from dataclasses import dataclass
24
+ from datetime import datetime
25
+ from typing import TYPE_CHECKING, List, Optional
26
+
27
+ from matrixone.exceptions import AccountError
28
+
29
+ if TYPE_CHECKING:
30
+ from matrixone.client import Client, TransactionWrapper
31
+
32
+
33
+ @dataclass
34
+ class Account:
35
+ """MatrixOne Account information"""
36
+
37
+ name: str
38
+ admin_name: str
39
+ created_time: Optional[datetime] = None
40
+ status: Optional[str] = None
41
+ comment: Optional[str] = None
42
+ suspended_time: Optional[datetime] = None
43
+ suspended_reason: Optional[str] = None
44
+
45
+ def __str__(self) -> str:
46
+ return f"Account(name='{self.name}', admin='{self.admin_name}', status='{self.status}')"
47
+
48
+ def __repr__(self) -> str:
49
+ return self.__str__()
50
+
51
+
52
+ @dataclass
53
+ class User:
54
+ """MatrixOne User information"""
55
+
56
+ name: str
57
+ host: str
58
+ account: str
59
+ created_time: Optional[datetime] = None
60
+ status: Optional[str] = None
61
+ comment: Optional[str] = None
62
+ locked_time: Optional[datetime] = None
63
+ locked_reason: Optional[str] = None
64
+
65
+ def __str__(self) -> str:
66
+ return f"User(name='{self.name}', host='{self.host}', account='{self.account}', status='{self.status}')"
67
+
68
+ def __repr__(self) -> str:
69
+ return self.__str__()
70
+
71
+
72
+ @dataclass
73
+ class Role:
74
+ """MatrixOne Role information"""
75
+
76
+ name: str
77
+ id: int
78
+ created_time: Optional[datetime] = None
79
+ comment: Optional[str] = None
80
+
81
+ def __str__(self) -> str:
82
+ return f"Role(name='{self.name}', id={self.id})"
83
+
84
+ def __repr__(self) -> str:
85
+ return self.__str__()
86
+
87
+
88
+ @dataclass
89
+ class Grant:
90
+ """MatrixOne Grant (permission) information"""
91
+
92
+ grant_statement: str
93
+ privilege: Optional[str] = None
94
+ object_type: Optional[str] = None
95
+ object_name: Optional[str] = None
96
+ user: Optional[str] = None
97
+
98
+ def __str__(self) -> str:
99
+ return f"Grant(privilege='{self.privilege}', object='{self.object_name}', user='{self.user}')"
100
+
101
+ def __repr__(self) -> str:
102
+ return self.__str__()
103
+
104
+
105
+ class AccountManager:
106
+ """
107
+ MatrixOne Account Manager for user and account management operations.
108
+
109
+ This class provides comprehensive account and user management functionality
110
+ for MatrixOne databases, including account creation, user management, role
111
+ assignments, and permission grants.
112
+
113
+ Key Features:
114
+
115
+ - Account creation and management
116
+ - User creation and authentication
117
+ - Role-based access control (RBAC)
118
+ - Permission grants and revocations
119
+ - Account and user listing and querying
120
+ - Integration with MatrixOne's security model
121
+
122
+ Supported Operations:
123
+
124
+ - Create and manage accounts with administrators
125
+ - Create users within accounts
126
+ - Assign roles to users
127
+ - Grant and revoke permissions
128
+ - List accounts, users, and roles
129
+ - Query account and user information
130
+
131
+ Usage Examples::
132
+
133
+ # Create a new account
134
+ account = client.account.create_account(
135
+ account_name='company_account',
136
+ admin_name='admin_user',
137
+ password='secure_password',
138
+ comment='Company main account'
139
+ )
140
+
141
+ # Create a user within an account
142
+ user = client.account.create_user(
143
+ username='john_doe',
144
+ password='user_password',
145
+ account='company_account',
146
+ comment='Employee user'
147
+ )
148
+
149
+ # Grant permissions to a user
150
+ client.account.grant_privilege(
151
+ username='john_doe',
152
+ account='company_account',
153
+ privilege='SELECT',
154
+ object_type='TABLE',
155
+ object_name='employees'
156
+ )
157
+
158
+ # List all accounts
159
+ accounts = client.account.list_accounts()
160
+
161
+ Note: Account management operations require appropriate administrative
162
+ privileges in MatrixOne.
163
+ """
164
+
165
+ def __init__(self, client: "Client"):
166
+ self._client = client
167
+
168
+ # Account Management
169
+ def create_account(self, account_name: str, admin_name: str, password: str, comment: Optional[str] = None) -> Account:
170
+ """
171
+ Create a new account in MatrixOne
172
+
173
+ Args::
174
+
175
+ account_name: Name of the account to create
176
+ admin_name: Name of the admin user for the account
177
+ password: Password for the admin user
178
+ comment: Comment for the account
179
+
180
+ Returns::
181
+
182
+ Account: Created account object
183
+
184
+ Raises::
185
+
186
+ AccountError: If account creation fails
187
+ """
188
+ try:
189
+ # Build CREATE ACCOUNT statement according to MatrixOne syntax
190
+ sql_parts = [f"CREATE ACCOUNT {self._client._escape_identifier(account_name)}"]
191
+ sql_parts.append(f"ADMIN_NAME {self._client._escape_string(admin_name)}")
192
+ sql_parts.append(f"IDENTIFIED BY {self._client._escape_string(password)}")
193
+
194
+ if comment:
195
+ sql_parts.append(f"COMMENT {self._client._escape_string(comment)}")
196
+
197
+ sql = " ".join(sql_parts)
198
+ self._client.execute(sql)
199
+
200
+ # Return the created account
201
+ return self.get_account(account_name)
202
+
203
+ except Exception as e:
204
+ raise AccountError(f"Failed to create account '{account_name}': {e}") from None
205
+
206
+ def drop_account(self, account_name: str, if_exists: bool = False) -> None:
207
+ """
208
+ Drop an account
209
+
210
+ Args::
211
+
212
+ account_name: Name of the account to drop
213
+ if_exists: If True, add IF EXISTS clause to avoid errors when account doesn't exist
214
+ """
215
+ try:
216
+ sql_parts = ["DROP ACCOUNT"]
217
+ if if_exists:
218
+ sql_parts.append("IF EXISTS")
219
+ sql_parts.append(self._client._escape_identifier(account_name))
220
+
221
+ sql = " ".join(sql_parts)
222
+ self._client.execute(sql)
223
+ except Exception as e:
224
+ raise AccountError(f"Failed to drop account '{account_name}': {e}") from None
225
+
226
+ def alter_account(
227
+ self,
228
+ account_name: str,
229
+ comment: Optional[str] = None,
230
+ suspend: Optional[bool] = None,
231
+ suspend_reason: Optional[str] = None,
232
+ ) -> Account:
233
+ """Alter an account"""
234
+ try:
235
+ sql_parts = [f"ALTER ACCOUNT {self._client._escape_identifier(account_name)}"]
236
+
237
+ if comment is not None:
238
+ sql_parts.append(f"COMMENT {self._client._escape_string(comment)}")
239
+
240
+ if suspend is not None:
241
+ if suspend:
242
+ if suspend_reason:
243
+ sql_parts.append(f"SUSPEND COMMENT {self._client._escape_string(suspend_reason)}")
244
+ else:
245
+ sql_parts.append("SUSPEND")
246
+ else:
247
+ sql_parts.append("OPEN")
248
+
249
+ sql = " ".join(sql_parts)
250
+ self._client.execute(sql)
251
+
252
+ return self.get_account(account_name)
253
+
254
+ except Exception as e:
255
+ raise AccountError(f"Failed to alter account '{account_name}': {e}") from None
256
+
257
+ def get_account(self, account_name: str) -> Account:
258
+ """Get account by name"""
259
+ try:
260
+ sql = "SHOW ACCOUNTS"
261
+ result = self._client.execute(sql)
262
+
263
+ if not result or not result.rows:
264
+ raise AccountError(f"Account '{account_name}' not found") from None
265
+
266
+ for row in result.rows:
267
+ if row[0] == account_name:
268
+ return self._row_to_account(row)
269
+
270
+ raise AccountError(f"Account '{account_name}' not found") from None
271
+
272
+ except Exception as e:
273
+ raise AccountError(f"Failed to get account '{account_name}': {e}") from None
274
+
275
+ def list_accounts(self) -> List[Account]:
276
+ """List all accounts"""
277
+ try:
278
+ sql = "SHOW ACCOUNTS"
279
+ result = self._client.execute(sql)
280
+
281
+ if not result or not result.rows:
282
+ return []
283
+
284
+ return [self._row_to_account(row) for row in result.rows]
285
+
286
+ except Exception as e:
287
+ raise AccountError(f"Failed to list accounts: {e}") from None
288
+
289
+ # User Management
290
+ def create_user(self, user_name: str, password: str, comment: Optional[str] = None) -> User:
291
+ """
292
+ Create a new user in MatrixOne
293
+
294
+ Note: MatrixOne CREATE USER syntax is: CREATE USER user_name IDENTIFIED BY 'password'
295
+ The user is created in the current account context.
296
+
297
+ Args::
298
+
299
+ user_name: Name of the user to create
300
+ password: Password for the user
301
+ comment: Comment for the user (not supported in MatrixOne)
302
+
303
+ Returns::
304
+
305
+ User: Created user object
306
+
307
+ Raises::
308
+
309
+ AccountError: If user creation fails
310
+ """
311
+ try:
312
+ # MatrixOne CREATE USER syntax
313
+ sql_parts = [f"CREATE USER {self._client._escape_identifier(user_name)}"]
314
+ sql_parts.append(f"IDENTIFIED BY {self._client._escape_string(password)}")
315
+
316
+ # Note: MatrixOne doesn't support COMMENT in CREATE USER
317
+ # if comment:
318
+ # sql_parts.append(f"COMMENT {self._client._escape_string(comment)}")
319
+
320
+ sql = " ".join(sql_parts)
321
+ self._client.execute(sql)
322
+
323
+ # Return a User object with current account context
324
+ current_account = self._get_current_account()
325
+ return User(
326
+ name=user_name,
327
+ host="%", # Default host
328
+ account=current_account,
329
+ created_time=datetime.now(),
330
+ status="ACTIVE",
331
+ comment=comment,
332
+ )
333
+
334
+ except Exception as e:
335
+ raise AccountError(f"Failed to create user '{user_name}': {e}") from None
336
+
337
+ def drop_user(self, user_name: str, if_exists: bool = False) -> None:
338
+ """
339
+ Drop a user according to MatrixOne DROP USER syntax:
340
+ DROP USER [IF EXISTS] user [, user] ...
341
+
342
+ Args::
343
+
344
+ user_name: Name of the user to drop
345
+ if_exists: If True, add IF EXISTS clause to avoid errors when user doesn't exist
346
+ """
347
+ try:
348
+ sql_parts = ["DROP USER"]
349
+ if if_exists:
350
+ sql_parts.append("IF EXISTS")
351
+
352
+ sql_parts.append(self._client._escape_identifier(user_name))
353
+ sql = " ".join(sql_parts)
354
+ self._client.execute(sql)
355
+
356
+ except Exception as e:
357
+ raise AccountError(f"Failed to drop user '{user_name}': {e}") from None
358
+
359
+ def alter_user(
360
+ self,
361
+ user_name: str,
362
+ password: Optional[str] = None,
363
+ comment: Optional[str] = None,
364
+ lock: Optional[bool] = None,
365
+ lock_reason: Optional[str] = None,
366
+ ) -> User:
367
+ """
368
+ Alter a user
369
+
370
+ Note: MatrixOne ALTER USER supports:
371
+ - ✅ ALTER USER user IDENTIFIED BY 'password' - Password modification
372
+ - ✅ ALTER USER user LOCK - Lock user
373
+ - ✅ ALTER USER user UNLOCK - Unlock user
374
+ - ❌ ALTER USER user COMMENT 'comment' - Not supported
375
+ """
376
+ try:
377
+ # Check if there are any operations to perform
378
+ has_operations = False
379
+ sql_parts = [f"ALTER USER {self._client._escape_identifier(user_name)}"]
380
+
381
+ if password is not None:
382
+ sql_parts.append(f"IDENTIFIED BY {self._client._escape_string(password)}")
383
+ has_operations = True
384
+
385
+ # MatrixOne doesn't support COMMENT in ALTER USER
386
+ if comment is not None:
387
+ raise AccountError(f"MatrixOne doesn't support COMMENT in ALTER USER. Comment: '{comment}'") from None
388
+
389
+ # MatrixOne supports LOCK/UNLOCK in ALTER USER
390
+ if lock is not None:
391
+ if lock:
392
+ sql_parts.append("LOCK")
393
+ else:
394
+ sql_parts.append("UNLOCK")
395
+ has_operations = True
396
+
397
+ # Only execute if there are operations to perform
398
+ if has_operations:
399
+ sql = " ".join(sql_parts)
400
+ self._client.execute(sql)
401
+ else:
402
+ # If no operations, just return current user info
403
+ pass
404
+
405
+ # Return updated user info
406
+ current_account = self._get_current_account()
407
+ return User(
408
+ name=user_name,
409
+ host="%", # Default host
410
+ account=current_account,
411
+ created_time=datetime.now(),
412
+ status="LOCKED" if lock else "ACTIVE",
413
+ comment=comment,
414
+ locked_time=datetime.now() if lock else None,
415
+ locked_reason=lock_reason,
416
+ )
417
+
418
+ except Exception as e:
419
+ raise AccountError(f"Failed to alter user '{user_name}': {e}") from None
420
+
421
+ def get_current_user(self) -> User:
422
+ """Get current user information"""
423
+ try:
424
+ sql = "SELECT USER()"
425
+ result = self._client.execute(sql)
426
+
427
+ if not result or not result.rows:
428
+ raise AccountError("Failed to get current user") from None
429
+
430
+ # Parse current user from USER() function result
431
+ current_user_str = result.rows[0][0] # e.g., 'root@localhost'
432
+ if "@" in current_user_str:
433
+ username, host = current_user_str.split("@", 1)
434
+ else:
435
+ username = current_user_str
436
+ host = "%"
437
+
438
+ current_account = self._get_current_account()
439
+
440
+ return User(
441
+ name=username,
442
+ host=host,
443
+ account=current_account,
444
+ created_time=None,
445
+ status="ACTIVE",
446
+ comment=None,
447
+ locked_time=None,
448
+ locked_reason=None,
449
+ )
450
+
451
+ except Exception as e:
452
+ raise AccountError(f"Failed to get current user: {e}") from None
453
+
454
+ def list_users(self) -> List[User]:
455
+ """
456
+ List users in current account
457
+
458
+ Note: MatrixOne doesn't provide a direct way to list all users.
459
+ This method returns the current user's information.
460
+ """
461
+ try:
462
+ current_user = self.get_current_user()
463
+ return [current_user]
464
+
465
+ except Exception as e:
466
+ raise AccountError(f"Failed to list users: {e}") from None
467
+
468
+ # Role Management
469
+ def create_role(self, role_name: str, comment: Optional[str] = None) -> Role:
470
+ """Create a new role"""
471
+ try:
472
+ # MatrixOne CREATE ROLE syntax doesn't support COMMENT
473
+ sql = f"CREATE ROLE {self._client._escape_identifier(role_name)}"
474
+ self._client.execute(sql)
475
+
476
+ return self.get_role(role_name)
477
+
478
+ except Exception as e:
479
+ raise AccountError(f"Failed to create role '{role_name}': {e}") from None
480
+
481
+ def drop_role(self, role_name: str, if_exists: bool = False) -> None:
482
+ """
483
+ Drop a role
484
+
485
+ Args::
486
+
487
+ role_name: Name of the role to drop
488
+ if_exists: If True, add IF EXISTS clause to avoid errors when role doesn't exist
489
+ """
490
+ try:
491
+ sql_parts = ["DROP ROLE"]
492
+ if if_exists:
493
+ sql_parts.append("IF EXISTS")
494
+ sql_parts.append(self._client._escape_identifier(role_name))
495
+
496
+ sql = " ".join(sql_parts)
497
+ self._client.execute(sql)
498
+ except Exception as e:
499
+ raise AccountError(f"Failed to drop role '{role_name}': {e}") from None
500
+
501
+ def get_role(self, role_name: str) -> Role:
502
+ """Get role by name"""
503
+ try:
504
+ sql = "SHOW ROLES"
505
+ result = self._client.execute(sql)
506
+
507
+ if not result or not result.rows:
508
+ raise AccountError(f"Role '{role_name}' not found") from None
509
+
510
+ for row in result.rows:
511
+ if row[0] == role_name:
512
+ return self._row_to_role(row)
513
+
514
+ raise AccountError(f"Role '{role_name}' not found") from None
515
+
516
+ except Exception as e:
517
+ raise AccountError(f"Failed to get role '{role_name}': {e}") from None
518
+
519
+ def list_roles(self) -> List[Role]:
520
+ """List all roles"""
521
+ try:
522
+ sql = "SHOW ROLES"
523
+ result = self._client.execute(sql)
524
+
525
+ if not result or not result.rows:
526
+ return []
527
+
528
+ return [self._row_to_role(row) for row in result.rows]
529
+
530
+ except Exception as e:
531
+ raise AccountError(f"Failed to list roles: {e}") from None
532
+
533
+ # Permission Management
534
+ def grant_privilege(
535
+ self,
536
+ privilege: str,
537
+ object_type: str,
538
+ object_name: str,
539
+ to_user: Optional[str] = None,
540
+ to_role: Optional[str] = None,
541
+ ) -> None:
542
+ """
543
+ Grant privilege to user or role
544
+
545
+ Note: In MatrixOne, users are treated as roles for permission purposes.
546
+
547
+ Args::
548
+
549
+ privilege: Privilege to grant (e.g., 'CREATE DATABASE', 'SELECT')
550
+ object_type: Type of object (e.g., 'ACCOUNT', 'DATABASE', 'TABLE')
551
+ object_name: Name of the object (e.g., 'test_db', '*')
552
+ to_user: User to grant to (treated as role in MatrixOne)
553
+ to_role: Role to grant to
554
+ """
555
+ try:
556
+ if not to_user and not to_role:
557
+ raise AccountError("Must specify either to_user or to_role") from None
558
+
559
+ # In MatrixOne, users are treated as roles
560
+ target = to_user if to_user else to_role
561
+
562
+ sql_parts = [f"GRANT {privilege} ON {object_type} {self._client._escape_identifier(object_name)}"]
563
+ sql_parts.append(f"TO {self._client._escape_identifier(target)}")
564
+
565
+ sql = " ".join(sql_parts)
566
+ self._client.execute(sql)
567
+
568
+ except Exception as e:
569
+ raise AccountError(f"Failed to grant privilege: {e}") from None
570
+
571
+ def revoke_privilege(
572
+ self,
573
+ privilege: str,
574
+ object_type: str,
575
+ object_name: str,
576
+ from_user: Optional[str] = None,
577
+ from_role: Optional[str] = None,
578
+ ) -> None:
579
+ """Revoke privilege from user or role"""
580
+ try:
581
+ if not from_user and not from_role:
582
+ raise AccountError("Must specify either from_user or from_role") from None
583
+
584
+ # In MatrixOne, users are treated as roles
585
+ target = from_user if from_user else from_role
586
+
587
+ sql_parts = [f"REVOKE {privilege} ON {object_type} {self._client._escape_identifier(object_name)}"]
588
+ sql_parts.append(f"FROM {self._client._escape_identifier(target)}")
589
+
590
+ sql = " ".join(sql_parts)
591
+ self._client.execute(sql)
592
+
593
+ except Exception as e:
594
+ raise AccountError(f"Failed to revoke privilege: {e}") from None
595
+
596
+ def grant_role(self, role_name: str, to_user: str) -> None:
597
+ """Grant role to user"""
598
+ try:
599
+ # MatrixOne syntax: GRANT role_name TO user_name
600
+ sql = f"GRANT {self._client._escape_identifier(role_name)} TO {self._client._escape_identifier(to_user)}"
601
+ self._client.execute(sql)
602
+ except Exception as e:
603
+ raise AccountError(f"Failed to grant role '{role_name}' to user '{to_user}': {e}") from None
604
+
605
+ def revoke_role(self, role_name: str, from_user: str) -> None:
606
+ """Revoke role from user"""
607
+ try:
608
+ # MatrixOne syntax: REVOKE role_name FROM user_name
609
+ sql = f"REVOKE {self._client._escape_identifier(role_name)} FROM {self._client._escape_identifier(from_user)}"
610
+ self._client.execute(sql)
611
+ except Exception as e:
612
+ raise AccountError(f"Failed to revoke role '{role_name}' from user '{from_user}': {e}") from None
613
+
614
+ def list_grants(self, user: Optional[str] = None) -> List[Grant]:
615
+ """List grants for current user or specified user"""
616
+ try:
617
+ if user:
618
+ sql = f"SHOW GRANTS FOR {self._client._escape_identifier(user)}"
619
+ else:
620
+ sql = "SHOW GRANTS"
621
+
622
+ result = self._client.execute(sql)
623
+
624
+ if not result or not result.rows:
625
+ return []
626
+
627
+ grants = []
628
+ for row in result.rows:
629
+ grant = self._parse_grant_statement(row[0])
630
+ grants.append(grant)
631
+
632
+ return grants
633
+
634
+ except Exception as e:
635
+ raise AccountError(f"Failed to list grants: {e}") from None
636
+
637
+ # Helper methods
638
+ def _get_current_account(self) -> str:
639
+ """Get current account name"""
640
+ try:
641
+ # Try to get account from connection context
642
+ # This is a simplified approach - in practice, you might need to
643
+ # parse the connection string or use other methods
644
+ return "sys" # Default account
645
+ except Exception:
646
+ return "sys"
647
+
648
+ def _row_to_account(self, row: tuple) -> Account:
649
+ """Convert database row to Account object"""
650
+ return Account(
651
+ name=row[0],
652
+ admin_name=row[1],
653
+ created_time=row[2] if len(row) > 2 else None,
654
+ status=row[3] if len(row) > 3 else None,
655
+ comment=row[4] if len(row) > 4 else None,
656
+ suspended_time=row[5] if len(row) > 5 else None,
657
+ suspended_reason=row[6] if len(row) > 6 else None,
658
+ )
659
+
660
+ def _row_to_role(self, row: tuple) -> Role:
661
+ """Convert database row to Role object"""
662
+ return Role(
663
+ name=row[0],
664
+ id=row[1] if len(row) > 1 else 0,
665
+ created_time=row[2] if len(row) > 2 else None,
666
+ comment=row[3] if len(row) > 3 else None,
667
+ )
668
+
669
+ def _parse_grant_statement(self, grant_statement: str) -> Grant:
670
+ """Parse grant statement to extract components"""
671
+ # Example: "GRANT create account ON account `root`@`localhost`"
672
+ try:
673
+ # Simple parsing - can be enhanced
674
+ parts = grant_statement.split()
675
+ privilege = parts[1] if len(parts) > 1 else None
676
+ object_type = parts[3] if len(parts) > 3 else None
677
+ object_name = parts[4] if len(parts) > 4 else None
678
+
679
+ # Extract user from the end
680
+ user_match = re.search(r"`([^`]+)`@`([^`]+)`", grant_statement)
681
+ user = f"{user_match.group(1)}@{user_match.group(2)}" if user_match else None
682
+
683
+ return Grant(
684
+ grant_statement=grant_statement,
685
+ privilege=privilege,
686
+ object_type=object_type,
687
+ object_name=object_name,
688
+ user=user,
689
+ )
690
+ except Exception:
691
+ return Grant(grant_statement=grant_statement)
692
+
693
+
694
+ class TransactionAccountManager(AccountManager):
695
+ """Transaction-scoped account manager"""
696
+
697
+ def __init__(self, transaction: "TransactionWrapper"):
698
+ super().__init__(transaction.client)
699
+ self._transaction = transaction
700
+
701
+ def _execute_sql(self, sql: str):
702
+ """Execute SQL within transaction"""
703
+ return self._transaction.execute(sql)
704
+
705
+ # Override all methods to use transaction
706
+ def create_account(self, account_name: str, admin_name: str, password: str, comment: Optional[str] = None) -> Account:
707
+ try:
708
+ sql_parts = [f"CREATE ACCOUNT {self._client._escape_identifier(account_name)}"]
709
+ sql_parts.append(f"ADMIN_NAME {self._client._escape_string(admin_name)}")
710
+ sql_parts.append(f"IDENTIFIED BY {self._client._escape_string(password)}")
711
+
712
+ if comment:
713
+ sql_parts.append(f"COMMENT {self._client._escape_string(comment)}")
714
+
715
+ sql = " ".join(sql_parts)
716
+ self._transaction.execute(sql)
717
+
718
+ return self.get_account(account_name)
719
+
720
+ except Exception as e:
721
+ raise AccountError(f"Failed to create account '{account_name}': {e}") from None
722
+
723
+ # Add other transaction methods as needed...