pystratum_mysql.helper.MySqlRoutineLoaderHelper   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Test Coverage

Coverage 19.61%

Importance

Changes 0
Metric Value
wmc 39
eloc 128
dl 0
loc 250
ccs 20
cts 102
cp 0.1961
rs 9.28
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
B MySqlRoutineLoaderHelper._get_bulk_insert_table_columns_info() 0 32 5
A MySqlRoutineLoaderHelper.__init__() 0 50 1
B MySqlRoutineLoaderHelper._get_routine_parameters_info() 0 20 7
A MySqlRoutineLoaderHelper._is_start_of_stored_routine_body() 0 7 1
A MySqlRoutineLoaderHelper._get_name() 0 17 4
A MySqlRoutineLoaderHelper._log_exception() 0 22 5
A MySqlRoutineLoaderHelper._drop_routine() 0 6 2
C MySqlRoutineLoaderHelper._must_reload() 0 28 11
A MySqlRoutineLoaderHelper._is_start_of_stored_routine() 0 7 1
A MySqlRoutineLoaderHelper._load_routine_file() 0 14 1
A MySqlRoutineLoaderHelper._get_data_type_helper() 0 5 1
1 1
import re
2 1
from typing import Any, Dict, Optional
3
4 1
from mysql import connector
5 1
from pystratum_backend.StratumIO import StratumIO
6
from pystratum_common.exception.LoaderException import LoaderException
7 1
from pystratum_common.helper.DataTypeHelper import DataTypeHelper
8 1
from pystratum_common.helper.RoutineLoaderHelper import RoutineLoaderHelper
9 1
10 1
from pystratum_mysql.helper.MySqlDataTypeHelper import MySqlDataTypeHelper
11 1
from pystratum_mysql.MySqlMetadataDataLayer import MySqlMetadataDataLayer
12
13
14 1
class MySqlRoutineLoaderHelper(RoutineLoaderHelper):
15
    """
16
    Class for loading a single stored routine into a MySQL instance from a (pseudo) SQL file.
17
    """
18
19
    # ------------------------------------------------------------------------------------------------------------------
20 1
    def __init__(self,
21
                 io: StratumIO,
22
                 dl: MySqlMetadataDataLayer,
23
                 routine_filename: str,
24
                 routine_file_encoding: str,
25
                 pystratum_old_metadata: Optional[Dict],
26
                 replace_pairs: Dict[str, Any],
27
                 rdbms_old_metadata: Optional[Dict],
28
                 sql_mode: str,
29
                 character_set: str,
30
                 collate: str):
31
        """
32
        Object constructor.
33
                                
34
        :param io: The output decorator.
35
        :param dl: The metadata layer.
36
        :param routine_filename: The filename of the source of the stored routine.
37
        :param routine_file_encoding: The encoding of the source file.
38
        :param pystratum_old_metadata: The metadata of the stored routine from PyStratum.
39
        :param replace_pairs: A map from placeholders to their actual values.
40
        :param rdbms_old_metadata: The old metadata of the stored routine from MS SQL Server.
41
        :param sql_mode: The SQL mode under which the stored routine must be loaded and run.
42
        :param character_set: The default character set under which the stored routine must be loaded and run.
43
        :param collate: The default collate under which the stored routine must be loaded and run.
44
        """
45
        RoutineLoaderHelper.__init__(self,
46
                                     io,
47
                                     routine_filename,
48
                                     routine_file_encoding,
49
                                     pystratum_old_metadata,
50
                                     replace_pairs,
51
                                     rdbms_old_metadata)
52
53
        self._character_set: str = character_set
54
        """
55
        The default character set under which the stored routine will be loaded and run.
56
        """
57
58
        self._collate: str = collate
59
        """
60
        The default collate under which the stored routine will be loaded and run.
61
        """
62
63
        self._sql_mode: str = sql_mode
64
        """
65
        The SQL-mode under which the stored routine will be loaded and run.
66
        """
67
68
        self._dl: MySqlMetadataDataLayer = dl
69
        """
70
        The metadata layer.
71
        """
72
73
    # ------------------------------------------------------------------------------------------------------------------
74 1
    def _get_bulk_insert_table_columns_info(self) -> None:
75
        """
76
        Gets the column names and column types of the current table for bulk insert.
77
        """
78
        table_is_non_temporary = self._dl.check_table_exists(self._table_name)
79
80
        if not table_is_non_temporary:
81
            self._dl.call_stored_routine(self._routine_name)
82
83
        columns = self._dl.describe_table(self._table_name)
84
85
        tmp_column_types = []
86
        tmp_fields = []
87
88
        n1 = 0
89
        for column in columns:
90
            prog = re.compile('(\\w+)')
91
            c_type = prog.findall(column['Type'])
92
            tmp_column_types.append(c_type[0])
93
            tmp_fields.append(column['Field'])
94
            n1 += 1
95
96
        n2 = len(self._columns)
97
98
        if not table_is_non_temporary:
99
            self._dl.drop_temporary_table(self._table_name)
100
101
        if n1 != n2:
102
            raise LoaderException("Number of fields %d and number of columns %d don't match." % (n1, n2))
103
104
        self._columns_types = tmp_column_types
105
        self._fields = tmp_fields
106
107
    # ------------------------------------------------------------------------------------------------------------------
108 1
    def _get_data_type_helper(self) -> DataTypeHelper:
109
        """
110
        Returns a data type helper object for MySQL.
111
        """
112
        return MySqlDataTypeHelper()
113
114
    # ------------------------------------------------------------------------------------------------------------------
115
    def _get_name(self) -> None:
116
        """
117 1
        Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source.
118
        """
119
        prog = re.compile("create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)")
120
        matches = prog.findall(self._routine_source_code)
121
122
        if matches:
123
            self._routine_type = matches[0][0].lower()
124
125
            if self._routine_name != matches[0][1]:
126
                raise LoaderException('Stored routine name {0} does not match filename in file {1}'.
127
                                      format(matches[0][1], self._source_filename))
128
129
        if not self._routine_type:
130
            raise LoaderException('Unable to find the stored routine name and type in file {0}'.
131
                                  format(self._source_filename))
132
133
    # ------------------------------------------------------------------------------------------------------------------
134
    def _get_routine_parameters_info(self) -> None:
135
        """
136 1
        Retrieves information about the stored routine parameters from the metadata of MySQL.
137
        """
138
        routine_parameters = self._dl.get_routine_parameters(self._routine_name)
139
        for routine_parameter in routine_parameters:
140
            if routine_parameter['parameter_name']:
141
                value = routine_parameter['column_type']
142
                if 'character_set_name' in routine_parameter:
143
                    if routine_parameter['character_set_name']:
144
                        value += ' character set %s' % routine_parameter['character_set_name']
145
                if 'collation' in routine_parameter:
146
                    if routine_parameter['character_set_name']:
147
                        value += ' collation %s' % routine_parameter['collation']
148
149
                self._parameters.append({'name':                 routine_parameter['parameter_name'],
150
                                         'data_type':            routine_parameter['parameter_type'],
151
                                         'numeric_precision':    routine_parameter['numeric_precision'],
152
                                         'numeric_scale':        routine_parameter['numeric_scale'],
153
                                         'data_type_descriptor': value})
154
155
    # ------------------------------------------------------------------------------------------------------------------
156
    def _is_start_of_stored_routine(self, line: str) -> bool:
157
        """
158 1
        Returns True if a line is the start of the code of the stored routine.
159
160
        :param line: The line with source code of the stored routine.
161
        """
162
        return re.match(r'^\s*create\s+(procedure|function)', line, re.IGNORECASE) is not None
163
164
    # ------------------------------------------------------------------------------------------------------------------
165
    def _is_start_of_stored_routine_body(self, line: str) -> bool:
166
        """
167
        Returns True if a line is the start of the body of the stored routine.
168
169 1
        :param line: The line with source code of the stored routine.
170
        """
171
        return re.match(r'^\s*begin', line, re.IGNORECASE) is not None
172
173
    # ------------------------------------------------------------------------------------------------------------------
174
    def _load_routine_file(self) -> None:
175
        """
176
        Loads the stored routine into the MySQL instance.
177
        """
178
        self._io.text('Loading {0} <dbo>{1}</dbo>'.format(self._routine_type, self._routine_name))
179
180 1
        self._unset_magic_constants()
181
        self._drop_routine()
182
183
        self._dl.set_sql_mode(self._sql_mode)
184
185
        self._dl.set_character_set(self._character_set, self._collate)
186
187
        self._dl.execute_none(self._routine_source_code)
188
189
    # ------------------------------------------------------------------------------------------------------------------
190
    def _log_exception(self, exception: Exception) -> None:
191
        """
192
        Logs an exception.
193
194
        :param exception: The exception.
195
        """
196 1
        RoutineLoaderHelper._log_exception(self, exception)
197
198
        if isinstance(exception, connector.errors.Error):
199
            if exception.errno == 1064:
200
                # Exception is caused by an invalid SQL statement.
201
                sql = self._dl.last_sql()
202
                if sql:
203
                    sql = sql.strip()
204
                    # The format of a 1064 message is: %s near '%s' at line %d
205
                    parts = re.search(r'(\d+)$', exception.msg)
206
                    if parts:
207
                        error_line = int(parts.group(1))
208
                    else:
209
                        error_line = 0
210
211
                    self._print_sql_with_error(sql, error_line)
212
213
    # ------------------------------------------------------------------------------------------------------------------
214
    def _must_reload(self) -> bool:
215
        """
216
        Returns whether the source file must be load or reloaded.
217
        """
218
        if not self._pystratum_old_metadata:
219
            return True
220 1
221
        if self._pystratum_old_metadata['timestamp'] != self._m_time:
222
            return True
223
224
        if self._pystratum_old_metadata['replace']:
225
            for key, value in self._pystratum_old_metadata['replace'].items():
226
                if key.lower() not in self._replace_pairs or self._replace_pairs[key.lower()] != value:
227
                    return True
228
229
        if not self._rdbms_old_metadata:
230
            return True
231
232
        if self._rdbms_old_metadata['sql_mode'] != self._sql_mode:
233
            return True
234
235
        if self._rdbms_old_metadata['character_set_client'] != self._character_set:
236
            return True
237
238
        if self._rdbms_old_metadata['collation_connection'] != self._collate:
239
            return True
240
241
        return False
242
243
    # ------------------------------------------------------------------------------------------------------------------
244
    def _drop_routine(self) -> None:
245
        """
246
        Drops the stored routine if it exists.
247
        """
248
        if self._rdbms_old_metadata:
249
            self._dl.drop_stored_routine(self._rdbms_old_metadata['routine_type'], self._routine_name)
250
251
# ----------------------------------------------------------------------------------------------------------------------
252