Files
superset2/superset/versioning/db_errors.py
T

70 lines
3.1 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Classify database errors the versioning capture path swallows.
The capture listeners fail open — a versioning bug must never break a
user's save — and one failure is genuinely benign: the versioning tables
not existing yet, during the window between deploying the code and
running its migration. The listeners swallow that case silently.
But the exception classes that case raises (``OperationalError`` on
SQLite/MySQL, ``ProgrammingError`` on PostgreSQL) also cover deadlocks,
lock timeouts, and dropped connections. Swallowing the whole class turns
any of those into a silent capture drop: the user's save succeeds and its
version history quietly doesn't exist. :func:`is_missing_table_error`
separates the benign case from everything else, so the listeners can stay
quiet for the migration race and log-plus-count the rest.
"""
from __future__ import annotations
from sqlalchemy.exc import DBAPIError
#: PostgreSQL SQLSTATE for "relation does not exist" (undefined_table).
_PG_UNDEFINED_TABLE = "42P01"
#: MySQL/MariaDB error code for "Table ... doesn't exist".
_MYSQL_NO_SUCH_TABLE = 1146
def is_missing_table_error(exc: DBAPIError) -> bool:
"""Whether *exc* is a "table does not exist" error on any supported
metadata database, as raised during the pre-migration startup race.
Checks driver error codes first (psycopg2's ``pgcode``, MySQL's
numeric errno) and falls back to SQLite's message, which is the only
signal that driver provides. Deliberately narrow: an ambiguous error
should be logged by the caller, not swallowed — over-reporting is
recoverable, a silent capture drop is not.
"""
orig = getattr(exc, "orig", None)
# PostgreSQL via psycopg2. (psycopg 3 spells it ``sqlstate``; checked
# too so a driver swap doesn't silently widen the swallow.)
pgcode = getattr(orig, "pgcode", None) or getattr(orig, "sqlstate", None)
if pgcode is not None:
return pgcode == _PG_UNDEFINED_TABLE
# MySQL/MariaDB drivers put the numeric errno first in args.
args = getattr(orig, "args", ())
if args and isinstance(args[0], int):
return args[0] == _MYSQL_NO_SUCH_TABLE
# SQLite has no error codes on OperationalError; the message is the
# documented, stable signal.
return "no such table" in str(orig if orig is not None else exc).lower()