diff options
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/make_errorcodes.py | 62 | ||||
-rwxr-xr-x | scripts/make_errors.py | 210 |
2 files changed, 219 insertions, 53 deletions
diff --git a/scripts/make_errorcodes.py b/scripts/make_errorcodes.py index 1b3f594..3c72a2c 100755 --- a/scripts/make_errorcodes.py +++ b/scripts/make_errorcodes.py @@ -22,8 +22,6 @@ import sys import urllib2 from collections import defaultdict -from BeautifulSoup import BeautifulSoup as BS - def main(): if len(sys.argv) != 2: @@ -35,8 +33,7 @@ def main(): file_start = read_base_file(filename) # If you add a version to the list fix the docs (in errorcodes.rst) classes, errors = fetch_errors( - ['8.1', '8.2', '8.3', '8.4', '9.0', '9.1', '9.2', '9.3', '9.4', '9.5', - '9.6', '10', '11']) + ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11']) f = open(filename, "w") for line in file_start: @@ -90,48 +87,6 @@ def parse_errors_txt(url): return classes, errors -def parse_errors_sgml(url): - page = BS(urllib2.urlopen(url)) - table = page('table')[1]('tbody')[0] - - classes = {} - errors = defaultdict(dict) - - for tr in table('tr'): - if tr.td.get('colspan'): # it's a class - label = ' '.join(' '.join(tr(text=True)).split()) \ - .replace(u'\u2014', '-').encode('ascii') - assert label.startswith('Class') - class_ = label.split()[1] - assert len(class_) == 2 - classes[class_] = label - - else: # it's an error - errcode = tr.tt.string.encode("ascii") - assert len(errcode) == 5 - - tds = tr('td') - if len(tds) == 3: - errlabel = '_'.join(tds[1].string.split()).encode('ascii') - - # double check the columns are equal - cond_name = tds[2].string.strip().upper().encode("ascii") - assert errlabel == cond_name, tr - - elif len(tds) == 2: - # found in PG 9.1 docs - errlabel = tds[1].tt.string.upper().encode("ascii") - - else: - assert False, tr - - errors[class_][errcode] = errlabel - - return classes, errors - -errors_sgml_url = \ - "https://www.postgresql.org/docs/%s/static/errcodes-appendix.html" - errors_txt_url = \ "http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob_plain;" \ "f=src/backend/utils/errcodes.txt;hb=%s" @@ -144,15 +99,16 @@ def fetch_errors(versions): for version in versions: print(version, file=sys.stderr) tver = tuple(map(int, version.split()[0].split('.'))) - if tver < (9, 1): - c1, e1 = parse_errors_sgml(errors_sgml_url % version) - else: - tag = '%s%s_STABLE' % ( - (tver[0] >= 10 and 'REL_' or 'REL'), - version.replace('.', '_')) - c1, e1 = parse_errors_txt(errors_txt_url % tag) + tag = '%s%s_STABLE' % ( + (tver[0] >= 10 and 'REL_' or 'REL'), + version.replace('.', '_')) + c1, e1 = parse_errors_txt(errors_txt_url % tag) classes.update(c1) + # This error was in old server versions but probably never used + # https://github.com/postgres/postgres/commit/12f87b2c82 + errors['22']['22020'] = 'INVALID_LIMIT_VALUE' + # TODO: this error was added in PG 10 beta 1 but dropped in the # final release. It doesn't harm leaving it in the file. Check if it # will be added back in PG 12. diff --git a/scripts/make_errors.py b/scripts/make_errors.py new file mode 100755 index 0000000..cdf299f --- /dev/null +++ b/scripts/make_errors.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +"""Generate the errors module from PostgreSQL source code. + +The script can be run at a new PostgreSQL release to refresh the module. +""" + +# Copyright (C) 2018 Daniele Varrazzo <daniele.varrazzo@gmail.com> +# +# psycopg2 is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# psycopg2 is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +from __future__ import print_function + +import re +import sys +import urllib2 +from collections import defaultdict + + +def main(): + if len(sys.argv) != 2: + print("usage: %s /path/to/errors.py" % sys.argv[0], file=sys.stderr) + return 2 + + filename = sys.argv[1] + + file_start = read_base_file(filename) + # If you add a version to the list fix the docs (in errors.rst) + classes, errors = fetch_errors( + ['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11']) + + f = open(filename, "w") + for line in file_start: + print(line, file=f) + for line in generate_module_data(classes, errors): + print(line, file=f) + + +def read_base_file(filename): + rv = [] + for line in open(filename): + rv.append(line.rstrip("\n")) + if line.startswith("# autogenerated"): + return rv + + raise ValueError("can't find the separator. Is this the right file?") + + +def parse_errors_txt(url): + classes = {} + errors = defaultdict(dict) + + page = urllib2.urlopen(url) + for line in page: + # Strip comments and skip blanks + line = line.split('#')[0].strip() + if not line: + continue + + # Parse a section + m = re.match(r"Section: (Class (..) - .+)", line) + if m: + label, class_ = m.groups() + classes[class_] = label + continue + + # Parse an error + m = re.match(r"(.....)\s+(?:E|W|S)\s+ERRCODE_(\S+)(?:\s+(\S+))?$", line) + if m: + errcode, macro, spec = m.groups() + # skip errcodes without specs as they are not publically visible + if not spec: + continue + errlabel = spec.upper() + errors[class_][errcode] = errlabel + continue + + # We don't expect anything else + raise ValueError("unexpected line:\n%s" % line) + + return classes, errors + + +errors_txt_url = \ + "http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob_plain;" \ + "f=src/backend/utils/errcodes.txt;hb=%s" + + +def fetch_errors(versions): + classes = {} + errors = defaultdict(dict) + + for version in versions: + print(version, file=sys.stderr) + tver = tuple(map(int, version.split()[0].split('.'))) + tag = '%s%s_STABLE' % ( + (tver[0] >= 10 and 'REL_' or 'REL'), + version.replace('.', '_')) + c1, e1 = parse_errors_txt(errors_txt_url % tag) + classes.update(c1) + + for c, cerrs in e1.items(): + errors[c].update(cerrs) + + return classes, errors + + +def generate_module_data(classes, errors): + tmpl = """ + +class %(cls)s(%(base)s): + pass + +_by_sqlstate[%(errcode)r] = %(cls)s\ +""" + for clscode, clslabel in sorted(classes.items()): + if clscode in ('00', '01'): + # success and warning - never raised + continue + + yield "\n\n# %s" % clslabel + + for errcode, errlabel in sorted(errors[clscode].items()): + clsname = errlabel.title().replace('_', '') + yield tmpl % { + 'cls': clsname, + 'base': get_base_class_name(errcode), + 'errcode': errcode + } + + +def get_base_class_name(errcode): + """ + This is a python porting of exception_from_sqlstate code in pqpath.c + """ + if errcode[0] == '0': + if errcode[1] == 'A': # Class 0A - Feature Not Supported + return 'NotSupportedError' + elif errcode[0] == '2': + if errcode[1] in '01': + # Class 20 - Case Not Found + # Class 21 - Cardinality Violation + return 'ProgrammingError' + elif errcode[1] == '2': # Class 22 - Data Exception + return 'DataError' + elif errcode[1] == '3': # Class 23 - Integrity Constraint Violation + return 'IntegrityError' + elif errcode[1] in '45': + # Class 24 - Invalid Cursor State + # Class 25 - Invalid Transaction State + return 'InternalError' + elif errcode[1] in '678': + # Class 26 - Invalid SQL Statement Name + # Class 27 - Triggered Data Change Violation + # Class 28 - Invalid Authorization Specification + return 'OperationalError' + elif errcode[1] in 'BDF': + # Class 2B - Dependent Privilege Descriptors Still Exist + # Class 2D - Invalid Transaction Termination + # Class 2F - SQL Routine Exception + return 'InternalError' + elif errcode[0] == '3': + if errcode[1] == '4': # Class 34 - Invalid Cursor Name + return 'OperationalError' + if errcode[1] in '89B': + # Class 38 - External Routine Exception + # Class 39 - External Routine Invocation Exception + # Class 3B - Savepoint Exception + return 'InternalError' + if errcode[1] in 'DF': + # Class 3D - Invalid Catalog Name + # Class 3F - Invalid Schema Name + return 'ProgrammingError' + elif errcode[0] == '4': + if errcode[1] == '0': # Class 40 - Transaction Rollback + return 'TransactionRollbackError' + if errcode[1] in '24': + # Class 42 - Syntax Error or Access Rule Violation + # Class 44 - WITH CHECK OPTION Violation + return 'ProgrammingError' + elif errcode[0] == '5': + if errcode == "57014": + return 'QueryCanceledError' + # Class 53 - Insufficient Resources + # Class 54 - Program Limit Exceeded + # Class 55 - Object Not In Prerequisite State + # Class 57 - Operator Intervention + # Class 58 - System Error (errors external to PostgreSQL itself) + else: + return 'OperationalError' + elif errcode[0] == 'F': # Class F0 - Configuration File Error + return 'InternalError' + elif errcode[0] == 'H': # Class HV - Foreign Data Wrapper Error (SQL/MED) + return 'OperationalError' + elif errcode[0] == 'P': # Class P0 - PL/pgSQL Error + return 'InternalError' + elif errcode[0] == 'X': # Class XX - Internal Error + return 'InternalError' + + return 'DatabaseError' + + +if __name__ == '__main__': + sys.exit(main()) |