matrixone-python-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
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...
|