File: server_errors.py

package info (click to toggle)
boost1.90 1.90.0-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 593,120 kB
  • sloc: cpp: 4,190,908; xml: 196,648; python: 34,618; ansic: 23,145; asm: 5,468; sh: 3,774; makefile: 1,161; perl: 1,020; sql: 728; ruby: 676; yacc: 478; java: 77; lisp: 24; csh: 6
file content (388 lines) | stat: -rwxr-xr-x 14,407 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
#!/usr/bin/python3
#
# Copyright (c) 2019-2025 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#

# This scripts generates files containing the server defined error codes,
# and code to convert from error codes to strings. This is complex because:
#  - There are *a lot* of error codes.
#  - There are common error codes and MariaDB/MySQL specific ones.
#  - Some codes have been repurposed, renamed or removed from MySQL 5.x to MySQL 8.x and MariaDB.
# To generate precise output, we need the mysqld_error.h header for MySQL 5.x, 8.x and MariaDB.
# MySQL and MariaDB often rename, deprecate, etc. codes. To attempt to maintain sanity,
# we generate a CSV file with what we did for the previous version. We load it and combine it
# with the new errors, and we only perform backwards-compatible changes.
#
# To update the codes for a new release:
#   - MySQL: https://dev.mysql.com/downloads/mysql/
#   - MariaDB: https://mariadb.org/connector-c/all-releases/

import pandas as pd
from os import path
from pathlib import Path
from typing import Literal, List, Optional, cast, NamedTuple
from subprocess import run


# DataFrames have 'symbol', 'numbr' columns
class ErrorCodes(NamedTuple):
    common: pd.DataFrame
    mysql: pd.DataFrame
    mariadb: pd.DataFrame


REPO_BASE = Path(path.abspath(path.join(path.dirname(path.realpath(__file__)), '..', '..')))
_CSV_PATH = REPO_BASE.joinpath('tools', 'error_codes.csv')

# All server errors range between 1000 and 4999. Errors between 2000 and 2999
# are reserved for the client and are not used. In theory, codes between COMMON_ERROR_FIRST
# and COMMON_ERROR_LAST are shared between MySQL and MariaDB. However, some exceptions apply -
# some codes were not used originally by MySQL and are now used only by MariaDB, some have been renamed,
# etc. All codes >= COMMON_ERROR_LAST are server-specific. Codes between [COMMON_ERROR_FIRST, COMMON_ERROR_LAST)
# may be euther common or server-specific.
COMMON_ERROR_FIRST = 1000
COMMON_ERROR_LAST = 1880
SERVER_ERROR_LAST = 5000

COMMON_SERVER_ERRC_ENTRY = '''
    /**
     * \\brief Common server error. Error number: {number}, symbol:
     * <a href="https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_{symbol_lower}">{symbol_upper}</a>.
     */
    {symbol_lower} = {number},
'''

COMMON_SERVER_ERRC_TEMPLATE = '''
#ifndef BOOST_MYSQL_COMMON_SERVER_ERRC_HPP
#define BOOST_MYSQL_COMMON_SERVER_ERRC_HPP

#include <boost/mysql/error_code.hpp>

#include <boost/mysql/detail/config.hpp>

#include <boost/system/error_category.hpp>

namespace boost {{
namespace mysql {{

/**
 * \\brief Server-defined error codes, shared between MySQL and MariaDB.
 * \\details The numeric value and semantics match the ones described in the MySQL documentation.
 * For more info, consult the error reference for
 * <a href="https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html">MySQL 8.0</a>, 
 * <a href="https://dev.mysql.com/doc/mysql-errors/5.7/en/server-error-reference.html">MySQL 5.7</a>,
 * <a href="https://mariadb.com/kb/en/mariadb-error-codes/">MariaDB</a>.
 */
enum class common_server_errc : int
{{
{}
}};

BOOST_MYSQL_DECL
const boost::system::error_category& get_common_server_category() noexcept;

/// Creates an \\ref error_code from a \\ref common_server_errc.
inline error_code make_error_code(common_server_errc error)
{{
    return error_code(static_cast<int>(error), get_common_server_category());
}}

}}  // namespace mysql

#ifndef BOOST_MYSQL_DOXYGEN
namespace system {{

template <>
struct is_error_code_enum<::boost::mysql::common_server_errc>
{{
    static constexpr bool value = true;
}};

}}  // namespace system
#endif

}}  // namespace boost

#ifdef BOOST_MYSQL_HEADER_ONLY
#include <boost/mysql/impl/error_categories.ipp>
#endif

#endif
'''

# Render the enumeration with common codes
def render_common_server_errc(df_common: pd.DataFrame) -> str:
    entries = ''.join(COMMON_SERVER_ERRC_ENTRY.format(
        number=r.numbr,
        symbol_upper=r.symbol,
        symbol_lower=r.symbol.lower()
    ) for r in df_common.itertuples())
    return COMMON_SERVER_ERRC_TEMPLATE.format(entries)


SPECIFIC_SERVER_ERRC_ENTRY = '''
/// Server error specific to {flavor}. Error number: {number}, symbol: {symbol_upper}.
BOOST_INLINE_CONSTEXPR int {symbol_lower} = {number};
'''

SPECIFIC_SERVER_ERRC_TEMPLATE = '''
#ifndef BOOST_MYSQL_{flavor}_SERVER_ERRC_HPP
#define BOOST_MYSQL_{flavor}_SERVER_ERRC_HPP

#include <boost/config.hpp>

namespace boost {{
namespace mysql {{

namespace {flavor}_server_errc {{
{entries}
}}  // namespace {flavor}_server_errc

}}  // namespace mysql
}}  // namespace boost

#endif
'''

# Render error codes specific to a server
def render_server_specific_errc(flavor: Literal['mysql', 'mariadb'], df_db: pd.DataFrame):
    entries = ''.join(SPECIFIC_SERVER_ERRC_ENTRY.format(
        number=r.numbr,
        symbol_upper=r.symbol,
        symbol_lower=r.symbol.lower(),
        flavor=flavor
    ) for r in df_db.itertuples())
    return SPECIFIC_SERVER_ERRC_TEMPLATE.format(entries=entries, flavor=flavor)

SERVER_ERROR_TO_STRING_TEMPLATE_ENTRY='    case {number}: return "{symbol_lower}";\n'

SERVER_ERROR_TO_STRING_TEMPLATE='''
#ifndef BOOST_MYSQL_IMPL_INTERNAL_ERROR_SERVER_ERROR_TO_STRING_HPP
#define BOOST_MYSQL_IMPL_INTERNAL_ERROR_SERVER_ERROR_TO_STRING_HPP

// This file was generated by server_errors.py - do not edit directly.

#include <boost/config.hpp>
#include <boost/mysql/impl/internal/error/server_error_to_string.hpp>

namespace boost {{
namespace mysql {{
namespace detail {{

BOOST_INLINE_CONSTEXPR const char* common_error_messages[] = {{
{common_entries}
}};

}} // namespace detail
}} // namespace mysql
}} // namespace boost

const char* boost::mysql::detail::common_error_to_string(int v)
{{
    constexpr int first = {common_error_first};
    constexpr int last = first + sizeof(common_error_messages) / sizeof(const char*);
    return (v >= first && v < last) ? common_error_messages[v - first] : nullptr;
}}

const char* boost::mysql::detail::mysql_error_to_string(int v)
{{
    switch (v)
    {{
{mysql_entries}
    default: return "<unknown MySQL-specific server error>";
    }}
}}

const char* boost::mysql::detail::mariadb_error_to_string(int v)
{{
    switch (v)
    {{
{mariadb_entries}
    default: return "<unknown MariaDB-specific server error>";
    }}
}}

#endif

'''

# Renders the cpp that implements server error codes to strings
def render_server_error_to_string(df_common: pd.DataFrame, df_mysql: pd.DataFrame, df_mariadb: pd.DataFrame) -> str:
    # Common entries. We need to include non-present entries here, too (as nullptr's)
    number_to_symbol = df_common.set_index('numbr')['symbol']
    symbols = [cast(Optional[str], number_to_symbol.get(i)) for i in range(COMMON_ERROR_FIRST, COMMON_ERROR_LAST)]
    common_entries_list = [f'"{elm.lower()}"' if elm is not None else 'nullptr' for elm in symbols]
    common_entries = ''.join(f'    {elm},\n' for elm in common_entries_list)

    # DB specific entries
    def _gen_specific_entries(df_db: pd.DataFrame) -> str:
        return ''.join(SERVER_ERROR_TO_STRING_TEMPLATE_ENTRY.format(
            number=r.numbr,
            symbol_lower=r.symbol.lower()
        ) for r in df_db.itertuples())
    mysql_entries = _gen_specific_entries(df_mysql)
    mariadb_entries = _gen_specific_entries(df_mariadb)
     
    return SERVER_ERROR_TO_STRING_TEMPLATE.format(
        common_error_first=COMMON_ERROR_FIRST,
        common_entries=common_entries,
        mysql_entries=mysql_entries,
        mariadb_entries=mariadb_entries
    )


# Parse a header into a dataframe of (number, symbol) pairs
def parse_err_header(fname: Path) -> pd.DataFrame:
    with open(fname, 'rt') as f:
        lines = f.read().split('\n')
    v = [elm for elm in lines if elm.startswith('#define')]
    v = [elm.split(' ')[1:] for elm in v]
    df = pd.DataFrame(v, columns=['symbol', 'numbr'])
    df = df[~df['numbr'].isna()]
    df['numbr'] = df['numbr'].astype(int)
    df = df[df['numbr'] < SERVER_ERROR_LAST]
    # Discard pseudo error codes that some header have
    df = df[df['symbol'].map(lambda x: not (
        x.startswith('ER_ERROR_FIRST') or
        x.startswith('ER_ERROR_LAST') or
        x == 'ER_LAST_MYSQL_ERROR_MESSAGE' or
        x.startswith('ER_UNUSED') or
        x.endswith('__UNUSED')
    ))]
    return df


# MySQL 5.x and 8.x don't fully agree on error names. Some names have been
# removed, others have been added and others have been renamed. We merge
# both so the library can be used with both systems. In case of conflict, pick the 8.x name
# (they generally add a _UNUSED suffix for the codes they no longer use).
# Some symbols appear both in 5.x and 8.x but with different values - we pick the 8.x in
# case of conflict. 
def merge_mysql_errors(df_mysql5: pd.DataFrame, df_mysql8: pd.DataFrame) -> pd.DataFrame:
    def resolve_symbol(r):
        s5 = r['symbol_5']
        s8 = r['symbol_8']
        if not pd.isna(s5) and pd.isna(s8):
            symbol, dbver = s5, 5
        else:
            symbol, dbver = s8, 8
        return pd.Series(dict(numbr=r['numbr'], symbol=symbol, dbver=dbver))
    
    return df_mysql5 \
        .join(df_mysql8.set_index('numbr'), how='outer', on='numbr', lsuffix='_5', rsuffix='_8') \
        .apply(resolve_symbol, axis=1) \
        .sort_values(by='dbver') \
        .drop_duplicates(['symbol'], keep='last') \
        .drop(columns=['dbver'])


# Split between common and specific codes
def generate_error_ranges(df_mysql: pd.DataFrame, df_mariadb: pd.DataFrame) -> ErrorCodes:
    # Join
    joined = df_mysql.join(df_mariadb.set_index('numbr'), how='outer', on='numbr', lsuffix='_mysql', rsuffix='_mariadb')
    joined = joined[joined['numbr'] < COMMON_ERROR_LAST]

    # Common range
    res_common = joined[joined['symbol_mysql'] == joined['symbol_mariadb']]
    res_common = res_common.rename(columns={'symbol_mysql': 'symbol'}).drop(columns=['symbol_mariadb'])

    # Values in the common range that differ between mysql and mariadb
    joined_different = joined[joined['symbol_mysql'] != joined['symbol_mariadb']]
    res_mysql_1 = joined_different[joined_different['symbol_mysql'].notna()].rename(columns={'symbol_mysql': 'symbol'}).drop(columns=['symbol_mariadb'])
    res_mariadb_1 = joined_different[joined_different['symbol_mariadb'].notna()].rename(columns={'symbol_mariadb': 'symbol'}).drop(columns=['symbol_mysql'])

    # Values that are outside the common range
    res_mysql_2 = df_mysql[df_mysql['numbr'] >= COMMON_ERROR_LAST]
    res_mariadb_2 = df_mariadb[df_mariadb['numbr'] >= COMMON_ERROR_LAST]

    return ErrorCodes(
        common=res_common.sort_values(by='numbr'),
        mysql=pd.concat([res_mysql_1, res_mysql_2]).sort_values(by='numbr'),
        mariadb=pd.concat([res_mariadb_1, res_mariadb_2]).sort_values(by='numbr')
    )

# Does the full process. folder should contain the relevant headers
def parse_headers(folder: Path) -> ErrorCodes:
    df_mysql8_header = parse_err_header(folder.joinpath('mysql8.h'))
    df_mysql5_header = parse_err_header(folder.joinpath('mysql5.h'))
    df_mariadb_header = parse_err_header(folder.joinpath('mariadb.h'))
    df_mysql_header = merge_mysql_errors(df_mysql5_header, df_mysql8_header)
    return generate_error_ranges(df_mysql_header, df_mariadb_header)


# Writes a CSV file with the contents of headers, so we can keep track of what we did in the last Boost version
def write_csv(codes: ErrorCodes) -> None:
    df = pd.concat([
        codes.common.assign(category='common'),
        codes.mysql.assign(category='mysql'),
        codes.mariadb.assign(category='mariadb')
    ]).sort_values(by=['numbr', 'category'])
    df.to_csv(_CSV_PATH, index=False)


# Loads the CSV file from the previous version
def load_csv() -> ErrorCodes:
    df = pd.read_csv(_CSV_PATH)
    return ErrorCodes(
        common=df[df['category'] == 'common'].drop(columns=['category']),
        mysql=df[df['category'] == 'mysql'].drop(columns=['category']),
        mariadb=df[df['category'] == 'mariadb'].drop(columns=['category']),
    )


def _merge_new_codes_single(df_common: pd.DataFrame, df_old: pd.DataFrame, df_new: pd.DataFrame) -> pd.DataFrame:
    # Remove anything that's already present in the current headers
    temp = pd.concat([df_common, df_new, df_common]).drop_duplicates(subset='numbr', keep=False)
    temp = pd.concat([df_old, temp, df_old]).drop_duplicates(subset='numbr', keep=False)
    return pd.concat([df_old, temp])


def merge_new_codes(old: ErrorCodes, new: ErrorCodes) -> ErrorCodes:
    return ErrorCodes(
        common=old.common, # the common range never gets modified
        mysql=_merge_new_codes_single(old.common, old.mysql, new.mysql),
        mariadb=_merge_new_codes_single(old.common, old.mariadb, new.mariadb),
    )


# Actually perform the generation
def write_headers(codes: ErrorCodes) -> None:
    def header_path(p: List[str]) -> Path:
        return REPO_BASE.joinpath('include', 'boost', 'mysql', *p)

    # common_server_errc.hpp
    with open(header_path(['common_server_errc.hpp']), 'wt') as f:
        f.write(render_common_server_errc(codes.common))
    
    # mysql_server_errc.hpp
    with open(header_path(['mysql_server_errc.hpp']), 'wt') as f:
        f.write(render_server_specific_errc('mysql', codes.mysql))

    # mariadb_server_errc.hpp
    with open(header_path(['mariadb_server_errc.hpp']), 'wt') as f:
        f.write(render_server_specific_errc('mariadb', codes.mariadb))
    
    # detail/auxiliar/server_errc_strings.hpp
    with open(header_path(['impl', 'internal', 'error', 'server_error_to_string.ipp']), 'wt') as f:
        f.write(render_server_error_to_string(codes.common, codes.mysql, codes.mariadb))


# We need to run file_headers.py to set copyrights and headers
def invoke_file_headers() -> None:
    run(['python', str(REPO_BASE.joinpath('tools', 'scripts', 'file_headers.py'))])


def main():
    old_codes = load_csv()
    new_codes = parse_headers(REPO_BASE.joinpath('private', 'errors', '1.86'))
    codes = merge_new_codes(old_codes, new_codes)
    write_csv(codes)
    write_headers(codes)
    invoke_file_headers()

            
if __name__ == '__main__':
    main()