Source code for postgast.errors
"""Error handling for postgast.
Provides the public PgQueryError exception and an internal helper to raise it from C result structs returned by
libpg_query.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ctypes import Structure
[docs]
class PgQueryError(Exception):
"""Structured error raised when libpg_query rejects a SQL statement.
Every postgast function that calls into libpg_query (:func:`~postgast.parse`, :func:`~postgast.deparse`,
:func:`~postgast.normalize`, :func:`~postgast.fingerprint`, :func:`~postgast.split`, :func:`~postgast.scan`,
:func:`~postgast.parse_plpgsql`, and :func:`~postgast.format_sql`) may raise this exception. The error carries the
same structured fields that the C library provides, so callers can build precise diagnostics (e.g., underlining the
offending token) without parsing the message string.
``cursorpos`` is a **1-based byte offset** into the original SQL string pointing to the token where the error was
detected. When it is ``0`` the position is unknown. Because it counts *bytes*, ``e.cursorpos - 1`` only equals the
corresponding Python string index when the SQL is pure ASCII. For SQL containing multibyte UTF-8 characters
(e.g., Unicode identifiers or string literals), index into the UTF-8-encoded ``bytes`` representation instead::
pos = sql.encode("utf-8")[: e.cursorpos - 1].decode("utf-8")
char_offset = len(pos)
The ``funcname``, ``filename``, and ``lineno`` fields refer to the *internal C source* of
libpg_query / PostgreSQL's parser, not to your Python code. They are mainly useful for filing upstream bug reports.
Attributes:
message: Human-readable error description from the PostgreSQL parser.
cursorpos: 1-based byte offset in the SQL string where the error was detected
(``0`` when the position is unavailable).
context: Additional context from the parser (e.g., PL/pgSQL function name), or ``None``.
funcname: Internal C function name where the error originated, or ``None``.
filename: Internal C source file where the error originated, or ``None``.
lineno: Line number in the internal C source file (``0`` when unavailable).
Examples:
Catch a syntax error and inspect the cursor position:
>>> from postgast import parse, PgQueryError
>>> try:
... parse("SELECT FROM")
... except PgQueryError as e:
... print(e.cursorpos)
12
Use ``cursorpos`` to highlight the error location (ASCII-safe shortcut):
>>> from postgast import parse, PgQueryError
>>> sql = "SELECT * FORM users"
>>> try:
... parse(sql)
... except PgQueryError as e:
... idx = max(e.cursorpos - 1, 0)
... print(sql)
... print(" " * idx + "^")
... print(e.message)
SELECT * FORM users
^
syntax error at or near "FORM"
For SQL that may contain non-ASCII characters, convert via the encoded bytes to get the correct
character offset::
sql = "SELECT '\u00fc' FORM t"
try:
parse(sql)
except PgQueryError as e:
idx = len(sql.encode("utf-8")[: e.cursorpos - 1].decode("utf-8"))
print(sql)
print(" " * idx + "^")
"""
def __init__(
self,
message: str,
*,
cursorpos: int = 0,
context: str | None = None,
funcname: str | None = None,
filename: str | None = None,
lineno: int = 0,
) -> None:
"""Create a PgQueryError.
Args:
message: Human-readable error description.
cursorpos: 1-based position in the SQL string where the error was detected.
context: Additional parser context.
funcname: Internal C function name where the error originated.
filename: Internal C source file.
lineno: Line number in the C source file.
"""
super().__init__(message)
self.message = message
self.cursorpos = cursorpos
self.context = context
self.funcname = funcname
self.filename = filename
self.lineno = lineno
def check_error(result: Structure) -> None:
"""Inspect a C result struct's error pointer and raise if set.
If the error pointer is non-null, extracts the error fields and raises ``PgQueryError``. The caller is responsible
for freeing the C result (typically via a ``finally`` block).
Args:
result: A ctypes Structure with an ``error`` field (pointer to PgQueryError C struct).
"""
err_ptr = result.error
if not err_ptr:
return
err = err_ptr.contents
message = err.message.decode("utf-8") if err.message else "unknown error"
context = err.context.decode("utf-8") if err.context else None
funcname = err.funcname.decode("utf-8") if err.funcname else None
filename = err.filename.decode("utf-8") if err.filename else None
raise PgQueryError(
message,
cursorpos=err.cursorpos,
context=context,
funcname=funcname,
filename=filename,
lineno=err.lineno,
)