mirror of
https://github.com/apache/superset.git
synced 2026-09-01 21:11:28 +00:00
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
70 lines
3.1 KiB
Python
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()
|