Skip to content

Conversation

@dganesh05
Copy link

feat(database): add custom database error message configuration system

Fixes #21

SUMMARY

This PR introduces a configurable system (CUSTOM_DATABASE_ERRORS) that allows Superset administrators to define custom, user-friendly error messages for specific database errors per database connection. This improves the user experience by replacing cryptic database exception messages with context-specific guidance.

Key Features:

  • Configuration System: Added CUSTOM_DATABASE_ERRORS config constant that can be defined in superset_config.py
  • Regex Pattern Matching: Supports regex patterns (both string and compiled Pattern objects) to match database errors
  • Dynamic Message Interpolation: Supports regex capture groups for dynamic message content (e.g., table names, column names)
  • Issue Code Suppression: Optional show_issue_info: False flag to hide technical issue codes and the "See more" button
  • Custom Documentation Links: Support for custom_doc_links to provide context-specific help resources
  • Universal Coverage: Works across all database error contexts (SQL Lab, chart execution, database validation, connection testing)

Implementation Details:

  • Modified BaseEngineSpec.extract_errors() to check CUSTOM_DATABASE_ERRORS config before falling back to default error handling
  • Updated all error extraction call sites to pass database_name in context (SQL Lab, chart queries, database validation, connection testing)
  • Enhanced SupersetError class to respect show_issue_info flag for conditional issue code inclusion
  • Updated DatabaseErrorMessage frontend component to conditionally display issue codes and custom doc links
  • Added comprehensive unit tests covering all functionality

Design Decisions:

  • Custom errors are checked first, before engine spec's built-in custom_errors, giving administrators full control
  • Gracefully handles cases where Flask current_app is not available (e.g., tests, CLI)
  • Backward compatible: defaults to showing issue codes if show_issue_info flag is not present
  • Supports per-database configuration, allowing different error messages for different database connections

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

issue_before ![issue_after](https://github.com/user-attachments/assets/3db00db3-bf60-4134-b828-87129c1b1f5e)

TESTING INSTRUCTIONS

Manual Testing:

  1. Configure Custom Errors:

    • Add the following to your superset_config.py:
    import re
    from flask_babel import gettext as __
    from superset.errors import SupersetErrorType
    
    CUSTOM_DATABASE_ERRORS = {
        "examples": {
            re.compile(r"no such table: (?P<table_name>.+)"): (
                __("The table '%(table_name)s' does not exist. Please check the table name and try again."),
                SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
                {
                    "custom_doc_links": [
                        {
                            "url": "https://example.com/docs/tables",
                            "label": "View available tables"
                        }
                    ],
                    "show_issue_info": False,
                }
            )
        }
    }
  2. Restart Superset to load the new configuration

  3. Test in SQL Lab:

    • Navigate to SQL Lab in Superset
    • Connect to the "examples" database
    • Execute a query that will fail: SELECT * FROM non_existing_table
    • Verify the custom error message appears instead of the raw database error
    • Verify the "See more" button with issue codes is NOT displayed (due to show_issue_info: False)
    • Verify the custom doc link is displayed and clickable
  4. Test with show_issue_info: True:

    • Update the config to set "show_issue_info": True
    • Restart Superset
    • Execute the same failing query
    • Verify the "See more" button appears with issue codes
  5. Test Regex Capture Groups:

    • The example above uses (?P<table_name>.+) to capture the table name
    • Verify that the table name is correctly interpolated into the error message
  6. Test in Other Contexts:

    • Test with chart queries that fail
    • Test with database connection validation errors
    • Verify custom errors work across all contexts

Automated Testing:

Run the unit tests:

docker compose exec superset pytest tests/unit_tests/db_engine_specs/test_custom_errors.py -v

The test suite covers:

  • Custom error pattern matching
  • Regex capture groups for dynamic messages
  • show_issue_info flag behavior (False, True, and default)
  • custom_doc_links inclusion
  • Fallback behavior when patterns don't match
  • Multiple database configurations
  • Empty config handling

ADDITIONAL INFORMATION

- Added support for custom error messages based on database connection configurations.
- Introduced `CUSTOM_DATABASE_ERRORS` in the configuration to allow administrators to define specific error messages for different database errors.
- Enhanced error extraction methods to utilize custom error patterns and provide detailed feedback to users.
- Updated frontend components to conditionally display error information based on the new configuration.
- Added unit tests to ensure the functionality of custom error handling and message formatting.
Copilot AI review requested due to automatic review settings December 8, 2025 01:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a comprehensive custom database error message configuration system that allows Superset administrators to replace cryptic database errors with user-friendly, context-specific messages on a per-database basis.

Key Changes:

  • Implements CUSTOM_DATABASE_ERRORS configuration system with regex pattern matching and message interpolation
  • Adds show_issue_info flag to conditionally hide technical issue codes from error messages
  • Supports custom documentation links in error messages
  • Updates all database error extraction call sites to pass database_name in context

Reviewed changes

Copilot reviewed 11 out of 14 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tests/unit_tests/db_engine_specs/test_custom_errors.py Comprehensive unit tests covering custom error pattern matching, regex capture groups, show_issue_info flag, custom doc links, and fallback behavior
superset/db_engine_specs/base.py Core implementation of custom error extraction logic that checks CUSTOM_DATABASE_ERRORS config before falling back to engine spec's built-in errors
superset/errors.py Modified SupersetError.post_init to respect show_issue_info flag for conditional issue code suppression
superset/config.py Added CUSTOM_DATABASE_ERRORS configuration constant with documentation
superset/sql_lab.py Updated error extraction to pass database_name in context
superset/models/helpers.py Updated error extraction to pass database_name in context
superset/connectors/sqla/models.py Updated error extraction to pass database_name in context
superset/commands/database/validate.py Added database_name to context for validation errors
superset/commands/database/test_connection.py Added database_name to context for connection test errors
superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx Enhanced UI to conditionally display issue codes and custom doc links based on show_issue_info flag
docker/pythonpath_dev/superset_config.py Example configuration demonstrating CUSTOM_DATABASE_ERRORS usage
superset-frontend/package-lock.json Incidental dependency change (peerDependencies for eslint-plugin-icons)
.gitignore Added developer-specific IDE and environment directories
Files not reviewed (1)
  • superset-frontend/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +60
// When show_issue_info is false, hide the "See more" button entirely
// This must be false when show_issue_info is explicitly False, regardless of other content
// If show_issue_info is False, hasDescriptionDetails must be false to hide "See more"
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Redundant comments repeating the same concept. Lines 58-60 all state that when show_issue_info is false, certain behavior should occur. Consider consolidating these into a single, clear comment.

Suggested change
// When show_issue_info is false, hide the "See more" button entirely
// This must be false when show_issue_info is explicitly False, regardless of other content
// If show_issue_info is False, hasDescriptionDetails must be false to hide "See more"
/**
* Only show the "See more" button and additional details if show_issue_info is not explicitly false.
* If show_issue_info is false, hasDescriptionDetails will be false and all extra details are hidden.
*/

Copilot uses AI. Check for mistakes.
return [
SupersetError(
error_type=error_type,
message=message % params,
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential security risk: Using % string formatting with user-controlled regex capture groups (line 1397) could be vulnerable to format string injection if the custom error message template contains format specifiers that aren't matched by the regex groups. Consider using .format() with named placeholders or validating that all format specifiers in the message template are present in the params dict to prevent potential errors or security issues.

Copilot uses AI. Check for mistakes.
Comment on lines +324 to +327
"license": "Apache-2.0",
"peerDependencies": {
"eslint": ">=0.8.0"
}
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This change to package-lock.json appears unrelated to the custom database error message feature. The addition of peerDependencies for eslint-plugin-icons seems like an incidental change that may have occurred during dependency resolution. Consider whether this should be part of a separate PR focused on dependency management.

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +133
hasCustomDocLinks &&
extra.custom_doc_links && (
<>
{alertDescription && <br />}
<br />
{t('For more information, see:')}
<br />
{extra.custom_doc_links.map((link, index) => (
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for displaying custom doc links when show_issue_info is false has a potential issue. Line 127 checks extra.custom_doc_links for truthiness after already checking hasCustomDocLinks on line 126, which already verifies that extra.custom_doc_links exists and has length > 0. The second check on line 127 is redundant. The expression should simply be: !showIssueInfo && hasCustomDocLinks && (...)

Suggested change
hasCustomDocLinks &&
extra.custom_doc_links && (
<>
{alertDescription && <br />}
<br />
{t('For more information, see:')}
<br />
{extra.custom_doc_links.map((link, index) => (
hasCustomDocLinks && (
<>
{alertDescription && <br />}
<br />
{t('For more information, see:')}
<br />
{extra.custom_doc_links!.map((link, index) => (

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +144
# Match PostgreSQL "relation does not exist" errors with capture group
# This pattern captures the table name from the error message
re.compile(r'relation "(?P<table_name>.+)" does not exist', re.IGNORECASE): (
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern on line 144 uses a raw string with escaped quotes (r'relation "(?P<table_name>.+)" does not exist'), but the comment on line 142 states this is for PostgreSQL errors. However, the "examples" database in Superset typically uses SQLite, not PostgreSQL. SQLite's error message for missing tables is "no such table: table_name" not "relation does not exist". This example configuration may not work as intended with the default examples database. Consider using a pattern that matches SQLite errors, or clarifying that this example assumes PostgreSQL is configured as the examples database.

Suggested change
# Match PostgreSQL "relation does not exist" errors with capture group
# This pattern captures the table name from the error message
re.compile(r'relation "(?P<table_name>.+)" does not exist', re.IGNORECASE): (
# Match SQLite "no such table" errors with capture group
# This pattern captures the table name from the error message
re.compile(r'no such table: (?P<table_name>\w+)', re.IGNORECASE): (

Copilot uses AI. Check for mistakes.
"label": "View available tables"
}
],
"show_issue_info": True,
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The example configuration uses show_issue_info: True, but according to the PR description's testing instructions, one of the key features is to test with show_issue_info: False to hide issue codes. For a more illustrative example that demonstrates the feature's primary use case (suppressing issue codes), consider setting this to False by default in the example.

Suggested change
"show_issue_info": True,
"show_issue_info": False,

Copilot uses AI. Check for mistakes.
Comment on lines +50 to 168
// When show_issue_info is explicitly False, never show "See more" button
// Check if show_issue_info is explicitly set to False
const showIssueInfo = extra?.show_issue_info !== false;
const hasIssueCodes =
showIssueInfo && extra?.issue_codes && extra.issue_codes.length > 0;
const hasOwners = isVisualization && extra?.owners && extra.owners.length > 0;
const hasCustomDocLinks =
extra?.custom_doc_links && extra.custom_doc_links.length > 0;
// When show_issue_info is false, hide the "See more" button entirely
// This must be false when show_issue_info is explicitly False, regardless of other content
// If show_issue_info is False, hasDescriptionDetails must be false to hide "See more"
const hasDescriptionDetails =
showIssueInfo && (hasIssueCodes || hasOwners || hasCustomDocLinks);

const body = extra && hasDescriptionDetails && (
<>
<p>
{t('This may be triggered by:')}
<br />
{extra.issue_codes
?.map<ReactNode>(issueCode => (
<IssueCode {...issueCode} key={issueCode.code} />
))
.reduce((prev, curr) => [prev, <br />, curr])}
</p>
{isVisualization && extra.owners && (
<>
{hasIssueCodes && (
<p>
{t('This may be triggered by:')}
<br />
{extra.issue_codes!
.map<ReactNode>(issueCode => (
<IssueCode {...issueCode} key={issueCode.code} />
))
.reduce((prev, curr) => [prev, <br />, curr])}
</p>
)}
{hasOwners && (
<>
{hasIssueCodes && <br />}
<p>
{tn(
'Please reach out to the Chart Owner for assistance.',
'Please reach out to the Chart Owners for assistance.',
extra.owners.length,
extra.owners!.length,
)}
</p>
<p>
{tn(
'Chart Owner: %s',
'Chart Owners: %s',
extra.owners.length,
extra.owners.join(', '),
extra.owners!.length,
extra.owners!.join(', '),
)}
</p>
</>
)}
{hasCustomDocLinks && (
<>
{(hasIssueCodes || hasOwners) && <br />}
<p>
{t('For more information, see:')}
<br />
{extra.custom_doc_links!.map((link, index) => (
<span key={link.url}>
{index > 0 && <br />}
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
{link.label}
</a>
</span>
))}
</p>
</>
)}
</>
);

// When show_issue_info is false, show custom doc links inline in description
// instead of in the collapsible "See more" section
const inlineCustomDocLinks =
!showIssueInfo &&
hasCustomDocLinks &&
extra.custom_doc_links && (
<>
{alertDescription && <br />}
<br />
{t('For more information, see:')}
<br />
{extra.custom_doc_links.map((link, index) => (
<span key={link.url}>
{index > 0 && <br />}
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
{link.label}
</a>
</span>
))}
</>
);

const finalDescription =
alertDescription || inlineCustomDocLinks
? (
<>
{alertDescription}
{inlineCustomDocLinks}
</>
)
: null;

return (
<ErrorAlert
errorType={t('%s Error', extra?.engine_name || t('DB engine'))}
message={alertMessage}
description={alertDescription}
description={finalDescription}
type={level}
descriptionDetails={body}
descriptionDetails={hasDescriptionDetails && body ? body : undefined}
/>
);
}
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontend changes to DatabaseErrorMessage.tsx introduce new behavior for show_issue_info and custom_doc_links, but the existing test file (DatabaseErrorMessage.test.tsx) does not appear to have been updated to test these new features. Tests should be added to verify:

  1. When show_issue_info is false, the "See more" button is not rendered
  2. When show_issue_info is false and custom_doc_links are present, the links are displayed inline in the description
  3. When show_issue_info is true (or not set) and custom_doc_links are present, the links appear in the collapsible section
  4. The structure and rendering of custom_doc_links

Copilot uses AI. Check for mistakes.
return [
SupersetError(
error_type=error_type,
message=message % params,
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using message % params (line 1397) could raise a KeyError or ValueError if the message template contains format specifiers (e.g., %(some_key)s) that aren't present in the params dictionary. While this is a configuration error, it would be better to catch this exception and log a helpful error message that identifies which custom error pattern caused the issue, rather than letting the exception propagate and break error handling entirely. Consider wrapping this in a try-except block.

Copilot uses AI. Check for mistakes.
import re
from unittest.mock import MagicMock

import pytest
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
from pytest_mock import MockerFixture

from superset.db_engine_specs.base import BaseEngineSpec
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'SupersetError' is not used.

Suggested change
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.errors import ErrorLevel, SupersetErrorType

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue #8 - Add configurable percentage metric calculation mode for Table charts

1 participant