Before:
ProgrammingError: server returned SQ_ERR sqlcode=-201 isamcode=0
After:
ProgrammingError: [-201] A syntax error has occurred (offset 1)
ProgrammingError: [-206] The specified table is not in the database
(near 'no_such_table') [ISAM -111] (offset 27)
IntegrityError: [-268] Cannot insert duplicate value -
violates UNIQUE constraint (near 'on table u')
[ISAM -100] (offset 23)
IntegrityError: [-391] Cannot insert NULL value into a NOT NULL column
(near 't.id') (offset 23)
OperationalError: [-255] Not in transaction
PEP 249 exception classes mapped from sqlcode:
-201, -206, -217, -286, -310, ... → ProgrammingError
-239, -268, -291, -292, -391, -703 → IntegrityError
-255, -256, -407, -440, -908, ... → OperationalError
-329, -349, -510 → NotSupportedError
others → DatabaseError (safe fallback)
SQ_ERR wire decode (per IfxSqli.receiveError line 2717):
[short sqlcode][short isamcode][int offset]
[short near_token_len][bytes name][optional pad][short SQ_EOT]
The "near" token is the object name where the error occurred (table or
column name for "not found" errors); empty for most syntax errors.
Structured fields attached to every Informix error for programmatic
inspection:
e.sqlcode — Informix error code (e.g. -206)
e.isamcode — ISAM/RSAM-level error (e.g. -111 = "table not found")
e.offset — character offset in the SQL where the error occurred
e.near — object name in the "near 'XYZ'" clause (or "")
Connection state survives errors: a failed query doesn't poison the
session — subsequent execute() calls work normally. Verified by
test_connection_survives_query_error.
Built-in error catalog of ~50 most common Informix sqlcodes shipped
in src/informix_db/_errcodes.py. Users can extend at runtime with
register_error_text(code, text). Unknown codes get a generic
"Informix error <N>" with structured fields still populated.
Module changes:
src/informix_db/_errcodes.py (new) — error catalog + exception
classification + register_error_text()
src/informix_db/cursors.py — _raise_sq_err now uses the catalog
src/informix_db/connections.py — same upgrade for the connection-side
SQ_ERR path (catches commit/rollback errors etc.)
Tests: 40 unit + 63 integration (8 new error tests) = 103 total, all
green, ruff clean. Tests cover:
- syntax error → ProgrammingError(-201)
- table not found → ProgrammingError(-206) with near='no_such_table'
- column not found → ProgrammingError(-217)
- UNIQUE violation → IntegrityError(-268)
- NOT NULL violation → IntegrityError(-391)
- commit on unlogged DB → OperationalError(-255)
- connection survives errors (subsequent queries work)
- all errors carry structured sqlcode/isamcode/offset/near attrs
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""Phase 5.a integration tests — error message decoding + PEP 249 classification.
|
|
|
|
Server SQ_ERR responses get decoded to:
|
|
- the appropriate PEP 249 exception class (Programming/Integrity/...)
|
|
- a human-readable message with sqlcode, near-token, ISAM code, offset
|
|
- structured ``sqlcode``, ``isamcode``, ``offset``, ``near`` attributes
|
|
|
|
The connection survives errors — subsequent queries work normally.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import informix_db
|
|
from tests.conftest import ConnParams
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def _connect(conn_params: ConnParams) -> informix_db.Connection:
|
|
return informix_db.connect(
|
|
host=conn_params.host,
|
|
port=conn_params.port,
|
|
user=conn_params.user,
|
|
password=conn_params.password,
|
|
database=conn_params.database,
|
|
server=conn_params.server,
|
|
connect_timeout=10.0,
|
|
read_timeout=10.0,
|
|
)
|
|
|
|
|
|
def test_syntax_error_is_programming_error(conn_params: ConnParams) -> None:
|
|
"""sqlcode -201 (syntax error) → ProgrammingError."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
with pytest.raises(informix_db.ProgrammingError) as excinfo:
|
|
cur.execute("SELEKT * FROM systables")
|
|
e = excinfo.value
|
|
assert e.sqlcode == -201
|
|
assert "syntax error" in str(e).lower()
|
|
|
|
|
|
def test_table_not_found_is_programming_error(conn_params: ConnParams) -> None:
|
|
"""sqlcode -206 (table not found) → ProgrammingError; near-token has table name."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
with pytest.raises(informix_db.ProgrammingError) as excinfo:
|
|
cur.execute("SELECT * FROM no_such_table_xyz")
|
|
e = excinfo.value
|
|
assert e.sqlcode == -206
|
|
assert e.near == "no_such_table_xyz"
|
|
assert "not in the database" in str(e)
|
|
|
|
|
|
def test_column_not_found_is_programming_error(conn_params: ConnParams) -> None:
|
|
"""sqlcode -217 (column not found) → ProgrammingError; near-token has column name."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
with pytest.raises(informix_db.ProgrammingError) as excinfo:
|
|
cur.execute("SELECT no_such_column FROM systables")
|
|
e = excinfo.value
|
|
assert e.sqlcode == -217
|
|
assert e.near == "no_such_column"
|
|
|
|
|
|
def test_unique_violation_is_integrity_error(conn_params: ConnParams) -> None:
|
|
"""sqlcode -268 (UNIQUE violation) → IntegrityError."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_uniq (id INTEGER PRIMARY KEY)")
|
|
cur.execute("INSERT INTO t_uniq VALUES (?)", (1,))
|
|
with pytest.raises(informix_db.IntegrityError) as excinfo:
|
|
cur.execute("INSERT INTO t_uniq VALUES (?)", (1,))
|
|
e = excinfo.value
|
|
assert e.sqlcode == -268
|
|
assert "duplicate" in str(e).lower()
|
|
|
|
|
|
def test_not_null_violation_is_integrity_error(conn_params: ConnParams) -> None:
|
|
"""sqlcode -391 (NOT NULL violation) → IntegrityError."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_nn (id INTEGER NOT NULL)")
|
|
with pytest.raises(informix_db.IntegrityError) as excinfo:
|
|
cur.execute("INSERT INTO t_nn VALUES (?)", (None,))
|
|
assert excinfo.value.sqlcode == -391
|
|
|
|
|
|
def test_commit_in_unlogged_db_is_operational_error(conn_params: ConnParams) -> None:
|
|
"""sqlcode -255 (no transaction) → OperationalError."""
|
|
with _connect(conn_params) as conn:
|
|
with pytest.raises(informix_db.OperationalError) as excinfo:
|
|
conn.commit()
|
|
assert excinfo.value.sqlcode == -255
|
|
|
|
|
|
def test_connection_survives_query_error(conn_params: ConnParams) -> None:
|
|
"""Critical: a failed query must not poison the connection — subsequent queries must work."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
# First query fails
|
|
with pytest.raises(informix_db.ProgrammingError):
|
|
cur.execute("SELECT * FROM no_such_table")
|
|
# Second query works
|
|
cur.execute("SELECT 1 FROM systables WHERE tabid = 1")
|
|
assert cur.fetchone() == (1,)
|
|
|
|
|
|
def test_error_has_structured_fields(conn_params: ConnParams) -> None:
|
|
"""``sqlcode``, ``isamcode``, ``offset``, ``near`` are attached for inspection."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
with pytest.raises(informix_db.Error) as excinfo:
|
|
cur.execute("SELECT * FROM no_such_table")
|
|
e = excinfo.value
|
|
# All four structured attributes present
|
|
assert hasattr(e, "sqlcode")
|
|
assert hasattr(e, "isamcode")
|
|
assert hasattr(e, "offset")
|
|
assert hasattr(e, "near")
|
|
assert e.sqlcode == -206
|
|
assert e.near == "no_such_table"
|