#!/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 # # 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())