diff --git a/.rat-excludes b/.rat-excludes
index 228cc2a39b0..135f96cb3ba 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -67,20 +67,8 @@ temporary_superset_ui/*
# skip license checks for auto-generated test snapshots
.*snap
-# docs overrides for third party logos we don't have the rights to
-google-big-query.svg
-google-sheets.svg
-ibm-db2.svg
-netlify.png
-postgresql.svg
-snowflake.svg
-ydb.svg
-loading.svg
-apache-solr.svg
-azure.svg
-superset.svg
-
-# docs third-party logos, i.e. docs/static/img/logos/*
+# docs third-party logos (database logos, org logos, etc.)
+databases/*
logos/*
# docs-related
diff --git a/README.md b/README.md
index d12a7d066cb..617852c1908 100644
--- a/README.md
+++ b/README.md
@@ -107,71 +107,67 @@ Here are some of the major database solutions that are supported:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/scripts/generate-database-docs.mjs b/docs/scripts/generate-database-docs.mjs
index 04980f24f83..912569294ee 100644
--- a/docs/scripts/generate-database-docs.mjs
+++ b/docs/scripts/generate-database-docs.mjs
@@ -30,9 +30,12 @@
import { spawnSync } from 'child_process';
import fs from 'fs';
+import { createRequire } from 'module';
import path from 'path';
import { fileURLToPath } from 'url';
+const require = createRequire(import.meta.url);
+
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '../..');
@@ -41,6 +44,7 @@ const DATA_OUTPUT_DIR = path.join(DOCS_DIR, 'src/data');
const DATA_OUTPUT_FILE = path.join(DATA_OUTPUT_DIR, 'databases.json');
const MDX_OUTPUT_DIR = path.join(DOCS_DIR, 'docs/databases');
const MDX_SUPPORTED_DIR = path.join(MDX_OUTPUT_DIR, 'supported');
+const IMAGES_DIR = path.join(DOCS_DIR, 'static/img/databases');
/**
* Try to run the full lib.py script with Flask context
@@ -607,30 +611,109 @@ const README_PATH = path.join(ROOT_DIR, 'README.md');
const README_START_MARKER = '';
const README_END_MARKER = '';
+/**
+ * Read image dimensions, with fallback SVG viewBox parsing for cases where
+ * image-size can't handle SVG width/height attributes (e.g., scientific notation).
+ */
+function getImageDimensions(imgPath) {
+ const sizeOf = require('image-size');
+ try {
+ const dims = sizeOf(imgPath);
+ // image-size may misparse SVG attributes (e.g. width="1e3" → 1).
+ // Fall back to viewBox parsing if a dimension looks wrong.
+ if (dims.type === 'svg' && (dims.width < 2 || dims.height < 2)) {
+ const content = fs.readFileSync(imgPath, 'utf-8');
+ const vbMatch = content.match(/viewBox=["']([^"']+)["']/);
+ if (vbMatch) {
+ const parts = vbMatch[1].trim().split(/[\s,]+/).map(Number);
+ if (parts.length >= 4 && parts[2] > 0 && parts[3] > 0) {
+ return { width: parts[2], height: parts[3] };
+ }
+ }
+ }
+ if (dims.width > 0 && dims.height > 0) {
+ return { width: dims.width, height: dims.height };
+ }
+ } catch { /* fall through */ }
+ return null;
+}
+
+/**
+ * Compute display dimensions that fit within a bounding box while preserving
+ * the image's aspect ratio. Enforces a minimum height so very wide logos
+ * remain legible.
+ */
+function fitToBoundingBox(imgWidth, imgHeight, maxWidth, maxHeight, minHeight) {
+ const ratio = imgWidth / imgHeight;
+ // Start at max height, compute width
+ let h = maxHeight;
+ let w = h * ratio;
+ // If too wide, cap width and reduce height
+ if (w > maxWidth) {
+ w = maxWidth;
+ h = w / ratio;
+ }
+ // If height fell below minimum, enforce minimum (allow width to exceed max)
+ if (h < minHeight) {
+ h = minHeight;
+ w = h * ratio;
+ }
+ return { width: Math.round(w), height: Math.round(h) };
+}
+
/**
* Generate the database logos HTML for README.md
- * Only includes databases that have logos defined
+ * Only includes databases that have logos and homepage URLs.
+ * Deduplicates by logo filename to match the docs homepage behavior.
+ * Reads actual image dimensions to preserve aspect ratios.
*/
function generateReadmeLogos(databases) {
- // Get databases with logos, sorted alphabetically
+ // Get databases with logos and homepage URLs, sorted alphabetically,
+ // deduplicated by logo filename (matches docs homepage logic in index.tsx)
+ const seenLogos = new Set();
const dbsWithLogos = Object.entries(databases)
- .filter(([, db]) => db.documentation?.logo)
- .sort(([a], [b]) => a.localeCompare(b));
+ .filter(([, db]) => db.documentation?.logo && db.documentation?.homepage_url)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .filter(([, db]) => {
+ const logo = db.documentation.logo;
+ if (seenLogos.has(logo)) return false;
+ seenLogos.add(logo);
+ return true;
+ });
if (dbsWithLogos.length === 0) {
return '';
}
- // Generate HTML img tags
+ const MAX_WIDTH = 150;
+ const MAX_HEIGHT = 40;
+ const MIN_HEIGHT = 24;
+
+ const DOCS_BASE = 'https://superset.apache.org/docs/databases/supported';
+
+ // Generate linked logo tags with aspect-ratio-preserving dimensions
const logoTags = dbsWithLogos.map(([name, db]) => {
const logo = db.documentation.logo;
- const alt = name.toLowerCase().replace(/\s+/g, '-');
- // Use docs site URL for logos
- return ` `;
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+ const imgPath = path.join(IMAGES_DIR, logo);
+
+ const dims = getImageDimensions(imgPath);
+ let sizeAttrs;
+ if (dims) {
+ const { width, height } = fitToBoundingBox(dims.width, dims.height, MAX_WIDTH, MAX_HEIGHT, MIN_HEIGHT);
+ sizeAttrs = `width="${width}" height="${height}"`;
+ } else {
+ console.warn(` Could not read dimensions for ${logo}, using height-only fallback`);
+ sizeAttrs = `height="${MAX_HEIGHT}"`;
+ }
+
+ const img = ` `;
+ return ` ${img} `;
});
+ // Use between logos for spacing (GitHub strips style/class attributes)
return `
-${logoTags.join('\n')}
+${logoTags.join(' \n')}
`;
}
diff --git a/docs/src/data/databases.json b/docs/src/data/databases.json
index d8aaa76e6c9..4a64d016760 100644
--- a/docs/src/data/databases.json
+++ b/docs/src/data/databases.json
@@ -1,5 +1,5 @@
{
- "generated": "2026-01-27T06:14:03.276Z",
+ "generated": "2026-01-27T23:17:43.310Z",
"statistics": {
"totalDatabases": 68,
"withDocumentation": 68,
@@ -876,7 +876,7 @@
"module": "crate",
"documentation": {
"description": "CrateDB is a distributed SQL database for machine data and IoT workloads.",
- "logo": "cratedb.png",
+ "logo": "cratedb.svg",
"homepage_url": "https://crate.io/",
"categories": [
"TIME_SERIES",
@@ -4577,7 +4577,7 @@
"module": "risingwave",
"documentation": {
"description": "RisingWave is a distributed streaming database.",
- "logo": "risingwave.png",
+ "logo": "risingwave.svg",
"homepage_url": "https://risingwave.com/",
"pypi_packages": [
"psycopg2",
diff --git a/docs/static/img/databases/cratedb.png b/docs/static/img/databases/cratedb.png
deleted file mode 100644
index dedef2414c8..00000000000
Binary files a/docs/static/img/databases/cratedb.png and /dev/null differ
diff --git a/docs/static/img/databases/cratedb.svg b/docs/static/img/databases/cratedb.svg
new file mode 100644
index 00000000000..c34a58e0d11
--- /dev/null
+++ b/docs/static/img/databases/cratedb.svg
@@ -0,0 +1 @@
+CrateDB
diff --git a/docs/static/img/databases/risingwave.png b/docs/static/img/databases/risingwave.png
deleted file mode 100644
index a5db27a527f..00000000000
Binary files a/docs/static/img/databases/risingwave.png and /dev/null differ
diff --git a/docs/static/img/databases/risingwave.svg b/docs/static/img/databases/risingwave.svg
new file mode 100644
index 00000000000..5614269b8e4
--- /dev/null
+++ b/docs/static/img/databases/risingwave.svg
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/superset-frontend/packages/superset-ui-core/src/components/assets/images/loading.svg b/superset-frontend/packages/superset-ui-core/src/components/assets/images/loading.svg
index ffb9eb0ce73..e588d699082 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/assets/images/loading.svg
+++ b/superset-frontend/packages/superset-ui-core/src/components/assets/images/loading.svg
@@ -1,3 +1,21 @@
+
diff --git a/superset/db_engine_specs/crate.py b/superset/db_engine_specs/crate.py
index f23ccb4dcee..916f98f3f01 100644
--- a/superset/db_engine_specs/crate.py
+++ b/superset/db_engine_specs/crate.py
@@ -36,7 +36,7 @@ class CrateEngineSpec(BaseEngineSpec):
"description": (
"CrateDB is a distributed SQL database for machine data and IoT workloads."
),
- "logo": "cratedb.png",
+ "logo": "cratedb.svg",
"homepage_url": "https://crate.io/",
"categories": [DatabaseCategory.TIME_SERIES, DatabaseCategory.OPEN_SOURCE],
"pypi_packages": ["crate", "sqlalchemy-cratedb"],
diff --git a/superset/db_engine_specs/risingwave.py b/superset/db_engine_specs/risingwave.py
index 1a4c9cfe91b..899a9f4c834 100644
--- a/superset/db_engine_specs/risingwave.py
+++ b/superset/db_engine_specs/risingwave.py
@@ -28,7 +28,7 @@ class RisingWaveDbEngineSpec(PostgresEngineSpec):
metadata = {
"description": "RisingWave is a distributed streaming database.",
- "logo": "risingwave.png",
+ "logo": "risingwave.svg",
"homepage_url": "https://risingwave.com/",
"categories": [
DatabaseCategory.ANALYTICAL_DATABASES,