"""Generate the errorcodes module starting from PostgreSQL documentation.
The script can be run at a new PostgreSQL release to refresh the module.
"""
import re
import sys
from urllib.request import urlopen
from collections import defaultdict
def main():
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} /path/to/errorcodes.py", file=sys.stderr)
return 2
filename = sys.argv[1]
file_start = read_base_file(filename)
classes, errors = fetch_errors(
['9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '10', '11', '12', '13'])
disambiguate(errors)
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 = urlopen(url)
for line in page:
line = line.decode("ascii").split('#')[0].strip()
if not line:
continue
m = re.match(r"Section: (Class (..) - .+)", line)
if m:
label, class_ = m.groups()
classes[class_] = label
continue
m = re.match(r"(.....)\s+(?:E|W|S)\s+ERRCODE_(\S+)(?:\s+(\S+))?$", line)
if m:
errcode, macro, spec = m.groups()
if not spec:
continue
errlabel = spec.upper()
errors[class_][errcode] = errlabel
continue
raise ValueError(f"unexpected line:\n{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 = f"{tver[0] >= 10 and 'REL_' or 'REL'}{version.replace('.', '_')}_STABLE"
c1, e1 = parse_errors_txt(errors_txt_url % tag)
classes.update(c1)
errors['22']['22020'] = 'INVALID_LIMIT_VALUE'
for c, cerrs in e1.items():
errors[c].update(cerrs)
return classes, errors
def disambiguate(errors):
"""
Change name for exception defined more than once.
Change the first occurrence, because before introdcing the function
they were pretty much lost (see ticket #1133)
"""
for code in "01004 22004 2F002 2F003 2F004".split():
errors[code[:2]][code] += "_"
def generate_module_data(classes, errors):
yield ""
yield "# Error classes"
for clscode, clslabel in sorted(classes.items()):
err = clslabel.split(" - ")[1].split("(")[0] \
.strip().replace(" ", "_").replace('/', "_").upper()
yield f"CLASS_{err} = {clscode!r}"
seen = set()
for clscode, clslabel in sorted(classes.items()):
yield ""
yield f"# {clslabel}"
for errcode, errlabel in sorted(errors[clscode].items()):
if errlabel in seen:
raise Exception(f"error label already seen: {errlabel}")
seen.add(errlabel)
yield f"{errlabel} = {errcode!r}"
if __name__ == '__main__':
sys.exit(main())