feat(docs): auto-generate database documentation from lib.py (#36805)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-01-21 10:54:01 -08:00
committed by GitHub
parent 2c1a33fd32
commit b460ca94c6
133 changed files with 11531 additions and 2123 deletions

View File

@@ -98,6 +98,7 @@ interface SectionHeaderProps {
title: string;
subtitle?: string | ReactNode;
dark?: boolean;
link?: string;
}
const SectionHeader = ({
@@ -105,15 +106,24 @@ const SectionHeader = ({
title,
subtitle,
dark,
link,
}: SectionHeaderProps) => {
const Heading = level;
const StyledRoot =
level === 'h1' ? StyledSectionHeaderH1 : StyledSectionHeaderH2;
const titleContent = link ? (
<a href={link} style={{ color: 'inherit', textDecoration: 'none' }}>
{title}
</a>
) : (
title
);
return (
<StyledRoot dark={!!dark}>
<Heading className="title">{title}</Heading>
<Heading className="title">{titleContent}</Heading>
<img className="line" src="/img/community/line.png" alt="line" />
{subtitle && <div className="subtitle">{subtitle}</div>}
</StyledRoot>

View File

@@ -0,0 +1,578 @@
/**
* 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.
*/
import React, { useState, useMemo } from 'react';
import { Card, Row, Col, Statistic, Table, Tag, Input, Select, Tooltip } from 'antd';
import {
DatabaseOutlined,
CheckCircleOutlined,
ApiOutlined,
KeyOutlined,
SearchOutlined,
LinkOutlined,
} from '@ant-design/icons';
import type { DatabaseData, DatabaseInfo, TimeGrains } from './types';
interface DatabaseIndexProps {
data: DatabaseData;
}
// Type for table entries (includes both regular DBs and compatible DBs)
interface TableEntry {
name: string;
categories: string[]; // Multiple categories supported
score: number;
max_score: number;
timeGrainCount: number;
time_grains?: TimeGrains;
hasDrivers: boolean;
hasAuthMethods: boolean;
hasConnectionString: boolean;
joins?: boolean;
subqueries?: boolean;
supports_dynamic_schema?: boolean;
supports_catalog?: boolean;
ssh_tunneling?: boolean;
supports_file_upload?: boolean;
query_cancelation?: boolean;
query_cost_estimation?: boolean;
user_impersonation?: boolean;
sql_validation?: boolean;
documentation?: DatabaseInfo['documentation'];
// For compatible databases
isCompatible?: boolean;
compatibleWith?: string;
compatibleDescription?: string;
}
// Map category constant names to display names
const CATEGORY_DISPLAY_NAMES: Record<string, string> = {
'CLOUD_AWS': 'Cloud - AWS',
'CLOUD_GCP': 'Cloud - Google',
'CLOUD_AZURE': 'Cloud - Azure',
'CLOUD_DATA_WAREHOUSES': 'Cloud Data Warehouses',
'APACHE_PROJECTS': 'Apache Projects',
'TRADITIONAL_RDBMS': 'Traditional RDBMS',
'ANALYTICAL_DATABASES': 'Analytical Databases',
'SEARCH_NOSQL': 'Search & NoSQL',
'QUERY_ENGINES': 'Query Engines',
'TIME_SERIES': 'Time Series Databases',
'OTHER': 'Other Databases',
'OPEN_SOURCE': 'Open Source',
'HOSTED_OPEN_SOURCE': 'Hosted Open Source',
'PROPRIETARY': 'Proprietary',
};
// Category colors for visual distinction
const CATEGORY_COLORS: Record<string, string> = {
'Cloud - AWS': 'orange',
'Cloud - Google': 'blue',
'Cloud - Azure': 'cyan',
'Cloud Data Warehouses': 'purple',
'Apache Projects': 'red',
'Traditional RDBMS': 'green',
'Analytical Databases': 'magenta',
'Search & NoSQL': 'gold',
'Query Engines': 'lime',
'Time Series Databases': 'volcano',
'Other Databases': 'default',
// Licensing categories
'Open Source': 'geekblue',
'Hosted Open Source': 'cyan',
'Proprietary': 'default',
};
// Convert category constant to display name
function getCategoryDisplayName(cat: string): string {
return CATEGORY_DISPLAY_NAMES[cat] || cat;
}
// Get categories for a database - uses categories from metadata when available
// Falls back to name-based inference for compatible databases without categories
function getCategories(
name: string,
documentationCategories?: string[]
): string[] {
// Prefer categories from documentation metadata (computed by Python)
if (documentationCategories && documentationCategories.length > 0) {
return documentationCategories.map(getCategoryDisplayName);
}
// Fallback: infer from name (for compatible databases without categories)
const nameLower = name.toLowerCase();
if (nameLower.includes('aws') || nameLower.includes('amazon'))
return ['Cloud - AWS'];
if (nameLower.includes('google') || nameLower.includes('bigquery'))
return ['Cloud - Google'];
if (nameLower.includes('azure') || nameLower.includes('microsoft'))
return ['Cloud - Azure'];
if (nameLower.includes('snowflake') || nameLower.includes('databricks'))
return ['Cloud Data Warehouses'];
if (
nameLower.includes('apache') ||
nameLower.includes('druid') ||
nameLower.includes('hive') ||
nameLower.includes('spark')
)
return ['Apache Projects'];
if (
nameLower.includes('postgres') ||
nameLower.includes('mysql') ||
nameLower.includes('sqlite') ||
nameLower.includes('mariadb')
)
return ['Traditional RDBMS'];
if (
nameLower.includes('clickhouse') ||
nameLower.includes('vertica') ||
nameLower.includes('starrocks')
)
return ['Analytical Databases'];
if (
nameLower.includes('elastic') ||
nameLower.includes('solr') ||
nameLower.includes('couchbase')
)
return ['Search & NoSQL'];
if (nameLower.includes('trino') || nameLower.includes('presto'))
return ['Query Engines'];
return ['Other Databases'];
}
// Count supported time grains
function countTimeGrains(db: DatabaseInfo): number {
if (!db.time_grains) return 0;
return Object.values(db.time_grains).filter(Boolean).length;
}
// Format time grain name for display (e.g., FIVE_MINUTES -> "5 min")
function formatTimeGrain(grain: string): string {
const mapping: Record<string, string> = {
SECOND: 'Second',
FIVE_SECONDS: '5 sec',
THIRTY_SECONDS: '30 sec',
MINUTE: 'Minute',
FIVE_MINUTES: '5 min',
TEN_MINUTES: '10 min',
FIFTEEN_MINUTES: '15 min',
THIRTY_MINUTES: '30 min',
HALF_HOUR: '30 min',
HOUR: 'Hour',
SIX_HOURS: '6 hours',
DAY: 'Day',
WEEK: 'Week',
WEEK_STARTING_SUNDAY: 'Week (Sun)',
WEEK_STARTING_MONDAY: 'Week (Mon)',
WEEK_ENDING_SATURDAY: 'Week (→Sat)',
WEEK_ENDING_SUNDAY: 'Week (→Sun)',
MONTH: 'Month',
QUARTER: 'Quarter',
QUARTER_YEAR: 'Quarter',
YEAR: 'Year',
};
return mapping[grain] || grain;
}
// Get list of supported time grains for tooltip
function getSupportedTimeGrains(timeGrains?: TimeGrains): string[] {
if (!timeGrains) return [];
return Object.entries(timeGrains)
.filter(([, supported]) => supported)
.map(([grain]) => formatTimeGrain(grain));
}
const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
const [searchText, setSearchText] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const { statistics, databases } = data;
// Convert databases object to array, including compatible databases
const databaseList = useMemo(() => {
const entries: TableEntry[] = [];
Object.entries(databases).forEach(([name, db]) => {
// Add the main database
// Use categories from documentation metadata (computed by Python) when available
entries.push({
...db,
name,
categories: getCategories(name, db.documentation?.categories),
timeGrainCount: countTimeGrains(db),
hasDrivers: (db.documentation?.drivers?.length ?? 0) > 0,
hasAuthMethods: (db.documentation?.authentication_methods?.length ?? 0) > 0,
hasConnectionString: Boolean(
db.documentation?.connection_string ||
(db.documentation?.drivers?.length ?? 0) > 0
),
isCompatible: false,
});
// Add compatible databases from this database's documentation
const compatibleDbs = db.documentation?.compatible_databases ?? [];
compatibleDbs.forEach((compat) => {
// Check if this compatible DB already exists as a main entry
const existsAsMain = Object.keys(databases).some(
(dbName) => dbName.toLowerCase() === compat.name.toLowerCase()
);
if (!existsAsMain) {
// Compatible databases: use their categories if defined, or infer from name
entries.push({
name: compat.name,
categories: getCategories(compat.name, compat.categories),
// Compatible DBs inherit scores from parent
score: db.score,
max_score: db.max_score,
timeGrainCount: countTimeGrains(db),
hasDrivers: false,
hasAuthMethods: false,
hasConnectionString: Boolean(compat.connection_string),
joins: db.joins,
subqueries: db.subqueries,
supports_dynamic_schema: db.supports_dynamic_schema,
supports_catalog: db.supports_catalog,
ssh_tunneling: db.ssh_tunneling,
documentation: {
description: compat.description,
connection_string: compat.connection_string,
pypi_packages: compat.pypi_packages,
},
isCompatible: true,
compatibleWith: name,
compatibleDescription: `Uses ${name} driver`,
});
}
});
});
return entries;
}, [databases]);
// Filter and sort databases
const filteredDatabases = useMemo(() => {
return databaseList
.filter((db) => {
const matchesSearch =
!searchText ||
db.name.toLowerCase().includes(searchText.toLowerCase()) ||
db.documentation?.description
?.toLowerCase()
.includes(searchText.toLowerCase());
const matchesCategory = !categoryFilter || db.categories.includes(categoryFilter);
return matchesSearch && matchesCategory;
})
.sort((a, b) => b.score - a.score);
}, [databaseList, searchText, categoryFilter]);
// Get unique categories and counts for filter
const { categories, categoryCounts } = useMemo(() => {
const counts: Record<string, number> = {};
databaseList.forEach((db) => {
// Count each category the database belongs to
db.categories.forEach((cat) => {
counts[cat] = (counts[cat] || 0) + 1;
});
});
return {
categories: Object.keys(counts).sort(),
categoryCounts: counts,
};
}, [databaseList]);
// Table columns
const columns = [
{
title: 'Database',
dataIndex: 'name',
key: 'name',
sorter: (a: TableEntry, b: TableEntry) => a.name.localeCompare(b.name),
render: (name: string, record: TableEntry) => {
// Convert name to URL slug
const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
// Link to parent for compatible DBs, otherwise to own page
const linkTarget = record.isCompatible && record.compatibleWith
? `/docs/databases/supported/${toSlug(record.compatibleWith)}`
: `/docs/databases/supported/${toSlug(name)}`;
return (
<div>
<a href={linkTarget}>
<strong>{name}</strong>
</a>
{record.isCompatible && record.compatibleWith && (
<Tag
icon={<LinkOutlined />}
color="geekblue"
style={{ marginLeft: 8, fontSize: '11px' }}
>
{record.compatibleWith} compatible
</Tag>
)}
<div style={{ fontSize: '12px', color: '#666' }}>
{record.documentation?.description?.slice(0, 80)}
{(record.documentation?.description?.length ?? 0) > 80 ? '...' : ''}
</div>
</div>
);
},
},
{
title: 'Categories',
dataIndex: 'categories',
key: 'categories',
width: 220,
filters: categories.map((cat) => ({ text: cat, value: cat })),
onFilter: (value: React.Key | boolean, record: TableEntry) =>
record.categories.includes(value as string),
render: (cats: string[]) => (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{cats.map((cat) => (
<Tag key={cat} color={CATEGORY_COLORS[cat] || 'default'}>{cat}</Tag>
))}
</div>
),
},
{
title: 'Score',
dataIndex: 'score',
key: 'score',
width: 80,
sorter: (a: TableEntry, b: TableEntry) => a.score - b.score,
defaultSortOrder: 'descend' as const,
render: (score: number, record: TableEntry) => (
<span
style={{
color: score > 150 ? '#52c41a' : score > 100 ? '#1890ff' : '#666',
fontWeight: score > 150 ? 'bold' : 'normal',
}}
>
{score}/{record.max_score}
</span>
),
},
{
title: 'Time Grains',
dataIndex: 'timeGrainCount',
key: 'timeGrainCount',
width: 100,
sorter: (a: TableEntry, b: TableEntry) => a.timeGrainCount - b.timeGrainCount,
render: (count: number, record: TableEntry) => {
if (count === 0) return <span>-</span>;
const grains = getSupportedTimeGrains(record.time_grains);
return (
<Tooltip
title={
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', maxWidth: 280 }}>
{grains.map((grain) => (
<Tag key={grain} style={{ margin: 0 }}>{grain}</Tag>
))}
</div>
}
placement="top"
>
<span style={{ cursor: 'help', borderBottom: '1px dotted #999' }}>
{count} grains
</span>
</Tooltip>
);
},
},
{
title: 'Features',
key: 'features',
width: 280,
filters: [
{ text: 'JOINs', value: 'joins' },
{ text: 'Subqueries', value: 'subqueries' },
{ text: 'Dynamic Schema', value: 'dynamic_schema' },
{ text: 'Catalog', value: 'catalog' },
{ text: 'SSH Tunneling', value: 'ssh' },
{ text: 'File Upload', value: 'file_upload' },
{ text: 'Query Cancel', value: 'query_cancel' },
{ text: 'Cost Estimation', value: 'cost_estimation' },
{ text: 'User Impersonation', value: 'impersonation' },
{ text: 'SQL Validation', value: 'sql_validation' },
],
onFilter: (value: React.Key | boolean, record: TableEntry) => {
switch (value) {
case 'joins':
return Boolean(record.joins);
case 'subqueries':
return Boolean(record.subqueries);
case 'dynamic_schema':
return Boolean(record.supports_dynamic_schema);
case 'catalog':
return Boolean(record.supports_catalog);
case 'ssh':
return Boolean(record.ssh_tunneling);
case 'file_upload':
return Boolean(record.supports_file_upload);
case 'query_cancel':
return Boolean(record.query_cancelation);
case 'cost_estimation':
return Boolean(record.query_cost_estimation);
case 'impersonation':
return Boolean(record.user_impersonation);
case 'sql_validation':
return Boolean(record.sql_validation);
default:
return true;
}
},
render: (_: unknown, record: TableEntry) => (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{record.joins && <Tag color="green">JOINs</Tag>}
{record.subqueries && <Tag color="green">Subqueries</Tag>}
{record.supports_dynamic_schema && <Tag color="blue">Dynamic Schema</Tag>}
{record.supports_catalog && <Tag color="purple">Catalog</Tag>}
{record.ssh_tunneling && <Tag color="cyan">SSH</Tag>}
{record.supports_file_upload && <Tag color="orange">File Upload</Tag>}
{record.query_cancelation && <Tag color="volcano">Query Cancel</Tag>}
{record.query_cost_estimation && <Tag color="gold">Cost Est.</Tag>}
{record.user_impersonation && <Tag color="magenta">Impersonation</Tag>}
{record.sql_validation && <Tag color="lime">SQL Validation</Tag>}
</div>
),
},
{
title: 'Documentation',
key: 'docs',
width: 150,
render: (_: unknown, record: TableEntry) => (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{record.hasConnectionString && (
<Tag icon={<ApiOutlined />} color="default">
Connection
</Tag>
)}
{record.hasDrivers && (
<Tag icon={<DatabaseOutlined />} color="default">
Drivers
</Tag>
)}
{record.hasAuthMethods && (
<Tag icon={<KeyOutlined />} color="default">
Auth
</Tag>
)}
</div>
),
},
];
return (
<div className="database-index">
{/* Statistics Cards */}
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="Total Databases"
value={statistics.totalDatabases}
prefix={<DatabaseOutlined />}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="With Documentation"
value={statistics.withDocumentation}
prefix={<CheckCircleOutlined />}
suffix={`/ ${statistics.totalDatabases}`}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="Multiple Drivers"
value={statistics.withDrivers}
prefix={<ApiOutlined />}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="Auth Methods"
value={statistics.withAuthMethods}
prefix={<KeyOutlined />}
/>
</Card>
</Col>
</Row>
{/* Filters */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} sm={12}>
<Input
placeholder="Search databases..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
/>
</Col>
<Col xs={24} sm={12}>
<Select
placeholder="Filter by category"
style={{ width: '100%' }}
value={categoryFilter}
onChange={setCategoryFilter}
allowClear
options={categories.map((cat) => ({
label: (
<span>
<Tag
color={CATEGORY_COLORS[cat] || 'default'}
style={{ marginRight: 8 }}
>
{categoryCounts[cat] || 0}
</Tag>
{cat}
</span>
),
value: cat,
}))}
/>
</Col>
</Row>
{/* Database Table */}
<Table
dataSource={filteredDatabases}
columns={columns}
rowKey={(record) => record.isCompatible ? `${record.compatibleWith}-${record.name}` : record.name}
pagination={{
pageSize: 20,
showSizeChanger: true,
showTotal: (total) => `${total} databases`,
}}
size="middle"
/>
</div>
);
};
export default DatabaseIndex;

View File

@@ -0,0 +1,634 @@
/**
* 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.
*/
import React from 'react';
import {
Card,
Collapse,
Table,
Tag,
Typography,
Alert,
Space,
Divider,
Tabs,
} from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
WarningOutlined,
LinkOutlined,
KeyOutlined,
SettingOutlined,
BookOutlined,
EditOutlined,
GithubOutlined,
} from '@ant-design/icons';
import type { DatabaseInfo } from './types';
// Simple code block component for connection strings
const CodeBlock: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<pre
style={{
background: 'var(--ifm-code-background)',
padding: '12px 16px',
borderRadius: '4px',
overflow: 'auto',
fontSize: '13px',
fontFamily: 'var(--ifm-font-family-monospace)',
}}
>
<code>{children}</code>
</pre>
);
const { Title, Paragraph, Text } = Typography;
const { Panel } = Collapse;
const { TabPane } = Tabs;
interface DatabasePageProps {
database: DatabaseInfo;
name: string;
}
// Feature badge component
const FeatureBadge: React.FC<{ supported: boolean; label: string }> = ({
supported,
label,
}) => (
<Tag
icon={supported ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
color={supported ? 'success' : 'default'}
>
{label}
</Tag>
);
// Time grain badge
const TimeGrainBadge: React.FC<{ supported: boolean; grain: string }> = ({
supported,
grain,
}) => (
<Tag color={supported ? 'blue' : 'default'} style={{ margin: '2px' }}>
{grain}
</Tag>
);
const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
const { documentation: docs } = database;
// Helper to render connection string with copy button
const renderConnectionString = (connStr: string, description?: string) => (
<div style={{ marginBottom: 16 }}>
{description && (
<Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>
{description}
</Text>
)}
<CodeBlock>{connStr}</CodeBlock>
</div>
);
// Render driver information
const renderDrivers = () => {
if (!docs?.drivers?.length) return null;
return (
<Card title="Drivers" style={{ marginBottom: 16 }}>
<Tabs>
{docs.drivers.map((driver, idx) => (
<TabPane
tab={
<span>
{driver.name}
{driver.is_recommended && (
<Tag color="green" style={{ marginLeft: 8 }}>
Recommended
</Tag>
)}
</span>
}
key={idx}
>
<Space direction="vertical" style={{ width: '100%' }}>
{driver.pypi_package && (
<div>
<Text strong>PyPI Package: </Text>
<code>{driver.pypi_package}</code>
</div>
)}
{driver.connection_string &&
renderConnectionString(driver.connection_string)}
{driver.notes && (
<Alert message={driver.notes} type="info" showIcon />
)}
{driver.docs_url && (
<a href={driver.docs_url} target="_blank" rel="noreferrer">
<LinkOutlined /> Documentation
</a>
)}
</Space>
</TabPane>
))}
</Tabs>
</Card>
);
};
// Render authentication methods
const renderAuthMethods = () => {
if (!docs?.authentication_methods?.length) return null;
return (
<Card
title={
<>
<KeyOutlined /> Authentication Methods
</>
}
style={{ marginBottom: 16 }}
>
<Collapse accordion>
{docs.authentication_methods.map((auth, idx) => (
<Panel header={auth.name} key={idx}>
{auth.description && <Paragraph>{auth.description}</Paragraph>}
{auth.requirements && (
<Alert
message="Requirements"
description={auth.requirements}
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
)}
{auth.connection_string &&
renderConnectionString(
auth.connection_string,
'Connection String'
)}
{auth.secure_extra && (
<div>
<Text strong>Secure Extra Configuration:</Text>
<CodeBlock>
{JSON.stringify(auth.secure_extra, null, 2)}
</CodeBlock>
</div>
)}
{auth.engine_parameters && (
<div>
<Text strong>Engine Parameters:</Text>
<CodeBlock>
{JSON.stringify(auth.engine_parameters, null, 2)}
</CodeBlock>
</div>
)}
{auth.notes && (
<Alert message={auth.notes} type="info" showIcon />
)}
</Panel>
))}
</Collapse>
</Card>
);
};
// Render engine parameters
const renderEngineParams = () => {
if (!docs?.engine_parameters?.length) return null;
return (
<Card
title={
<>
<SettingOutlined /> Engine Parameters
</>
}
style={{ marginBottom: 16 }}
>
<Collapse>
{docs.engine_parameters.map((param, idx) => (
<Panel header={param.name} key={idx}>
{param.description && <Paragraph>{param.description}</Paragraph>}
{param.json && (
<CodeBlock>
{JSON.stringify(param.json, null, 2)}
</CodeBlock>
)}
{param.docs_url && (
<a href={param.docs_url} target="_blank" rel="noreferrer">
<LinkOutlined /> Learn more
</a>
)}
</Panel>
))}
</Collapse>
</Card>
);
};
// Render compatible databases (for PostgreSQL, etc.)
const renderCompatibleDatabases = () => {
if (!docs?.compatible_databases?.length) return null;
// Create array of all panel keys to expand by default
const allPanelKeys = docs.compatible_databases.map((_, idx) => idx);
return (
<Card title="Compatible Databases" style={{ marginBottom: 16 }}>
<Paragraph>
The following databases are compatible with the {name} driver:
</Paragraph>
<Collapse defaultActiveKey={allPanelKeys}>
{docs.compatible_databases.map((compat, idx) => (
<Panel
header={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{compat.logo && (
<img
src={`/img/databases/${compat.logo}`}
alt={compat.name}
style={{
width: 28,
height: 28,
objectFit: 'contain',
}}
/>
)}
<span>{compat.name}</span>
</div>
}
key={idx}
>
{compat.description && (
<Paragraph>{compat.description}</Paragraph>
)}
{compat.connection_string &&
renderConnectionString(compat.connection_string)}
{compat.parameters && (
<div>
<Text strong>Parameters:</Text>
<Table
dataSource={Object.entries(compat.parameters).map(
([key, value]) => ({
key,
parameter: key,
description: value,
})
)}
columns={[
{ title: 'Parameter', dataIndex: 'parameter', key: 'p' },
{
title: 'Description',
dataIndex: 'description',
key: 'd',
},
]}
pagination={false}
size="small"
/>
</div>
)}
{compat.notes && (
<Alert
message={compat.notes}
type="info"
showIcon
style={{ marginTop: 16 }}
/>
)}
</Panel>
))}
</Collapse>
</Card>
);
};
// Render feature matrix
const renderFeatures = () => {
const features: Array<{ key: keyof DatabaseInfo; label: string }> = [
{ key: 'joins', label: 'JOINs' },
{ key: 'subqueries', label: 'Subqueries' },
{ key: 'supports_dynamic_schema', label: 'Dynamic Schema' },
{ key: 'supports_catalog', label: 'Catalog Support' },
{ key: 'supports_dynamic_catalog', label: 'Dynamic Catalog' },
{ key: 'ssh_tunneling', label: 'SSH Tunneling' },
{ key: 'query_cancelation', label: 'Query Cancellation' },
{ key: 'supports_file_upload', label: 'File Upload' },
{ key: 'user_impersonation', label: 'User Impersonation' },
{ key: 'query_cost_estimation', label: 'Cost Estimation' },
{ key: 'sql_validation', label: 'SQL Validation' },
];
return (
<Card title="Supported Features" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{features.map(({ key, label }) => (
<FeatureBadge
key={key}
supported={Boolean(database[key])}
label={label}
/>
))}
</div>
{database.score > 0 && (
<div style={{ marginTop: 16 }}>
<Text>
Feature Score:{' '}
<Text strong>
{database.score}/{database.max_score}
</Text>
</Text>
</div>
)}
</Card>
);
};
// Render time grains
const renderTimeGrains = () => {
if (!database.time_grains) return null;
const commonGrains = [
'SECOND',
'MINUTE',
'HOUR',
'DAY',
'WEEK',
'MONTH',
'QUARTER',
'YEAR',
];
const extendedGrains = Object.keys(database.time_grains).filter(
(g) => !commonGrains.includes(g)
);
return (
<Card title="Time Grains" style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 16 }}>
<Text strong>Common Time Grains:</Text>
<div style={{ marginTop: 8 }}>
{commonGrains.map((grain) => (
<TimeGrainBadge
key={grain}
grain={grain}
supported={Boolean(
database.time_grains[grain as keyof typeof database.time_grains]
)}
/>
))}
</div>
</div>
{extendedGrains.length > 0 && (
<div>
<Text strong>Extended Time Grains:</Text>
<div style={{ marginTop: 8 }}>
{extendedGrains.map((grain) => (
<TimeGrainBadge
key={grain}
grain={grain}
supported={Boolean(
database.time_grains[grain as keyof typeof database.time_grains]
)}
/>
))}
</div>
</div>
)}
</Card>
);
};
return (
<div
className="database-page"
id={name.toLowerCase().replace(/\s+/g, '-')}
>
<div style={{ marginBottom: 16 }}>
{docs?.logo && (
<img
src={`/img/databases/${docs.logo}`}
alt={name}
style={{
height: 120,
objectFit: 'contain',
marginBottom: 12,
}}
/>
)}
<Title level={1} style={{ margin: 0 }}>{name}</Title>
{docs?.homepage_url && (
<a
href={docs.homepage_url}
target="_blank"
rel="noreferrer"
style={{ fontSize: 14 }}
>
<LinkOutlined /> {docs.homepage_url}
</a>
)}
</div>
{docs?.description && <Paragraph>{docs.description}</Paragraph>}
{/* Warnings */}
{docs?.warnings?.map((warning, idx) => (
<Alert
key={idx}
message={warning}
type="warning"
icon={<WarningOutlined />}
showIcon
style={{ marginBottom: 16 }}
/>
))}
{/* Known Limitations */}
{docs?.limitations?.length > 0 && (
<Card
title="Known Limitations"
style={{ marginBottom: 16 }}
type="inner"
>
<ul style={{ margin: 0, paddingLeft: 20 }}>
{docs.limitations.map((limitation, idx) => (
<li key={idx}>{limitation}</li>
))}
</ul>
</Card>
)}
{/* Installation */}
{(docs?.pypi_packages?.length || docs?.install_instructions) && (
<Card title="Installation" style={{ marginBottom: 16 }}>
{docs.pypi_packages?.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Text strong>Required packages: </Text>
{docs.pypi_packages.map((pkg) => (
<Tag key={pkg} color="blue">
{pkg}
</Tag>
))}
</div>
)}
{docs.version_requirements && (
<Alert
message={`Version requirement: ${docs.version_requirements}`}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
)}
{docs.install_instructions && (
<CodeBlock>{docs.install_instructions}</CodeBlock>
)}
</Card>
)}
{/* Basic Connection */}
{docs?.connection_string && !docs?.drivers?.length && (
<Card title="Connection String" style={{ marginBottom: 16 }}>
{renderConnectionString(docs.connection_string)}
{docs.parameters && (
<Table
dataSource={Object.entries(docs.parameters).map(
([key, value]) => ({
key,
parameter: key,
description: value,
})
)}
columns={[
{ title: 'Parameter', dataIndex: 'parameter', key: 'p' },
{ title: 'Description', dataIndex: 'description', key: 'd' },
]}
pagination={false}
size="small"
/>
)}
{docs.default_port && (
<Text type="secondary">Default port: {docs.default_port}</Text>
)}
</Card>
)}
{/* Drivers */}
{renderDrivers()}
{/* Connection Examples */}
{docs?.connection_examples?.length > 0 && (
<Card title="Connection Examples" style={{ marginBottom: 16 }}>
{docs.connection_examples.map((example, idx) => (
<div key={idx}>
{renderConnectionString(
example.connection_string,
example.description
)}
</div>
))}
</Card>
)}
{/* Authentication Methods */}
{renderAuthMethods()}
{/* Engine Parameters */}
{renderEngineParams()}
{/* Features */}
{renderFeatures()}
{/* Time Grains */}
{renderTimeGrains()}
{/* Compatible Databases */}
{renderCompatibleDatabases()}
{/* Notes */}
{docs?.notes && (
<Alert
message="Notes"
description={docs.notes}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
)}
{/* External Links */}
{(docs?.docs_url || docs?.tutorials?.length) && (
<Card
title={
<>
<BookOutlined /> Resources
</>
}
style={{ marginBottom: 16 }}
>
<Space direction="vertical">
{docs.docs_url && (
<a href={docs.docs_url} target="_blank" rel="noreferrer">
<LinkOutlined /> Official Documentation
</a>
)}
{docs.sqlalchemy_docs_url && (
<a href={docs.sqlalchemy_docs_url} target="_blank" rel="noreferrer">
<LinkOutlined /> SQLAlchemy Dialect Documentation
</a>
)}
{docs.tutorials?.map((tutorial, idx) => (
<a key={idx} href={tutorial} target="_blank" rel="noreferrer">
<LinkOutlined /> Tutorial {idx + 1}
</a>
))}
</Space>
</Card>
)}
{/* Edit link */}
{database.module && (
<Card
style={{
marginBottom: 16,
background: 'var(--ifm-background-surface-color)',
borderStyle: 'dashed',
}}
size="small"
>
<Space>
<GithubOutlined />
<Text type="secondary">
Help improve this documentation by editing the engine spec:
</Text>
<a
href={`https://github.com/apache/superset/edit/master/superset/db_engine_specs/${database.module}.py`}
target="_blank"
rel="noreferrer"
>
<EditOutlined /> Edit {database.module}.py
</a>
</Space>
</Card>
)}
<Divider />
</div>
);
};
export default DatabasePage;

View File

@@ -0,0 +1,22 @@
/**
* 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.
*/
export { default as DatabaseIndex } from './DatabaseIndex';
export { default as DatabasePage } from './DatabasePage';
export * from './types';

View File

@@ -0,0 +1,243 @@
/**
* 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.
*/
/**
* TypeScript types for database documentation data
* Generated from superset/db_engine_specs/lib.py
*/
export interface Driver {
name: string;
pypi_package?: string;
connection_string?: string;
is_recommended?: boolean;
notes?: string;
docs_url?: string;
default_port?: number;
odbc_driver_paths?: Record<string, string>;
environment_variables?: Record<string, string>;
}
export interface ConnectionExample {
description: string;
connection_string: string;
}
export interface HostExample {
platform: string;
host: string;
}
export interface AuthenticationMethod {
name: string;
description?: string;
requirements?: string;
connection_string?: string;
secure_extra?: Record<string, unknown>;
secure_extra_body?: Record<string, unknown>;
secure_extra_path?: Record<string, unknown>;
engine_parameters?: Record<string, unknown>;
config_example?: Record<string, unknown>;
notes?: string;
}
export interface EngineParameter {
name: string;
description?: string;
json?: Record<string, unknown>;
secure_extra?: Record<string, unknown>;
docs_url?: string;
}
export interface SSLConfiguration {
custom_certificate?: string;
disable_ssl_verification?: {
engine_params?: Record<string, unknown>;
};
}
export interface CompatibleDatabase {
name: string;
description?: string;
logo?: string;
homepage_url?: string;
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
pypi_packages?: string[];
connection_string?: string;
parameters?: Record<string, string>;
connection_examples?: ConnectionExample[];
notes?: string;
docs_url?: string;
}
export interface DatabaseDocumentation {
description?: string;
logo?: string;
homepage_url?: string;
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
pypi_packages?: string[];
connection_string?: string;
default_port?: number;
parameters?: Record<string, string>;
notes?: string;
limitations?: string[]; // Known limitations or caveats
connection_examples?: ConnectionExample[];
host_examples?: HostExample[];
drivers?: Driver[];
authentication_methods?: AuthenticationMethod[];
engine_parameters?: EngineParameter[];
ssl_configuration?: SSLConfiguration;
version_requirements?: string;
install_instructions?: string;
warnings?: string[];
tutorials?: string[];
docs_url?: string;
sqlalchemy_docs_url?: string;
advanced_features?: Record<string, string>;
compatible_databases?: CompatibleDatabase[];
}
export interface TimeGrains {
SECOND?: boolean;
MINUTE?: boolean;
HOUR?: boolean;
DAY?: boolean;
WEEK?: boolean;
MONTH?: boolean;
QUARTER?: boolean;
YEAR?: boolean;
FIVE_SECONDS?: boolean;
THIRTY_SECONDS?: boolean;
FIVE_MINUTES?: boolean;
TEN_MINUTES?: boolean;
FIFTEEN_MINUTES?: boolean;
THIRTY_MINUTES?: boolean;
HALF_HOUR?: boolean;
SIX_HOURS?: boolean;
WEEK_STARTING_SUNDAY?: boolean;
WEEK_STARTING_MONDAY?: boolean;
WEEK_ENDING_SATURDAY?: boolean;
WEEK_ENDING_SUNDAY?: boolean;
QUARTER_YEAR?: boolean;
}
export interface DatabaseInfo {
engine: string;
engine_name: string;
engine_aliases?: string[];
default_driver?: string;
module?: string;
documentation: DatabaseDocumentation;
// Diagnostics from lib.py diagnose() function
time_grains: TimeGrains;
score: number;
max_score: number;
// SQL capabilities
joins: boolean;
subqueries: boolean;
alias_in_select?: boolean;
alias_in_orderby?: boolean;
cte_in_subquery?: boolean;
sql_comments?: boolean;
escaped_colons?: boolean;
time_groupby_inline?: boolean;
alias_to_source_column?: boolean;
order_by_not_in_select?: boolean;
expressions_in_orderby?: boolean;
// Platform features
limit_method?: string;
limit_clause?: boolean;
max_column_name?: number;
supports_file_upload?: boolean;
supports_dynamic_schema?: boolean;
supports_catalog?: boolean;
supports_dynamic_catalog?: boolean;
// Advanced features
user_impersonation?: boolean;
ssh_tunneling?: boolean;
query_cancelation?: boolean;
expand_data?: boolean;
query_cost_estimation?: boolean;
sql_validation?: boolean;
get_metrics?: boolean;
where_latest_partition?: boolean;
get_extra_table_metadata?: boolean;
dbapi_exception_mapping?: boolean;
custom_errors?: boolean;
masked_encrypted_extra?: boolean;
column_type_mapping?: boolean;
function_names?: boolean;
}
export interface Statistics {
totalDatabases: number;
withDocumentation: number;
withConnectionString: number;
withDrivers: number;
withAuthMethods: number;
supportsJoins: number;
supportsSubqueries: number;
supportsDynamicSchema: number;
supportsCatalog: number;
averageScore: number;
maxScore: number;
byCategory: Record<string, string[]>;
}
export interface DatabaseData {
generated: string;
statistics: Statistics;
databases: Record<string, DatabaseInfo>;
}
// Helper type for sorting databases
export type SortField = 'name' | 'score' | 'category';
export type SortDirection = 'asc' | 'desc';
// Helper to get common time grains
export const COMMON_TIME_GRAINS = [
'SECOND',
'MINUTE',
'HOUR',
'DAY',
'WEEK',
'MONTH',
'QUARTER',
'YEAR',
] as const;
export const EXTENDED_TIME_GRAINS = [
'FIVE_SECONDS',
'THIRTY_SECONDS',
'FIVE_MINUTES',
'TEN_MINUTES',
'FIFTEEN_MINUTES',
'THIRTY_MINUTES',
'HALF_HOUR',
'SIX_HOURS',
'WEEK_STARTING_SUNDAY',
'WEEK_STARTING_MONDAY',
'WEEK_ENDING_SATURDAY',
'WEEK_ENDING_SUNDAY',
'QUARTER_YEAR',
] as const;