mysql-shell-client 0.6.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.
@@ -0,0 +1,7 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from .builders import *
5
+ from .clients import *
6
+ from .executors import *
7
+ from .models import *
@@ -0,0 +1,7 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from .authorization import *
5
+ from .locking import *
6
+ from .logging import *
7
+ from .quoting import *
@@ -0,0 +1,5 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from .base import BaseAuthorizationQueryBuilder
5
+ from .charm import CharmAuthorizationQueryBuilder
@@ -0,0 +1,23 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class BaseAuthorizationQueryBuilder(ABC):
8
+ """Base class for authorization query builders."""
9
+
10
+ @abstractmethod
11
+ def build_instance_auth_roles_query(self) -> str:
12
+ """Builds the instance roles creation query."""
13
+ raise NotImplementedError()
14
+
15
+ @abstractmethod
16
+ def build_instance_router_role_query(self, rolename: str) -> str:
17
+ """Builds the instance router role creation query."""
18
+ raise NotImplementedError()
19
+
20
+ @abstractmethod
21
+ def build_database_admin_role_query(self, rolename: str, database: str) -> str:
22
+ """Builds the database admin role creation query."""
23
+ raise NotImplementedError()
@@ -0,0 +1,213 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from ..quoting import StringQueryQuoter
5
+ from .base import BaseAuthorizationQueryBuilder
6
+
7
+
8
+ class CharmAuthorizationQueryBuilder(BaseAuthorizationQueryBuilder):
9
+ """Charm authorization query builder."""
10
+
11
+ ROLE_CREATION_QUERY = "CREATE ROLE {rolename}"
12
+ ROLE_GRANTING_QUERY = "GRANT {parents} TO {rolename}"
13
+ PRIV_GRANTING_QUERY = "GRANT {privileges} ON {database}.* TO {rolename}"
14
+
15
+ _DATA_PRIVILEGES = [
16
+ "SELECT",
17
+ "INSERT",
18
+ "DELETE",
19
+ "UPDATE",
20
+ "EXECUTE",
21
+ ]
22
+
23
+ _SCHEMA_PRIVILEGES = [
24
+ "ALTER",
25
+ "ALTER ROUTINE",
26
+ "CREATE",
27
+ "CREATE ROUTINE",
28
+ "CREATE VIEW",
29
+ "DROP",
30
+ "INDEX",
31
+ "LOCK TABLES",
32
+ "REFERENCES",
33
+ "TRIGGER",
34
+ ]
35
+
36
+ def __init__(
37
+ self,
38
+ role_admin: str,
39
+ role_backup: str,
40
+ role_ddl: str,
41
+ role_stats: str,
42
+ role_reader: str,
43
+ role_writer: str,
44
+ ):
45
+ """Initialize the query builder."""
46
+ self._quoter = StringQueryQuoter()
47
+ self._role_admin = self._quoter.quote_value(role_admin)
48
+ self._role_backup = self._quoter.quote_value(role_backup)
49
+ self._role_ddl = self._quoter.quote_value(role_ddl)
50
+ self._role_stats = self._quoter.quote_value(role_stats)
51
+ self._role_reader = self._quoter.quote_value(role_reader)
52
+ self._role_writer = self._quoter.quote_value(role_writer)
53
+
54
+ def _build_instance_admin_role_queries(self) -> list[str]:
55
+ """Builds the instance admin role creation queries."""
56
+ return [
57
+ self.ROLE_CREATION_QUERY.format(
58
+ rolename=self._role_admin,
59
+ ),
60
+ self.ROLE_GRANTING_QUERY.format(
61
+ parents=", ".join([
62
+ self._role_backup,
63
+ self._role_ddl,
64
+ self._role_stats,
65
+ self._role_writer,
66
+ ]),
67
+ rolename=self._role_admin,
68
+ ),
69
+ self.PRIV_GRANTING_QUERY.format(
70
+ privileges=", ".join([
71
+ *self._DATA_PRIVILEGES,
72
+ "EVENT",
73
+ "SHUTDOWN",
74
+ "AUDIT_ADMIN",
75
+ "CONNECTION_ADMIN",
76
+ "SYSTEM_VARIABLES_ADMIN",
77
+ ]),
78
+ rolename=self._role_admin,
79
+ database="*",
80
+ ),
81
+ ]
82
+
83
+ def _build_instance_backup_role_queries(self) -> list[str]:
84
+ """Builds the instance backups role creation queries."""
85
+ return [
86
+ self.ROLE_CREATION_QUERY.format(
87
+ rolename=self._role_backup,
88
+ ),
89
+ self.ROLE_GRANTING_QUERY.format(
90
+ parents=", ".join([self._role_stats]),
91
+ rolename=self._role_backup,
92
+ ),
93
+ self.PRIV_GRANTING_QUERY.format(
94
+ privileges=", ".join([
95
+ "EXECUTE",
96
+ "LOCK TABLES",
97
+ "PROCESS",
98
+ "RELOAD",
99
+ "BACKUP_ADMIN",
100
+ "CONNECTION_ADMIN",
101
+ ]),
102
+ rolename=self._role_backup,
103
+ database="*",
104
+ ),
105
+ ]
106
+
107
+ def _build_instance_ddl_role_queries(self) -> list[str]:
108
+ """Builds the instance backups role creation queries."""
109
+ return [
110
+ self.ROLE_CREATION_QUERY.format(
111
+ rolename=self._role_ddl,
112
+ ),
113
+ self.ROLE_GRANTING_QUERY.format(
114
+ parents=", ".join([self._role_writer]),
115
+ rolename=self._role_ddl,
116
+ ),
117
+ self.PRIV_GRANTING_QUERY.format(
118
+ privileges=", ".join([*self._SCHEMA_PRIVILEGES, "SHOW_ROUTINE", "SHOW VIEW"]),
119
+ rolename=self._role_ddl,
120
+ database="*",
121
+ ),
122
+ ]
123
+
124
+ def _build_instance_stats_role_queries(self) -> list[str]:
125
+ """Builds the instance stats role creation queries."""
126
+ return [
127
+ self.ROLE_CREATION_QUERY.format(
128
+ rolename=self._role_stats,
129
+ ),
130
+ self.PRIV_GRANTING_QUERY.format(
131
+ privileges=", ".join(["SELECT"]),
132
+ rolename=self._role_stats,
133
+ database="performance_schema",
134
+ ),
135
+ self.PRIV_GRANTING_QUERY.format(
136
+ privileges=", ".join(["PROCESS", "RELOAD", "REPLICATION CLIENT"]),
137
+ rolename=self._role_stats,
138
+ database="*",
139
+ ),
140
+ ]
141
+
142
+ def _build_instance_reader_role_queries(self) -> list[str]:
143
+ """Builds the instance reader role creation queries."""
144
+ return [
145
+ self.ROLE_CREATION_QUERY.format(
146
+ rolename=self._role_reader,
147
+ ),
148
+ ]
149
+
150
+ def _build_instance_writer_role_queries(self) -> list[str]:
151
+ """Builds the instance writer role creation queries."""
152
+ return [
153
+ self.ROLE_CREATION_QUERY.format(
154
+ rolename=self._role_writer,
155
+ ),
156
+ ]
157
+
158
+ def build_instance_auth_roles_query(self) -> str:
159
+ """Builds the instance roles creation query."""
160
+ return ";".join([
161
+ *self._build_instance_reader_role_queries(),
162
+ *self._build_instance_writer_role_queries(),
163
+ *self._build_instance_stats_role_queries(),
164
+ *self._build_instance_ddl_role_queries(),
165
+ *self._build_instance_backup_role_queries(),
166
+ *self._build_instance_admin_role_queries(),
167
+ ])
168
+
169
+ def build_instance_router_role_query(self, rolename: str) -> str:
170
+ """Builds the instance router role creation query."""
171
+ rolename = self._quoter.quote_value(rolename)
172
+
173
+ return ";".join([
174
+ self.ROLE_CREATION_QUERY.format(
175
+ rolename=rolename,
176
+ ),
177
+ (self.PRIV_GRANTING_QUERY + " WITH GRANT OPTION").format(
178
+ privileges=", ".join(["CREATE USER"]),
179
+ rolename=rolename,
180
+ database="*",
181
+ ),
182
+ self.PRIV_GRANTING_QUERY.format(
183
+ privileges=", ".join([*self._DATA_PRIVILEGES]),
184
+ rolename=rolename,
185
+ database="mysql_innodb_cluster_metadata",
186
+ ),
187
+ self.PRIV_GRANTING_QUERY.format(
188
+ privileges=", ".join(["SELECT"]),
189
+ rolename=rolename,
190
+ database="mysql",
191
+ ),
192
+ self.PRIV_GRANTING_QUERY.format(
193
+ privileges=", ".join(["SELECT"]),
194
+ rolename=rolename,
195
+ database="performance_schema",
196
+ ),
197
+ ])
198
+
199
+ def build_database_admin_role_query(self, rolename: str, database: str) -> str:
200
+ """Builds the database admin role creation query."""
201
+ database = self._quoter.quote_identifier(database)
202
+ rolename = self._quoter.quote_value(rolename)
203
+
204
+ return ";".join([
205
+ self.ROLE_CREATION_QUERY.format(
206
+ rolename=rolename,
207
+ ),
208
+ self.PRIV_GRANTING_QUERY.format(
209
+ privileges=", ".join([*self._DATA_PRIVILEGES, *self._SCHEMA_PRIVILEGES]),
210
+ rolename=rolename,
211
+ database=database,
212
+ ),
213
+ ])
@@ -0,0 +1,5 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from .base import BaseLockingQueryBuilder
5
+ from .charm import CharmLockingQueryBuilder
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class BaseLockingQueryBuilder(ABC):
8
+ """Base class for all the locking query builders."""
9
+
10
+ @abstractmethod
11
+ def build_table_creation_query(self) -> str:
12
+ """Builds the locking table creation query."""
13
+ raise NotImplementedError()
14
+
15
+ @abstractmethod
16
+ def build_fetch_acquired_query(self, task: str) -> str:
17
+ """Builds the acquired lock check query."""
18
+ raise NotImplementedError()
19
+
20
+ @abstractmethod
21
+ def build_acquire_query(self, task: str, instance: str) -> str:
22
+ """Builds the lock acquiring query."""
23
+ raise NotImplementedError()
24
+
25
+ @abstractmethod
26
+ def build_release_query(self, task: str, instance: str) -> str:
27
+ """Builds the lock releasing query."""
28
+ raise NotImplementedError()
@@ -0,0 +1,97 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from ..quoting import StringQueryQuoter
5
+ from .base import BaseLockingQueryBuilder
6
+
7
+
8
+ class CharmLockingQueryBuilder(BaseLockingQueryBuilder):
9
+ """Charm locking query builder."""
10
+
11
+ INSTANCE_ADDITION_TASK = "unit-add"
12
+ INSTANCE_REMOVAL_TASK = "unit-teardown"
13
+
14
+ TASKS = [
15
+ INSTANCE_ADDITION_TASK,
16
+ INSTANCE_REMOVAL_TASK,
17
+ ]
18
+
19
+ def __init__(self, table_schema: str, table_name: str):
20
+ """Initialize the query builder."""
21
+ self._quoter = StringQueryQuoter()
22
+ self._table = "{table_schema}.{table_name}".format(
23
+ table_schema=self._quoter.quote_identifier(table_schema),
24
+ table_name=self._quoter.quote_identifier(table_name),
25
+ )
26
+
27
+ def build_table_creation_query(self) -> str:
28
+ """Builds the locking table creation query."""
29
+ create_query = (
30
+ "CREATE TABLE IF NOT EXISTS {table} ( "
31
+ " task VARCHAR(20), "
32
+ " executor VARCHAR(20), "
33
+ " status VARCHAR(20), "
34
+ " PRIMARY KEY(task) "
35
+ ")"
36
+ )
37
+ insert_query = (
38
+ "INSERT INTO {table} (task, executor, status) "
39
+ "VALUES ('{task}', '', 'not-started') "
40
+ "ON DUPLICATE KEY UPDATE "
41
+ " executor = '', "
42
+ " status = 'not-started'"
43
+ )
44
+
45
+ create_queries = [create_query.format(table=self._table)]
46
+ insert_queries = [insert_query.format(table=self._table, task=task) for task in self.TASKS]
47
+
48
+ return ";".join((
49
+ *create_queries,
50
+ *insert_queries,
51
+ ))
52
+
53
+ def build_fetch_acquired_query(self, task: str) -> str:
54
+ """Builds the acquired lock fetch query."""
55
+ query = "SELECT executor FROM {table} WHERE task = {task} AND status = {status}"
56
+
57
+ return query.format(
58
+ table=self._table,
59
+ task=self._quoter.quote_value(task),
60
+ status=self._quoter.quote_value("in-progress"),
61
+ )
62
+
63
+ def build_acquire_query(self, task: str, instance: str) -> str:
64
+ """Builds the lock acquiring query."""
65
+ if task not in self.TASKS:
66
+ raise ValueError("Task not supported")
67
+
68
+ query = (
69
+ "UPDATE {table} "
70
+ "SET status = {status}, executor = {instance} "
71
+ "WHERE task = {task} AND executor = ''"
72
+ )
73
+
74
+ return query.format(
75
+ table=self._table,
76
+ task=self._quoter.quote_value(task),
77
+ instance=self._quoter.quote_value(instance),
78
+ status=self._quoter.quote_value("in-progress"),
79
+ )
80
+
81
+ def build_release_query(self, task: str, instance: str) -> str:
82
+ """Builds the lock releasing query."""
83
+ if task not in self.TASKS:
84
+ raise ValueError(f"Task not supported")
85
+
86
+ query = (
87
+ "UPDATE {table} "
88
+ "SET status = {status}, executor = '' "
89
+ "WHERE task = {task} AND executor = {instance}"
90
+ )
91
+
92
+ return query.format(
93
+ table=self._table,
94
+ task=self._quoter.quote_value(task),
95
+ instance=self._quoter.quote_value(instance),
96
+ status=self._quoter.quote_value("not-started"),
97
+ )
@@ -0,0 +1,5 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from .base import BaseLoggingQueryBuilder
5
+ from .charm import CharmLoggingQueryBuilder
@@ -0,0 +1,14 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from abc import ABC, abstractmethod
5
+ from typing import Sequence
6
+
7
+
8
+ class BaseLoggingQueryBuilder(ABC):
9
+ """Base class for all the logging query builders."""
10
+
11
+ @abstractmethod
12
+ def build_logs_flushing_query(self, logs: Sequence[str] | None) -> str:
13
+ """Builds the logs flushing query."""
14
+ raise NotImplementedError()
@@ -0,0 +1,36 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from typing import Sequence
5
+
6
+ from ...models import LogType
7
+ from ..quoting import StringQueryQuoter
8
+ from .base import BaseLoggingQueryBuilder
9
+
10
+
11
+ class CharmLoggingQueryBuilder(BaseLoggingQueryBuilder):
12
+ """Charm logging query builder."""
13
+
14
+ def __init__(self):
15
+ """Initialize the query builder."""
16
+ self._quoter = StringQueryQuoter()
17
+
18
+ def build_logs_flushing_query(self, logs: Sequence[LogType] | None = None) -> str:
19
+ """Builds the logs flushing query.
20
+
21
+ Arguments:
22
+ logs: a sequence of LogTypes to flush. If None, flush all
23
+ """
24
+ binlog_query = "SET @@SESSION.sql_log_bin = {value}"
25
+
26
+ if not logs:
27
+ flush_query = "FLUSH LOGS"
28
+ else:
29
+ flush_query = "FLUSH {log} LOGS"
30
+ flush_query = ";".join(flush_query.format(log=log.value) for log in logs)
31
+
32
+ return ";".join((
33
+ binlog_query.format(value=self._quoter.quote_value("OFF")),
34
+ flush_query,
35
+ binlog_query.format(value=self._quoter.quote_value("ON")),
36
+ ))
@@ -0,0 +1,46 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from typing import Any
5
+
6
+
7
+ class StringQueryQuoter:
8
+ """Class to escape and quote MySQL query input parameters."""
9
+
10
+ @staticmethod
11
+ def escape(value: Any) -> Any:
12
+ """Escapes a string that can be used in a SQL statement.
13
+
14
+ This function will only be used in the context of MySQL Shell,
15
+ as the recommended way to deal with SQL injection attacks
16
+ (using parametrized queries) is not available.
17
+
18
+ The code has been copied from the mysql-connector Python package.
19
+ https://github.com/mysql/mysql-connector-python/blob/8.0.33/lib/mysql/connector/conversion.py#L174-L201
20
+ """
21
+ if isinstance(value, (bytes, bytearray)):
22
+ value = value.replace(b"\\", b"\\\\")
23
+ value = value.replace(b"\n", b"\\n")
24
+ value = value.replace(b"\r", b"\\r")
25
+ value = value.replace(b"\140", b"\134\140") # tick quote
26
+ value = value.replace(b"\047", b"\134\047") # single quotes
27
+ value = value.replace(b"\042", b"\134\042") # double quotes
28
+ value = value.replace(b"\032", b"\134\032") # for Win32
29
+ elif isinstance(value, str):
30
+ value = value.replace("\\", "\\\\")
31
+ value = value.replace("\n", "\\n")
32
+ value = value.replace("\r", "\\r")
33
+ value = value.replace("\140", "\134\140") # tick quote
34
+ value = value.replace("\047", "\134\047") # single quotes
35
+ value = value.replace("\042", "\134\042") # double quotes
36
+ value = value.replace("\032", "\134\032") # for Win32
37
+
38
+ return value
39
+
40
+ def quote_value(self, value: Any) -> str:
41
+ """Quotes the provided value."""
42
+ return f"'{self.escape(value)}'"
43
+
44
+ def quote_identifier(self, name: str) -> str:
45
+ """Quotes the provided identifier name."""
46
+ return f"`{self.escape(name)}`"
@@ -0,0 +1,5 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ from .cluster import *
5
+ from .instance import *