userver
C++ Async Framework
Toggle main menu visibility
Loading...
Searching...
No Matches
control.py
1
import
contextlib
2
import
dataclasses
3
import
logging
4
import
pathlib
5
6
import
pymysql
7
import
pymysql.constants
8
9
from
testsuite.environment
import
shell
10
from
testsuite.utils
import
cached_property
11
12
from
.
import
classes, exceptions
13
14
logger = logging.getLogger(__name__)
15
16
MYSQL_HELPER = pathlib.Path(__file__).parent.joinpath(
'scripts/mysql-helper'
)
17
18
19
@dataclasses.dataclass(frozen=True)
20
class
MysqlQuery
:
21
body: str
22
source: str
23
path: str |
None
24
25
26
class
ConnectionWrapper
:
27
"""MySQL database connection wrapper."""
28
29
def
__init__(self, connection, conninfo, tables):
30
self.
_connection
= connection
31
self.
_conninfo
= conninfo
32
self.
_tables
: list[str] = tables
33
34
@property
35
def
conninfo
(self) -> classes.ConnectionInfo:
36
""":py:class:`classes.ConnectionInfo` instance."""
37
return
self.
_conninfo
38
39
def
cursor
(self, **kwargs) -> pymysql.cursors.Cursor:
40
"""Returns cursor instance."""
41
return
self.
_connection
.
cursor
(**kwargs)
42
43
def
dict_cursor
(self, **kwargs) -> pymysql.cursors.Cursor:
44
"""Return dictionary cursor, pymysql.cursors.DictCursor."""
45
kwargs[
'cursor'
] = pymysql.cursors.DictCursor
46
return
self.
cursor
(**kwargs)
47
48
def
commit(self) -> None:
49
self.
_connection
.commit()
50
51
def
_truncate_non_empty_tables(self) -> list[str] | None:
52
cursor = self.
cursor
()
53
if
self.
_tables
:
54
with
contextlib.closing(cursor):
55
queries = []
56
for
table
in
self.
_tables
:
57
queries.append(
58
f
"select '{table}' as name, count(*) as c from {table}"
59
)
60
subquery =
' union '
.join(queries)
61
query = f
'select name from ({subquery}) tables where c>0;'
62
cursor.execute(query)
63
tables = cursor.fetchall()
64
return
[table
for
(table,)
in
tables]
65
return
None
66
67
def
apply_queries(
68
self,
69
queries: list[MysqlQuery],
70
keep_tables: list[str] |
None
=
None
,
71
truncate_non_empty: bool =
False
,
72
) ->
None
:
73
if
not
keep_tables:
74
keep_tables = []
75
with
self.
cursor
()
as
cursor:
76
if
truncate_non_empty:
77
tables = self.
_truncate_non_empty_tables
()
78
else
:
79
tables = self.
_tables
80
81
if
tables:
82
truncate_table_sql = []
83
for
table
in
tables:
84
if
table
not
in
keep_tables:
85
truncate_table_sql.append(f
'truncate table {table};'
)
86
truncate_tables_sql =
' '
.join(truncate_table_sql)
87
cursor.execute(
88
'set foreign_key_checks=0;'
89
f
'{truncate_tables_sql}'
90
'set foreign_key_checks=1;'
,
91
)
92
for
query
in
queries:
93
try
:
94
cursor.execute(query.body, args=[])
95
except
pymysql.Error
as
exc:
96
error_message = (
97
f
'MySQL apply query error\nQuery from: {query.source}\n'
98
)
99
if
query.path:
100
error_message += f
'File path: {query.path}\n'
101
error_message +=
'\n'
+ str(exc)
102
raise
exceptions.MysqlError
(error_message)
103
self.
commit
()
104
105
106
class
ConnectionCache
:
107
def
__init__(self, conninfo, verbose: bool =
False
):
108
self.
_conninfo
= conninfo
109
self.
_cache
: dict = {}
110
self.
_master_connection
=
None
111
112
def
get_master_connection(self):
113
if
self.
_master_connection
is
None
:
114
self.
_master_connection
= self.
_connect
(self.
_conninfo
)
115
return
self.
_master_connection
116
117
def
get_conninfo(self, dbname: str) ->
classes.ConnectionInfo
:
118
return
self.
_conninfo
.replace(dbname=dbname)
119
120
def
get_connection(self, dbname):
121
if
dbname
not
in
self.
_cache
:
122
self.
_cache
[dbname] = self.
_create_connection
(dbname)
123
return
self.
_cache
[dbname]
124
125
def
_create_connection(self, dbname):
126
return
self.
_connect
(self.
get_conninfo
(dbname))
127
128
def
_connect(self, conninfo: classes.ConnectionInfo):
129
return
pymysql.connect(
130
host=conninfo.hostname,
131
port=conninfo.port,
132
user=conninfo.user,
133
password=conninfo.password
or
''
,
134
database=conninfo.dbname,
135
client_flag=pymysql.constants.CLIENT.MULTI_STATEMENTS,
136
)
137
138
139
class
DatabasesState
:
140
_migrations_run: set[tuple[str, str]]
141
_initialized: set[str]
142
143
def
__init__(self, connections: ConnectionCache, verbose: bool =
False
):
144
self.
_need_save_tables
=
True
145
self.
_connections
= connections
146
self.
_verbose
= verbose
147
self.
_migrations_run
= set()
148
self.
_initialized
= set()
149
self.
_tables
: dict[str, list[str]] = dict()
150
151
def
get_connection(self, dbname: str, create_db: bool =
True
):
152
if
dbname
not
in
self.
_initialized
:
153
if
create_db:
154
self.
_initdb
(dbname)
155
self.
_initialized
.add(dbname)
156
return
self.
_connections
.get_connection(dbname)
157
158
def
wrapper_for(self, dbname: str):
159
return
ConnectionWrapper
(
160
self.
_connections
.get_connection(dbname),
161
self.
_connections
.get_conninfo(dbname),
162
self.
_tables
.get(dbname),
163
)
164
165
def
run_migration(self, dbname: str, path: str):
166
key = dbname, path
167
if
key
in
self.
_migrations_run
:
168
return
169
logger.debug(
170
'Running mysql script %s against database %s'
,
171
path,
172
dbname,
173
)
174
conninfo = self.
_connections
.get_conninfo(dbname)
175
_run_script(conninfo, [
'-e'
, f
'source {path}'
], verbose=self.
_verbose
)
176
self.
_migrations_run
.add(key)
177
self.
_need_save_tables
=
True
178
179
@cached_property
180
def
known_databases(self):
181
connection = self.
_connections
.get_master_connection()
182
cursor = connection.cursor()
183
cursor.execute(
'show databases'
)
184
return
{row[0]
for
row
in
cursor.fetchall()}
185
186
def
_initdb(self, dbname: str):
187
connection = self.
_connections
.get_master_connection()
188
with
connection.cursor()
as
cursor:
189
if
dbname
in
self.
known_databases
:
190
cursor.execute(f
'DROP DATABASE IF EXISTS `{dbname}`'
)
191
cursor.execute(f
'CREATE DATABASE `{dbname}`'
)
192
connection.commit()
193
self.
_initialized
.add(dbname)
194
195
def
save_tables(self, dbname: str) ->
None
:
196
if
not
self.
_need_save_tables
:
197
return
198
connection = self.
_connections
.get_connection(dbname)
199
cursor = connection.cursor()
200
with
contextlib.closing(cursor):
201
cursor.execute(
'show tables'
)
202
self.
_tables
[dbname] = [table
for
(table,)
in
cursor.fetchall()]
203
self.
_need_save_tables
=
False
204
205
206
class
Control
:
207
def
__init__(
208
self,
209
databases: classes.DatabasesDict,
210
state: DatabasesState,
211
):
212
self.
_databases
= databases
213
self.
_state
= state
214
215
def
get_wrappers(self):
216
return
{
217
alias: self.
_state
.wrapper_for(dbconfig.dbname)
218
for
alias, dbconfig
in
self.
_databases
.items()
219
}
220
221
def
run_migrations(self):
222
for
dbconfig
in
self.
_databases
.values():
223
self.
_run_database_migrations
(dbconfig)
224
225
def
_run_database_migrations(self, dbconfig):
226
self.
_state
.get_connection(dbconfig.dbname, create_db=dbconfig.create)
227
for
path
in
dbconfig.migrations:
228
self.
_state
.run_migration(dbconfig.dbname, path)
229
self.
_state
.save_tables(dbconfig.dbname)
230
231
232
def
_build_mysql_args(conninfo: classes.ConnectionInfo) -> list[str]:
233
result = [
'--protocol=tcp'
]
234
if
conninfo.hostname:
235
result.append(f
'--host={conninfo.hostname}'
)
236
if
conninfo.port:
237
result.append(f
'--port={conninfo.port}'
)
238
if
conninfo.user:
239
result.append(f
'--user={conninfo.user}'
)
240
if
conninfo.password:
241
result.append(f
'--password={conninfo.password}'
)
242
if
conninfo.dbname:
243
result.append(f
'--database={conninfo.dbname}'
)
244
return
result
245
246
247
def
_run_script(
248
conninfo: classes.ConnectionInfo,
249
args: list[str],
250
verbose: bool,
251
):
252
command = [str(MYSQL_HELPER), *_build_mysql_args(conninfo), *args]
253
shell.execute(command, verbose=verbose, command_alias=
'mysql/script'
)
en
testsuite
databases
mysql
control.py
Generated on
for userver by
Doxygen
1.17.0