summaryrefslogtreecommitdiff
path: root/scripts/make_errors.py
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2018-06-17 22:39:10 +0100
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2018-08-17 02:00:40 +0100
commit782fa39647a79bc0269917cd9f539c793c13774a (patch)
tree2e45929ddf80db4af1b8843695b09b73d5aa3317 /scripts/make_errors.py
parent0bce58d0cdadb6756bb6f999cbae4f47feb49b95 (diff)
Generating the whole errors file from script
Diffstat (limited to 'scripts/make_errors.py')
-rwxr-xr-xscripts/make_errors.py210
1 files changed, 210 insertions, 0 deletions
diff --git a/scripts/make_errors.py b/scripts/make_errors.py
new file mode 100755
index 00000000..855ca35c
--- /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'])
+
+ 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())