refactor(desktop): organize lib modules by domain

This commit is contained in:
t8y2 committed 2026-07-04 13:53:52 +08:00
1 parent 90f0594e93
commit 7e7c4944e9
700 files changed
+1292 -1264

No files matched your search

@@ -0,0 +1,116 @@
type QueryLikeResult = {
columns: string[];
rows: (string | number | boolean | null)[][];
};
export interface CreateDatabaseCharsetMetadata {
charsets: string[];
collationsByCharset: Record<string, string[]>;
}
export const CREATE_DATABASE_CHARSET_OPTIONS = ["utf8mb4", "utf8", "gbk", "latin1", "ascii", "big5", "gb2312", "utf16", "utf32"] as const;
const FALLBACK_COLLATION_OPTIONS: Record<string, string[]> = {
ascii: ["ascii_general_ci", "ascii_bin"],
big5: ["big5_chinese_ci", "big5_bin"],
gb2312: ["gb2312_chinese_ci", "gb2312_bin"],
gbk: ["gbk_chinese_ci", "gbk_bin"],
latin1: ["latin1_swedish_ci", "latin1_general_ci", "latin1_bin"],
utf16: ["utf16_general_ci", "utf16_unicode_ci", "utf16_bin"],
utf32: ["utf32_general_ci", "utf32_unicode_ci", "utf32_bin"],
utf8: ["utf8mb3_general_ci", "utf8mb3_unicode_ci", "utf8mb3_bin"],
utf8mb3: ["utf8mb3_general_ci", "utf8mb3_unicode_ci", "utf8mb3_bin"],
utf8mb4: ["utf8mb4_unicode_ci", "utf8mb4_general_ci", "utf8mb4_bin"],
};
export function normalizeCreateDatabaseCharset(value: string): string {
return value.trim();
}
export function normalizeCreateDatabaseCharsetKey(value: string): string {
const normalized = normalizeCreateDatabaseCharset(value).toLowerCase();
return normalized === "utf8" ? "utf8mb3" : normalized;
}
export function createDatabaseCollationOptionsForCharset(charset: string, collationsByCharset: Record<string, string[]> = FALLBACK_COLLATION_OPTIONS): string[] {
return collationsByCharset[normalizeCreateDatabaseCharsetKey(charset)] ?? [];
}
export function defaultCreateDatabaseCollationForCharset(charset: string, collationsByCharset: Record<string, string[]> = FALLBACK_COLLATION_OPTIONS): string {
return createDatabaseCollationOptionsForCharset(charset, collationsByCharset)[0] ?? "";
}
export function nextCreateDatabaseCollation(nextCharset: string, previousCharset: string, currentCollation: string, collationsByCharset: Record<string, string[]> = FALLBACK_COLLATION_OPTIONS): string {
const previousDefault = defaultCreateDatabaseCollationForCharset(previousCharset, collationsByCharset);
if (currentCollation && currentCollation !== previousDefault) return currentCollation;
return defaultCreateDatabaseCollationForCharset(nextCharset, collationsByCharset) || currentCollation;
}
export function fallbackCreateDatabaseCharsetMetadata(): CreateDatabaseCharsetMetadata {
return {
charsets: [...CREATE_DATABASE_CHARSET_OPTIONS],
collationsByCharset: { ...FALLBACK_COLLATION_OPTIONS },
};
}
export function parseCreateDatabaseCharsetMetadata(charsetResult: QueryLikeResult, collationResult: QueryLikeResult): CreateDatabaseCharsetMetadata {
const charsetNameIndex = columnIndex(charsetResult.columns, ["charset", "character set", "character_set_name"]);
const charsetDefaultCollationIndex = columnIndex(charsetResult.columns, ["default collation", "default_collate_name", "default_collation_name"]);
const collationNameIndex = columnIndex(collationResult.columns, ["collation", "collation_name"]);
const collationCharsetIndex = columnIndex(collationResult.columns, ["charset", "character_set_name"]);
const collationDefaultIndex = columnIndex(collationResult.columns, ["default", "is_default"]);
const charsets = uniqueStrings(charsetResult.rows.map((row) => cellString(row[charsetNameIndex])));
const collationsByCharset: Record<string, string[]> = {};
for (const row of collationResult.rows) {
const collation = cellString(row[collationNameIndex]);
const charset = normalizeCreateDatabaseCharsetKey(cellString(row[collationCharsetIndex]));
if (!collation || !charset || isHiddenCreateDatabaseCollation(collation)) continue;
collationsByCharset[charset] ??= [];
if (isDefaultFlag(row[collationDefaultIndex])) {
collationsByCharset[charset].unshift(collation);
} else if (!collationsByCharset[charset].includes(collation)) {
collationsByCharset[charset].push(collation);
}
}
for (const row of charsetResult.rows) {
const charset = normalizeCreateDatabaseCharsetKey(cellString(row[charsetNameIndex]));
const defaultCollation = cellString(row[charsetDefaultCollationIndex]);
if (!charset || !defaultCollation || isHiddenCreateDatabaseCollation(defaultCollation)) continue;
collationsByCharset[charset] ??= [];
const existingIndex = collationsByCharset[charset].indexOf(defaultCollation);
if (existingIndex > 0) collationsByCharset[charset].splice(existingIndex, 1);
if (existingIndex !== 0) collationsByCharset[charset].unshift(defaultCollation);
}
return {
charsets: charsets.length ? charsets : [...CREATE_DATABASE_CHARSET_OPTIONS],
collationsByCharset: Object.keys(collationsByCharset).length ? collationsByCharset : { ...FALLBACK_COLLATION_OPTIONS },
};
}
function columnIndex(columns: string[], names: string[]): number {
const lowerNames = names.map((name) => name.toLowerCase());
const index = columns.findIndex((column) => lowerNames.includes(column.trim().toLowerCase()));
return index >= 0 ? index : 0;
}
function cellString(value: unknown): string {
return typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim();
}
function uniqueStrings(values: string[]): string[] {
return [...new Set(values.filter(Boolean))];
}
function isHiddenCreateDatabaseCollation(value: string): boolean {
return value.toLowerCase().includes("_0900_");
}
function isDefaultFlag(value: unknown): boolean {
if (value === true) return true;
if (typeof value !== "string") return false;
return ["yes", "y", "1", "true"].includes(value.trim().toLowerCase());
}
@@ -0,0 +1,45 @@
import type { DatabaseType } from "@/types/database";
import * as api from "@/lib/backend/api";
const MYSQL_COMPATIBLE_PROFILES = new Set(["mysql", "mariadb", "tidb", "oceanbase", "doris", "starrocks", "custom_mysql"]);
const MYSQL_COMPATIBLE_TYPES = new Set<DatabaseType>(["mysql", "doris", "starrocks", "goldendb"]);
export interface CreateDatabaseSqlOptions {
databaseType?: DatabaseType;
driverProfile?: string | null;
name: string;
charset?: string;
collation?: string;
}
export function supportsCreateDatabaseCharset(databaseType?: DatabaseType, driverProfile?: string | null): boolean {
return MYSQL_COMPATIBLE_TYPES.has(databaseType as DatabaseType) || (!!driverProfile && MYSQL_COMPATIBLE_PROFILES.has(driverProfile.toLowerCase()));
}
export function buildCreateDatabaseSql(options: CreateDatabaseSqlOptions): Promise<string> {
return api.buildCreateDatabaseSql(options);
}
export function buildDuckDbAttachDatabaseSql(path: string, name: string): Promise<string> {
return api.buildDuckDbAttachDatabaseSql(path, name);
}
export function duckDbAttachedDatabaseNameFromPath(path: string): string {
const fileName = path.split(/[\\/]/).pop() ?? "";
const withoutExtension = fileName.replace(/\.[^.\\/]+$/, "");
const normalized = withoutExtension
.trim()
.replace(/[^\p{L}\p{N}_]+/gu, "_")
.replace(/^_+|_+$/g, "");
return normalized || "duckdb_database";
}
export function uniqueDuckDbAttachedDatabaseName(baseName: string, existingNames: string[]): string {
const existing = new Set(existingNames.map((name) => name.toLowerCase()));
if (!existing.has(baseName.toLowerCase())) return baseName;
for (let index = 2; index < Number.MAX_SAFE_INTEGER; index++) {
const candidate = `${baseName}_${index}`;
if (!existing.has(candidate.toLowerCase())) return candidate;
}
return `${baseName}_${Date.now()}`;
}
@@ -0,0 +1,5 @@
export * from "@/lib/database/databaseCapabilitySets";
export * from "@/lib/database/databaseDriverManifest";
export * from "@/lib/database/databaseFeatureSupport";
export * from "@/lib/database/databaseObjectCapabilities";
export * from "@/lib/database/databaseTableDataCapabilities";
@@ -0,0 +1,77 @@
import type { DatabaseType } from "@/types/database";
export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
"postgres",
"sqlserver",
"oracle",
"redshift",
"dameng",
"gaussdb",
"kwdb",
"kingbase",
"highgo",
"vastbase",
"yashandb",
"databricks",
"saphana",
"teradata",
"vertica",
"exasol",
"opengauss",
"oceanbase-oracle",
"gbase",
"jdbc",
"h2",
"snowflake",
"trino",
"prestosql",
"hive",
"databend",
"db2",
"informix",
"xugu",
"oscar",
"iotdb",
"iris",
"duckdb",
]);
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "firebird", "oceanbase-oracle", "access", "questdb"]);
export const FETCH_FIRST_TYPES = new Set<DatabaseType>(["oracle", "dameng"]);
export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
"postgres",
"redshift",
"sqlserver",
"db2",
"gaussdb",
"kwdb",
"kingbase",
"highgo",
"vastbase",
"yashandb",
"databricks",
"saphana",
"teradata",
"vertica",
"exasol",
"opengauss",
"oceanbase-oracle",
"gbase",
"jdbc",
"trino",
"prestosql",
"h2",
"informix",
"xugu",
"oscar",
"iris",
"duckdb",
]);
export const DATABASE_OBJECT_TREE_TYPES = new Set<DatabaseType>(["jdbc"]);
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb"]);
export const DIAGRAM_SQL_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlite", "rqlite", "turso", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "questdb", "oceanbase-oracle"]);
@@ -0,0 +1,102 @@
import type { DatabaseType } from "@/types/database";
import driverManifest from "../../../../../crates/dbx-core/assets/database-drivers.manifest.json";
export type DatabaseSupportLevel = "connect" | "browse" | "understand" | "operate";
export type DatabaseRuntimeMode = "native" | "file" | "agent" | "external";
export const DATABASE_PRODUCT_CAPABILITY_KEYS = [
"queryExecution",
"metadataBrowse",
"objectBrowser",
"objectSource",
"schemaSearch",
"diagram",
"tableDataEdit",
"tableStructureEdit",
"tableImport",
"dataTransfer",
"sqlFileExecution",
"databaseCreate",
"fieldLineage",
"sqlExplain",
"userAdmin",
"driverManagement",
] as const;
export type DatabaseProductCapability = (typeof DATABASE_PRODUCT_CAPABILITY_KEYS)[number];
export type DatabaseProductCapabilities = Record<DatabaseProductCapability, boolean>;
interface DatabaseDriverManifestEntry {
dbType: DatabaseType;
runtimeMode: DatabaseRuntimeMode;
supportLevel: DatabaseSupportLevel;
capabilities: Partial<DatabaseProductCapabilities>;
}
const SUPPORT_LEVEL_DEFAULTS: Record<DatabaseSupportLevel, DatabaseProductCapabilities> = {
connect: productCapabilities({
queryExecution: true,
sqlFileExecution: true,
}),
browse: productCapabilities({
metadataBrowse: true,
objectBrowser: true,
sqlFileExecution: true,
}),
understand: productCapabilities({
metadataBrowse: true,
objectBrowser: true,
schemaSearch: true,
sqlFileExecution: true,
}),
operate: productCapabilities({
metadataBrowse: true,
objectBrowser: true,
schemaSearch: true,
tableDataEdit: true,
sqlFileExecution: true,
}),
};
const DEFAULT_CAPABILITIES = productCapabilities({});
const DATABASE_DRIVER_ENTRIES = (driverManifest.drivers as DatabaseDriverManifestEntry[]).map((entry) => ({
...entry,
capabilities: {
...SUPPORT_LEVEL_DEFAULTS[entry.supportLevel],
...entry.capabilities,
},
}));
const DATABASE_DRIVER_BY_TYPE = new Map<DatabaseType, DatabaseDriverManifestEntry>(DATABASE_DRIVER_ENTRIES.map((entry) => [entry.dbType, entry]));
export function databaseSupportLevel(dbType?: DatabaseType): DatabaseSupportLevel | undefined {
return dbType ? DATABASE_DRIVER_BY_TYPE.get(dbType)?.supportLevel : undefined;
}
export function databaseRuntimeMode(dbType?: DatabaseType): DatabaseRuntimeMode | undefined {
return dbType ? DATABASE_DRIVER_BY_TYPE.get(dbType)?.runtimeMode : undefined;
}
export function databaseProductCapabilities(dbType?: DatabaseType): DatabaseProductCapabilities {
if (!dbType) return DEFAULT_CAPABILITIES;
return (DATABASE_DRIVER_BY_TYPE.get(dbType)?.capabilities ?? DEFAULT_CAPABILITIES) as DatabaseProductCapabilities;
}
export function supportsDatabaseFeature(dbType: DatabaseType | undefined, capability: DatabaseProductCapability): boolean {
return databaseProductCapabilities(dbType)[capability];
}
export function manifestDatabaseTypes(): DatabaseType[] {
return DATABASE_DRIVER_ENTRIES.map((entry) => entry.dbType);
}
export function usesAgentCursorForQuery(dbType?: DatabaseType): boolean {
const runtimeMode = databaseRuntimeMode(dbType);
return runtimeMode === "agent" || runtimeMode === "external";
}
function productCapabilities(overrides: Partial<DatabaseProductCapabilities>): DatabaseProductCapabilities {
const capabilities = Object.fromEntries(DATABASE_PRODUCT_CAPABILITY_KEYS.map((key) => [key, false])) as DatabaseProductCapabilities;
return { ...capabilities, ...overrides };
}
@@ -0,0 +1,100 @@
import type { DatabaseType, TreeNodeType } from "@/types/database";
import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest";
import { canEditTableStructure } from "@/lib/table/tableStructureCapabilities";
import { DATABASE_OBJECT_TREE_TYPES, FETCH_FIRST_TYPES, PG_LIKE_STRUCTURE_TYPES, SCHEMA_AWARE_TYPES, SINGLE_DATABASE_TYPES, TREE_SCHEMA_TYPES } from "@/lib/database/databaseCapabilitySets";
export function isSchemaAware(dbType?: DatabaseType): boolean {
return !!dbType && SCHEMA_AWARE_TYPES.has(dbType);
}
export function usesTreeSchemaMode(dbType?: DatabaseType): boolean {
return !!dbType && TREE_SCHEMA_TYPES.has(dbType);
}
export function usesDatabaseObjectTreeMode(dbType?: DatabaseType): boolean {
return !!dbType && DATABASE_OBJECT_TREE_TYPES.has(dbType);
}
export function databaseObjectTreeQuerySchema(dbType: DatabaseType | undefined, database: string, schema?: string): string {
if (usesDatabaseObjectTreeMode(dbType)) return "";
return schema || database;
}
export function databaseObjectTreeNodeSchema(dbType: DatabaseType | undefined, database: string, schema?: string): string | undefined {
if (usesDatabaseObjectTreeMode(dbType)) return undefined;
if (schema) return schema;
return isSchemaAware(dbType) ? database : undefined;
}
export function isSingleDatabase(dbType?: DatabaseType): boolean {
return !!dbType && SINGLE_DATABASE_TYPES.has(dbType);
}
export function usesFetchFirst(dbType?: DatabaseType): boolean {
return !!dbType && FETCH_FIRST_TYPES.has(dbType);
}
export function supportsSqlFileExecution(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "sqlFileExecution");
}
export function supportsSchemaDiagram(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "diagram");
}
export function supportsDatabaseSearch(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "schemaSearch");
}
export function supportsTableImport(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "tableImport");
}
export function supportsTableStructureEditing(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "tableStructureEdit") && canEditTableStructure(dbType);
}
export function supportsDatabaseCreation(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "databaseCreate");
}
export function supportsFieldLineage(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "fieldLineage");
}
export function supportsTransfer(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "dataTransfer");
}
export function supportsDriverManagement(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "driverManagement");
}
export function supportsObjectBrowser(dbType?: DatabaseType): boolean {
return supportsDatabaseFeature(dbType, "objectBrowser");
}
export function supportsObjectBrowserTreeNode(dbType: DatabaseType | undefined, nodeType: TreeNodeType): boolean {
if (!supportsObjectBrowser(dbType)) return false;
if (nodeType === "database" && usesDatabaseObjectTreeMode(dbType)) return true;
if (nodeType === "database" && isSchemaAware(dbType) && dbType !== "sqlserver") return false;
return nodeType === "database" || nodeType === "schema" || nodeType === "object-browser";
}
export function supportsTableTruncate(dbType?: DatabaseType): boolean {
return !!dbType && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "manticoresearch";
}
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
}
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql"];
/**
* Returns true if the given database type supports explicit transaction control
* (i.e. toggling between auto-commit and manual transaction mode via BEGIN/COMMIT).
*/
export function supportsTransaction(dbType?: string): boolean {
return !!dbType && TRANSACTION_SUPPORTED_TYPES.includes(dbType);
}
@@ -0,0 +1,18 @@
import * as api from "@/lib/backend/api";
export const SQLITE_DATABASE_FILE_EXTENSIONS = ["db", "db3", "sqlite", "sqlite3", "sqlitedb"];
const SQLITE_DATABASE_EXTENSION_SET = new Set(SQLITE_DATABASE_FILE_EXTENSIONS.map((extension) => `.${extension}`));
export function databaseTypeFromKnownExtension(path: string): "sqlite" | "duckdb" | null {
const lower = path.toLowerCase();
if (lower.endsWith(".duckdb")) return "duckdb";
if ([...SQLITE_DATABASE_EXTENSION_SET].some((extension) => lower.endsWith(extension))) return "sqlite";
return null;
}
export async function detectDatabaseFileType(path: string): Promise<"sqlite" | "duckdb" | null> {
const knownType = databaseTypeFromKnownExtension(path);
if (knownType) return knownType;
return (await api.isSqliteDatabaseFile(path).catch(() => false)) ? "sqlite" : null;
}
@@ -0,0 +1,82 @@
import type { DatabaseType } from "@/types/database";
export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
export interface DatabaseObjectCapabilities {
sidebarObjects: SidebarObjectKind[];
sourceReadable: SidebarObjectKind[];
executable: SidebarObjectKind[];
}
const TABLE_VIEW_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW"];
const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"];
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"];
const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION"];
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
// postgres
["postgres", POSTGRES_OBJECTS],
["gaussdb", POSTGRES_OBJECTS],
["kwdb", POSTGRES_OBJECTS],
["opengauss", POSTGRES_OBJECTS],
// postgres like
["kingbase", POSTGRES_LIKE_OBJECTS],
["highgo", POSTGRES_LIKE_OBJECTS],
["vastbase", POSTGRES_LIKE_OBJECTS],
["redshift", POSTGRES_LIKE_OBJECTS],
// oracle
["oracle", ORACLE_OBJECTS],
["dameng", ORACLE_OBJECTS],
["oceanbase-oracle", ORACLE_OBJECTS],
// table and view
["sqlite", TABLE_VIEW_OBJECTS],
["rqlite", TABLE_VIEW_OBJECTS],
["turso", TABLE_VIEW_OBJECTS],
["duckdb", TABLE_VIEW_OBJECTS],
["clickhouse", TABLE_VIEW_OBJECTS],
["doris", TABLE_VIEW_OBJECTS],
["starrocks", TABLE_VIEW_OBJECTS],
["hive", TABLE_VIEW_OBJECTS],
["trino", TABLE_VIEW_OBJECTS],
["prestosql", TABLE_VIEW_OBJECTS],
["cassandra", TABLE_VIEW_OBJECTS],
["bigquery", TABLE_VIEW_OBJECTS],
["kylin", TABLE_VIEW_OBJECTS],
["tdengine", TABLE_VIEW_OBJECTS],
["iotdb", TABLE_VIEW_OBJECTS],
["neo4j", TABLE_VIEW_OBJECTS],
// others
["influxdb", ["TABLE"]],
["questdb", ["TABLE", "VIEW", "MATERIALIZED_VIEW"]],
["manticoresearch", ["TABLE", "FUNCTION"]],
["databend", ["TABLE", "VIEW", "PROCEDURE"]],
]);
export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjectCapabilities {
const sidebarObjects = sidebarObjectKindsForDatabase(dbType);
return {
sidebarObjects,
sourceReadable: sidebarObjects.filter((kind) => kind !== "TABLE"),
executable: sidebarObjects.filter((kind) => kind === "PROCEDURE"),
};
}
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObjectKind[] {
if (!dbType) return [...TABLE_VIEW_OBJECTS];
return DATABASE_TYPE_OBJECTS.get(dbType) ?? [...ROUTINE_OBJECTS];
}
export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
const value = type.toUpperCase();
const normalized = value.replace(/[\s-]+/g, "_");
if (normalized.includes("PACKAGE_BODY")) return "PACKAGE_BODY";
if (normalized.includes("PACKAGE")) return "PACKAGE";
if (normalized.includes("MATERIALIZED_VIEW")) return "MATERIALIZED_VIEW";
if (value.includes("VIEW")) return "VIEW";
if (value.includes("SEQ")) return "SEQUENCE";
if (value.includes("PROC")) return "PROCEDURE";
if (value.includes("FUNC")) return "FUNCTION";
return "TABLE";
}
@@ -0,0 +1,10 @@
export function filterDatabaseOptions(options: string[], query: string, displayName: (option: string) => string = (option) => option): string[] {
const normalizedQuery = query.trim().toLowerCase();
if (!normalizedQuery) return options;
return options.filter((option) => {
const raw = option.toLowerCase();
const label = displayName(option).toLowerCase();
return raw.includes(normalizedQuery) || label.includes(normalizedQuery);
});
}
@@ -0,0 +1,92 @@
import type { ColumnInfo, DatabaseType } from "@/types/database.ts";
import * as api from "@/lib/backend/api.ts";
export interface DatabaseSearchSqlOptions {
databaseType?: DatabaseType;
schema?: string;
tableName: string;
columns: ColumnInfo[];
term: string;
limit?: number;
}
export interface DatabaseSearchSql {
sql: string;
searchableColumns: string[];
}
export interface SearchResultWhereOptions {
databaseType?: DatabaseType;
columns: ColumnInfo[];
resultColumns: string[];
row: unknown[];
matchedColumns?: string[];
}
const TEXT_TYPES = ["char", "text", "clob", "varchar", "nvarchar", "nchar", "uuid", "uniqueidentifier", "enum"];
const NUMBER_TYPES = ["int", "serial", "number", "numeric", "decimal", "float", "double", "real", "money"];
const SKIPPED_TYPES = ["blob", "binary", "bytea", "image", "geometry", "geography"];
function normalizedType(column: ColumnInfo): string {
return column.data_type.toLowerCase();
}
export function isTextSearchColumn(column: ColumnInfo): boolean {
const type = normalizedType(column);
if (SKIPPED_TYPES.some((skipped) => type.includes(skipped))) return false;
return TEXT_TYPES.some((textType) => type.includes(textType));
}
export function isNumericSearchColumn(column: ColumnInfo): boolean {
const type = normalizedType(column);
if (SKIPPED_TYPES.some((skipped) => type.includes(skipped))) return false;
return NUMBER_TYPES.some((numberType) => type.includes(numberType));
}
function cleanTerm(term: string): string {
return term.trim();
}
function parseNumericTerm(term: string): string | null {
const trimmed = cleanTerm(term);
if (!/^[+-]?(?:\d+|\d+\.\d+|\.\d+)$/.test(trimmed)) return null;
return trimmed;
}
export async function buildDatabaseSearchSql(options: DatabaseSearchSqlOptions): Promise<DatabaseSearchSql | null> {
return api.buildDatabaseSearchSql(options);
}
function columnByName(columns: ColumnInfo[], name: string): ColumnInfo | undefined {
return columns.find((column) => column.name.toLowerCase() === name.toLowerCase());
}
export function findMatchedSearchColumns(resultColumns: string[], row: unknown[], columns: ColumnInfo[], term: string): string[] {
const query = cleanTerm(term).toLowerCase();
const numericTerm = parseNumericTerm(term);
if (!query) return [];
const matches: string[] = [];
resultColumns.forEach((columnName, index) => {
const value = row[index];
if (value === null || value === undefined) return;
const column = columnByName(columns, columnName);
if (!column || (!isTextSearchColumn(column) && !isNumericSearchColumn(column))) return;
if (isNumericSearchColumn(column) && numericTerm && String(value).trim() === numericTerm) {
matches.push(columnName);
return;
}
if (isTextSearchColumn(column) && String(value).toLowerCase().includes(query)) {
matches.push(columnName);
}
});
return matches;
}
export async function buildSearchResultWhere(options: SearchResultWhereOptions): Promise<string> {
return api.buildSearchResultWhere(options);
}
@@ -0,0 +1,194 @@
import type { DatabaseType } from "@/types/database";
import { isSchemaAware, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
export type SyntheticEditKey = "oracle-rowid" | "neo4j-element-id";
export interface TableDataCapability {
insert: boolean;
updateRequiresPrimaryKey: boolean;
deleteRequiresPrimaryKey: boolean;
keylessRowPredicate?: boolean;
requiresTransactionalTableForExistingRows: boolean;
existingRowsReadonly?: boolean;
transaction: boolean;
readonly?: boolean;
}
export interface DatabaseCapability {
schemaAware: boolean;
treeSchemaMode: boolean;
tableData: TableDataCapability;
syntheticKey?: SyntheticEditKey;
}
const DEFAULT_TABLE_DATA_CAPABILITY: TableDataCapability = {
insert: false,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
keylessRowPredicate: false,
requiresTransactionalTableForExistingRows: false,
transaction: true,
};
const NAVICAT_STYLE_TABLE_DATA_CAPABILITY: TableDataCapability = {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
};
const DEFAULT_CAPABILITY: DatabaseCapability = {
schemaAware: false,
treeSchemaMode: false,
tableData: DEFAULT_TABLE_DATA_CAPABILITY,
};
const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set<DatabaseType>([
"mysql",
"manticoresearch",
"postgres",
"sqlite",
"rqlite",
"turso",
"duckdb",
"sqlserver",
"oracle",
"doris",
"starrocks",
"redshift",
"dameng",
"gaussdb",
"kwdb",
"kingbase",
"highgo",
"vastbase",
"goldendb",
"yashandb",
"databricks",
"saphana",
"teradata",
"vertica",
"firebird",
"exasol",
"opengauss",
"questdb",
"oceanbase-oracle",
"gbase",
"access",
"h2",
"snowflake",
"db2",
"informix",
"bigquery",
"sundb",
"oscar",
"databend",
]);
const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<DatabaseCapability>>> = {
hive: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: true,
transaction: false,
},
},
jdbc: {
tableData: {
insert: false,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
manticoresearch: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
neo4j: {
syntheticKey: "neo4j-element-id",
},
oracle: {
syntheticKey: "oracle-rowid",
},
trino: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
prestosql: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
clickhouse: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
tdengine: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
influxdb: {
tableData: {
insert: false,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: true,
keylessRowPredicate: false,
requiresTransactionalTableForExistingRows: false,
existingRowsReadonly: true,
transaction: false,
readonly: true,
},
},
};
function defaultTableDataCapability(dbType?: DatabaseType): TableDataCapability {
if (dbType && NAVICAT_STYLE_TABLE_DATA_TYPES.has(dbType)) return NAVICAT_STYLE_TABLE_DATA_CAPABILITY;
return DEFAULT_TABLE_DATA_CAPABILITY;
}
export function getDatabaseCapability(dbType?: DatabaseType): DatabaseCapability {
const override = dbType ? DATABASE_CAPABILITY_OVERRIDES[dbType] : undefined;
const tableData = defaultTableDataCapability(dbType);
return {
...DEFAULT_CAPABILITY,
...override,
schemaAware: isSchemaAware(dbType),
treeSchemaMode: usesTreeSchemaMode(dbType),
tableData: {
...tableData,
...override?.tableData,
},
};
}
@@ -0,0 +1,85 @@
import type { ConnectionConfig, DatabaseInfo, TreeNode } from "@/types/database";
import { DEFAULT_DATABASE_TREE_LABEL } from "@/lib/sidebar/treeNodeContext";
const sidebarNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
export function shouldIncludeDefaultDatabaseNode(connection: Pick<ConnectionConfig, "db_type"> | undefined, databases: DatabaseInfo[]): boolean {
return connection?.db_type === "mysql" && databases.some((database) => !database.name.trim());
}
export function sortSidebarNames(names: readonly string[]): string[] {
return [...names].sort((left, right) => sidebarNameCollator.compare(left, right));
}
export function sortSidebarDatabases(databases: readonly DatabaseInfo[]): DatabaseInfo[] {
return [...databases].sort((left, right) => sidebarNameCollator.compare(left.name, right.name));
}
export function buildDatabaseTreeNodes(connectionId: string, databases: DatabaseInfo[], options: { includeDefaultWhenEmpty?: boolean } = {}): TreeNode[] {
const nodes = sortSidebarDatabases(databases).flatMap((db) => {
const name = db.name.trim();
if (!name) return [];
return [
{
id: `${connectionId}:${name}`,
label: name,
type: "database" as const,
connectionId,
database: name,
isExpanded: false,
children: [],
},
];
});
if (nodes.length > 0 || !options.includeDefaultWhenEmpty) return nodes;
return [
{
id: `${connectionId}:`,
label: DEFAULT_DATABASE_TREE_LABEL,
type: "database" as const,
connectionId,
database: "",
isExpanded: false,
children: [],
},
];
}
export function buildDuckDbConnectionTreeNodes(connectionId: string, databases: DatabaseInfo[], primarySchemas: string[]): TreeNode[] {
const schemaNodes = sortSidebarNames(primarySchemas).flatMap((schema) => {
const name = schema.trim();
if (!name) return [];
return [
{
id: `${connectionId}:main:${name}`,
label: name,
type: "schema" as const,
connectionId,
database: "main",
schema: name,
isExpanded: false,
children: [],
},
];
});
const attachedCatalogNodes = sortSidebarDatabases(databases).flatMap((db) => {
const name = db.name.trim();
if (!name || name === "main") return [];
return [
{
id: `${connectionId}:${name}`,
label: name,
type: "database" as const,
connectionId,
database: name,
isExpanded: false,
children: [],
},
];
});
return [...schemaNodes, ...attachedCatalogNodes];
}
@@ -0,0 +1,375 @@
import type { DatabaseType, QueryResult } from "@/types/database";
import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest";
export type UserAdminDialect = "mysql" | "postgres";
export type PrivilegeScope = "mysql" | "database" | "schema" | "table" | "role";
export interface DatabaseUserIdentity {
user: string;
host: string;
plugin?: string;
}
export interface CreatePrincipalInput extends DatabaseUserIdentity {
password: string;
canLogin?: boolean;
}
export interface PrivilegeChangeInput {
user: DatabaseUserIdentity;
privileges: string[];
database: string;
table?: string;
grantOption?: boolean;
scope?: PrivilegeScope;
role?: string;
}
export interface DatabaseUserAdminProvider {
dialect: UserAdminDialect;
defaultScope: PrivilegeScope;
listUsersSql(): string;
fallbackListUsersSql?: () => string;
parseUsers(result: QueryResult): DatabaseUserIdentity[];
parseFallbackUsers?: (result: QueryResult) => DatabaseUserIdentity[];
showGrantsSql(user: DatabaseUserIdentity): string;
createUserSql(input: CreatePrincipalInput): string;
alterPasswordSql(user: DatabaseUserIdentity, password: string): string;
alterLoginSql(user: DatabaseUserIdentity, enabled: boolean): string;
dropUserSql(user: DatabaseUserIdentity): string;
grantPrivilegesSql(input: PrivilegeChangeInput): string;
revokePrivilegesSql(input: PrivilegeChangeInput): string;
label(user: DatabaseUserIdentity): string;
detail(user: DatabaseUserIdentity): string | undefined;
privilegesForScope(scope: PrivilegeScope): readonly string[];
defaultPrivilegesForScope(scope: PrivilegeScope): string[];
}
export const MYSQL_USER_ADMIN_TYPES = new Set<DatabaseType>(["mysql", "goldendb"]);
export const POSTGRES_USER_ADMIN_TYPES = new Set<DatabaseType>(["postgres", "gaussdb", "highgo", "kingbase", "kwdb", "opengauss", "questdb", "vastbase"]);
export const MYSQL_COMMON_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "INDEX", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER", "EVENT", "CREATE TEMPORARY TABLES"] as const;
export const POSTGRES_DATABASE_PRIVILEGES = ["CONNECT", "CREATE", "TEMPORARY"] as const;
export const POSTGRES_SCHEMA_PRIVILEGES = ["USAGE", "CREATE"] as const;
export const POSTGRES_TABLE_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"] as const;
export function supportsDatabaseUserAdmin(dbType: DatabaseType | undefined): boolean {
return !!dbType && supportsDatabaseFeature(dbType, "userAdmin") && !!getDatabaseUserAdminProvider(dbType);
}
export function getDatabaseUserAdminProvider(dbType: DatabaseType | undefined): DatabaseUserAdminProvider | null {
if (!dbType) return null;
if (MYSQL_USER_ADMIN_TYPES.has(dbType)) return mysqlUserAdminProvider;
if (POSTGRES_USER_ADMIN_TYPES.has(dbType)) return postgresUserAdminProvider;
return null;
}
export function quoteSqlString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
export function quoteMySqlString(value: string): string {
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
}
export function quoteMySqlIdentifier(value: string): string {
return `\`${value.replace(/`/g, "``")}\``;
}
export function quotePostgresIdentifier(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
export function mysqlUserAccount(user: DatabaseUserIdentity): string {
return `${quoteMySqlString(user.user)}@${quoteMySqlString(user.host)}`;
}
export function mysqlUserLabel(user: DatabaseUserIdentity): string {
return `${user.user}@${user.host}`;
}
export function postgresRoleLabel(user: DatabaseUserIdentity): string {
return user.user;
}
export function mysqlListUsersSql(): string {
return "SELECT User AS user, Host AS host, plugin AS plugin FROM mysql.user ORDER BY User, Host;";
}
export function mysqlListUsersFallbackSql(): string {
return "SELECT DISTINCT GRANTEE AS grantee FROM information_schema.USER_PRIVILEGES ORDER BY GRANTEE;";
}
export function mysqlShowGrantsSql(user: DatabaseUserIdentity): string {
return `SHOW GRANTS FOR ${mysqlUserAccount(user)};`;
}
export function mysqlCreateUserSql(input: CreatePrincipalInput): string {
return `CREATE USER ${mysqlUserAccount(input)} IDENTIFIED BY ${quoteMySqlString(input.password)};`;
}
export function mysqlAlterUserPasswordSql(user: DatabaseUserIdentity, password: string): string {
return `ALTER USER ${mysqlUserAccount(user)} IDENTIFIED BY ${quoteMySqlString(password)};`;
}
export function mysqlAlterUserAccountLockSql(user: DatabaseUserIdentity, locked: boolean): string {
return `ALTER USER ${mysqlUserAccount(user)} ACCOUNT ${locked ? "LOCK" : "UNLOCK"};`;
}
export function mysqlDropUserSql(user: DatabaseUserIdentity): string {
return `DROP USER ${mysqlUserAccount(user)};`;
}
export function mysqlPrivilegeTargetSql(database: string, table = "*"): string {
const db = database.trim() || "*";
const tbl = table.trim() || "*";
const dbSql = db === "*" ? "*" : quoteMySqlIdentifier(db);
const tableSql = tbl === "*" ? "*" : quoteMySqlIdentifier(tbl);
return `${dbSql}.${tableSql}`;
}
export function mysqlGrantPrivilegesSql(input: PrivilegeChangeInput): string {
const privileges = normalizePrivileges(input.privileges).join(", ");
const grantOption = input.grantOption ? " WITH GRANT OPTION" : "";
return `GRANT ${privileges} ON ${mysqlPrivilegeTargetSql(input.database, input.table)} TO ${mysqlUserAccount(input.user)}${grantOption};`;
}
export function mysqlRevokePrivilegesSql(input: PrivilegeChangeInput): string {
const privileges = normalizePrivileges(input.privileges).join(", ");
return `REVOKE ${privileges} ON ${mysqlPrivilegeTargetSql(input.database, input.table)} FROM ${mysqlUserAccount(input.user)};`;
}
export function normalizePrivileges(privileges: string[], fallback = "SELECT"): string[] {
const normalized = privileges.map((privilege) => privilege.trim().toUpperCase()).filter(Boolean);
return Array.from(new Set(normalized.length > 0 ? normalized : [fallback]));
}
export const normalizeMySqlPrivileges = normalizePrivileges;
export function usersFromMySqlUserResult(result: QueryResult): DatabaseUserIdentity[] {
const userIndex = columnIndex(result, "user", "User");
const hostIndex = columnIndex(result, "host", "Host");
const pluginIndex = columnIndex(result, "plugin", "Plugin");
if (userIndex < 0 || hostIndex < 0) return [];
return result.rows
.map((row) => ({
user: String(row[userIndex] ?? ""),
host: String(row[hostIndex] ?? ""),
plugin: pluginIndex >= 0 && row[pluginIndex] != null ? String(row[pluginIndex]) : undefined,
}))
.filter((user) => user.user || user.host);
}
export function usersFromMySqlGranteeResult(result: QueryResult): DatabaseUserIdentity[] {
const granteeIndex = columnIndex(result, "grantee", "GRANTEE");
if (granteeIndex < 0) return [];
return result.rows.flatMap((row) => {
const parsed = parseMySqlGrantee(String(row[granteeIndex] ?? ""));
return parsed ? [parsed] : [];
});
}
export function postgresListRolesSql(): string {
const bypassRls = postgresRoleBypassRlsSql("r");
return `
SELECT
r.rolname AS user,
CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS host,
concat_ws(', ',
CASE WHEN r.rolsuper THEN 'SUPERUSER' END,
CASE WHEN r.rolcreatedb THEN 'CREATEDB' END,
CASE WHEN r.rolcreaterole THEN 'CREATEROLE' END,
CASE WHEN r.rolreplication THEN 'REPLICATION' END,
CASE WHEN ${bypassRls} THEN 'BYPASSRLS' END
) AS plugin
FROM pg_catalog.pg_roles r
ORDER BY r.rolname;`.trim();
}
export function usersFromPostgresRolesResult(result: QueryResult): DatabaseUserIdentity[] {
return usersFromMySqlUserResult(result);
}
export function postgresShowGrantsSql(user: DatabaseUserIdentity): string {
const role = quoteSqlString(user.user);
const bypassRls = postgresRoleBypassRlsSql("r");
return `
WITH target AS (
SELECT oid, rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin, rolreplication, ${bypassRls} AS rolbypassrls
FROM pg_catalog.pg_roles r
WHERE r.rolname = ${role}
)
SELECT line
FROM (
SELECT 1 AS sort, 'Role: ' || quote_ident(rolname) AS line
FROM target
UNION ALL
SELECT 2, 'Attributes: ' || COALESCE(NULLIF(concat_ws(', ',
CASE WHEN rolsuper THEN 'SUPERUSER' END,
CASE WHEN rolcreatedb THEN 'CREATEDB' END,
CASE WHEN rolcreaterole THEN 'CREATEROLE' END,
CASE WHEN rolcanlogin THEN 'LOGIN' ELSE 'NOLOGIN' END,
CASE WHEN rolreplication THEN 'REPLICATION' END,
CASE WHEN rolbypassrls THEN 'BYPASSRLS' END
), ''), 'none')
FROM target
UNION ALL
SELECT 10, 'Member of: ' || quote_ident(parent.rolname) || CASE WHEN m.admin_option THEN ' WITH ADMIN OPTION' ELSE '' END
FROM pg_catalog.pg_auth_members m
JOIN target t ON t.oid = m.member
JOIN pg_catalog.pg_roles parent ON parent.oid = m.roleid
UNION ALL
SELECT 20, 'Has member: ' || quote_ident(member.rolname) || CASE WHEN m.admin_option THEN ' WITH ADMIN OPTION' ELSE '' END
FROM pg_catalog.pg_auth_members m
JOIN target t ON t.oid = m.roleid
JOIN pg_catalog.pg_roles member ON member.oid = m.member
UNION ALL
SELECT 30, 'Database: ' || quote_ident(d.datname) || ' = ' ||
concat_ws(', ',
CASE WHEN has_database_privilege(t.rolname, d.oid, 'CONNECT') THEN 'CONNECT' END,
CASE WHEN has_database_privilege(t.rolname, d.oid, 'CREATE') THEN 'CREATE' END,
CASE WHEN has_database_privilege(t.rolname, d.oid, 'TEMPORARY') THEN 'TEMPORARY' END
)
FROM target t
CROSS JOIN pg_catalog.pg_database d
WHERE has_database_privilege(t.rolname, d.oid, 'CONNECT')
OR has_database_privilege(t.rolname, d.oid, 'CREATE')
OR has_database_privilege(t.rolname, d.oid, 'TEMPORARY')
UNION ALL
SELECT 40, 'Schema: ' || quote_ident(n.nspname) || ' = ' ||
concat_ws(', ',
CASE WHEN has_schema_privilege(t.rolname, n.oid, 'USAGE') THEN 'USAGE' END,
CASE WHEN has_schema_privilege(t.rolname, n.oid, 'CREATE') THEN 'CREATE' END
)
FROM target t
CROSS JOIN pg_catalog.pg_namespace n
WHERE n.nspname NOT LIKE 'pg~_%' ESCAPE '~'
AND n.nspname <> 'information_schema'
AND (has_schema_privilege(t.rolname, n.oid, 'USAGE') OR has_schema_privilege(t.rolname, n.oid, 'CREATE'))
UNION ALL
SELECT 50, 'Table: ' || quote_ident(table_schema) || '.' || quote_ident(table_name) || ' = ' ||
string_agg(privilege_type || CASE WHEN is_grantable = 'YES' THEN ' WITH GRANT OPTION' ELSE '' END, ', ' ORDER BY privilege_type)
FROM information_schema.role_table_grants
WHERE grantee = ${role}
GROUP BY table_schema, table_name
) grants
ORDER BY sort, line;`.trim();
}
export function postgresCreateRoleSql(input: CreatePrincipalInput): string {
const login = input.canLogin === false ? "NOLOGIN" : "LOGIN";
return `CREATE ROLE ${quotePostgresIdentifier(input.user)} ${login} PASSWORD ${quoteSqlString(input.password)};`;
}
export function postgresAlterRolePasswordSql(user: DatabaseUserIdentity, password: string): string {
return `ALTER ROLE ${quotePostgresIdentifier(user.user)} PASSWORD ${quoteSqlString(password)};`;
}
export function postgresAlterRoleLoginSql(user: DatabaseUserIdentity, enabled: boolean): string {
return `ALTER ROLE ${quotePostgresIdentifier(user.user)} ${enabled ? "LOGIN" : "NOLOGIN"};`;
}
export function postgresDropRoleSql(user: DatabaseUserIdentity): string {
return `DROP ROLE ${quotePostgresIdentifier(user.user)};`;
}
export function postgresPrivilegeTargetSql(input: Pick<PrivilegeChangeInput, "scope" | "database" | "table">): string {
const scope = input.scope || "database";
const name = input.database.trim();
if (scope === "database") return `DATABASE ${quotePostgresIdentifier(name || "postgres")}`;
if (scope === "schema") return `SCHEMA ${quotePostgresIdentifier(name || "public")}`;
const schema = name || "public";
const table = input.table?.trim() || "*";
if (table === "*") return `ALL TABLES IN SCHEMA ${quotePostgresIdentifier(schema)}`;
return `TABLE ${quotePostgresIdentifier(schema)}.${quotePostgresIdentifier(table)}`;
}
export function postgresGrantPrivilegesSql(input: PrivilegeChangeInput): string {
if (input.scope === "role") {
return `GRANT ${quotePostgresIdentifier(input.role?.trim() || "")} TO ${quotePostgresIdentifier(input.user.user)}${input.grantOption ? " WITH ADMIN OPTION" : ""};`;
}
const privileges = normalizePrivileges(input.privileges, postgresDefaultPrivilege(input.scope)).join(", ");
return `GRANT ${privileges} ON ${postgresPrivilegeTargetSql(input)} TO ${quotePostgresIdentifier(input.user.user)}${input.grantOption ? " WITH GRANT OPTION" : ""};`;
}
export function postgresRevokePrivilegesSql(input: PrivilegeChangeInput): string {
if (input.scope === "role") {
return `REVOKE ${quotePostgresIdentifier(input.role?.trim() || "")} FROM ${quotePostgresIdentifier(input.user.user)};`;
}
const privileges = normalizePrivileges(input.privileges, postgresDefaultPrivilege(input.scope)).join(", ");
return `REVOKE ${privileges} ON ${postgresPrivilegeTargetSql(input)} FROM ${quotePostgresIdentifier(input.user.user)};`;
}
export function grantsFromQueryResult(result: QueryResult): string[] {
return result.rows.map((row) => String(row[0] ?? "")).filter(Boolean);
}
function columnIndex(result: QueryResult, ...names: string[]): number {
const wanted = new Set(names.map((name) => name.toLowerCase()));
return result.columns.findIndex((column) => wanted.has(column.toLowerCase()));
}
function parseMySqlGrantee(value: string): DatabaseUserIdentity | null {
const match = /^'((?:''|[^'])*)'@'((?:''|[^'])*)'$/.exec(value.trim());
if (!match) return null;
return {
user: match[1].replace(/''/g, "'"),
host: match[2].replace(/''/g, "'"),
};
}
function postgresDefaultPrivilege(scope: PrivilegeScope | undefined): string {
if (scope === "schema") return "USAGE";
if (scope === "database") return "CONNECT";
return "SELECT";
}
function postgresPrivilegesForScope(scope: PrivilegeScope): readonly string[] {
if (scope === "database") return POSTGRES_DATABASE_PRIVILEGES;
if (scope === "schema") return POSTGRES_SCHEMA_PRIVILEGES;
if (scope === "table") return POSTGRES_TABLE_PRIVILEGES;
return [];
}
function postgresRoleBypassRlsSql(alias: string): string {
return `row_to_json(${alias})::text LIKE '%"rolbypassrls":true%'`;
}
export const mysqlUserAdminProvider: DatabaseUserAdminProvider = {
dialect: "mysql",
defaultScope: "mysql",
listUsersSql: mysqlListUsersSql,
fallbackListUsersSql: mysqlListUsersFallbackSql,
parseUsers: usersFromMySqlUserResult,
parseFallbackUsers: usersFromMySqlGranteeResult,
showGrantsSql: mysqlShowGrantsSql,
createUserSql: mysqlCreateUserSql,
alterPasswordSql: mysqlAlterUserPasswordSql,
alterLoginSql: (user, enabled) => mysqlAlterUserAccountLockSql(user, !enabled),
dropUserSql: mysqlDropUserSql,
grantPrivilegesSql: mysqlGrantPrivilegesSql,
revokePrivilegesSql: mysqlRevokePrivilegesSql,
label: mysqlUserLabel,
detail: (user) => user.plugin,
privilegesForScope: () => MYSQL_COMMON_PRIVILEGES,
defaultPrivilegesForScope: () => ["SELECT"],
};
export const postgresUserAdminProvider: DatabaseUserAdminProvider = {
dialect: "postgres",
defaultScope: "database",
listUsersSql: postgresListRolesSql,
parseUsers: usersFromPostgresRolesResult,
showGrantsSql: postgresShowGrantsSql,
createUserSql: postgresCreateRoleSql,
alterPasswordSql: postgresAlterRolePasswordSql,
alterLoginSql: postgresAlterRoleLoginSql,
dropUserSql: postgresDropRoleSql,
grantPrivilegesSql: postgresGrantPrivilegesSql,
revokePrivilegesSql: postgresRevokePrivilegesSql,
label: postgresRoleLabel,
detail: (user) => [user.host, user.plugin].filter(Boolean).join(" · ") || undefined,
privilegesForScope: postgresPrivilegesForScope,
defaultPrivilegesForScope: (scope) => (scope === "role" ? [] : [postgresDefaultPrivilege(scope)]),
};
+124
View File
@@ -0,0 +1,124 @@
import type { DatabaseObjectType, DatabaseType } from "@/types/database";
import * as api from "@/lib/backend/api";
export interface DropObjectSqlOptions {
databaseType?: DatabaseType;
objectType: DatabaseObjectType;
schema?: string | null;
name: string;
}
export interface TableAdminSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
tableName: string;
}
export type TableChildObjectType = "COLUMN" | "INDEX" | "FOREIGN_KEY" | "TRIGGER";
export interface DropTableChildObjectSqlOptions {
databaseType?: DatabaseType;
objectType: TableChildObjectType;
schema?: string | null;
tableName: string;
name: string;
}
export interface DatabaseNameSqlOptions {
databaseType?: DatabaseType;
name: string;
}
export interface SchemaNameSqlOptions {
databaseType?: DatabaseType;
name: string;
}
export interface SchemaCommentSqlOptions extends SchemaNameSqlOptions {
comment: string;
}
export interface DuplicateTableStructureSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
sourceName: string;
targetName: string;
}
export interface CopyTableDataSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
sourceName: string;
targetName: string;
columns?: string[];
postgresOverridingSystemValue?: boolean;
sqlserverIdentityInsert?: boolean;
}
export function buildDropObjectSql(options: DropObjectSqlOptions): Promise<string> {
return api.buildDropObjectSql(options);
}
export function buildDropTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildDropTableSql(options);
}
export function buildDropTableChildObjectSql(options: DropTableChildObjectSqlOptions): Promise<string> {
return api.buildDropTableChildObjectSql(options);
}
export function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildEmptyTableSql(options);
}
export function buildTruncateTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildTruncateTableSql(options);
}
export function buildDropDatabaseSql(options: DatabaseNameSqlOptions): Promise<string> {
return api.buildDropDatabaseSql(options);
}
export function buildCreateSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return api.buildCreateSchemaSql(options);
}
export function buildDropSchemaSql(options: SchemaNameSqlOptions): Promise<string> {
return api.buildDropSchemaSql(options);
}
export function supportsSchemaComment(databaseType?: DatabaseType): boolean {
return databaseType === "postgres";
}
export function buildGetSchemaCommentSql(options: SchemaNameSqlOptions): string {
if (!supportsSchemaComment(options.databaseType)) {
throw new Error("Schema comments are not supported by this database");
}
return ["SELECT d.description AS comment", "FROM pg_catalog.pg_namespace n", "LEFT JOIN pg_catalog.pg_description d ON d.objoid = n.oid AND d.objsubid = 0 AND d.classoid = 'pg_namespace'::regclass", `WHERE n.nspname = ${quoteSqlLiteral(options.name)};`].join("\n");
}
export function buildSetSchemaCommentSql(options: SchemaCommentSqlOptions): string {
if (!supportsSchemaComment(options.databaseType)) {
throw new Error("Schema comments are not supported by this database");
}
const comment = options.comment.trim();
const literal = comment ? quoteSqlLiteral(comment) : "NULL";
return `COMMENT ON SCHEMA ${quotePostgresIdentifier(options.name)} IS ${literal};`;
}
export function buildDuplicateTableStructureSql(options: DuplicateTableStructureSqlOptions): Promise<string> {
return api.buildDuplicateTableStructureSql(options);
}
export function buildCopyTableDataSql(options: CopyTableDataSqlOptions): Promise<string> {
return api.buildCopyTableDataSql(options);
}
function quotePostgresIdentifier(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
function quoteSqlLiteral(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
@@ -0,0 +1,34 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
export const TREE_SCHEMA_DEFAULT_DATABASE_SELECT_VALUE = "__dbx_tree_schema_default_database__";
export const EMPTY_DATABASE_SELECT_VALUE = "__dbx_empty_database__";
export function resolveDefaultDatabase(connection: Pick<ConnectionConfig, "database">, options: string[]): string {
return connection.database || options[0] || "";
}
export function isTreeSchemaDefaultDatabase(dbType: DatabaseType | undefined, database: string): boolean {
return database === "" && usesTreeSchemaMode(dbType);
}
export function encodeSelectableDatabaseValue(dbType: DatabaseType | undefined, database: string): string {
if (isTreeSchemaDefaultDatabase(dbType, database)) return TREE_SCHEMA_DEFAULT_DATABASE_SELECT_VALUE;
return database === "" ? EMPTY_DATABASE_SELECT_VALUE : database;
}
export function decodeSelectableDatabaseValue(dbType: DatabaseType | undefined, value: string): string {
if (value === TREE_SCHEMA_DEFAULT_DATABASE_SELECT_VALUE && usesTreeSchemaMode(dbType)) return "";
if (value === EMPTY_DATABASE_SELECT_VALUE) return "";
return value;
}
export function formatDatabaseLabel(connection: Pick<ConnectionConfig, "db_type"> | undefined, database: string, labels: { defaultDatabase: string; noDatabase: string }): string {
if (connection?.db_type === "redis" && database !== "") return `db${database}`;
if (isTreeSchemaDefaultDatabase(connection?.db_type, database)) return labels.defaultDatabase;
return database || labels.noDatabase;
}
export function isDefaultDatabase(connection: Pick<ConnectionConfig, "database"> | undefined, database: string): boolean {
return !!connection?.database && !!database && connection.database === database;
}
@@ -0,0 +1,165 @@
import type { ConnectionConfig } from "@/types/database";
export type H2ConnectionMode = "file" | "tcp";
const H2_FILE_PREFIX = "jdbc:h2:file:";
const H2_SPLIT_PREFIX = "jdbc:h2:split:";
const H2_FILE_PREFIXES = [H2_FILE_PREFIX, H2_SPLIT_PREFIX];
const H2_DEFAULT_PORT = 9092;
const H2_PARSED_PROFILE = {
dbType: "h2" as const,
driverProfile: "h2" as const,
driverLabel: "H2" as const,
};
function decodeH2UrlPart(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function extractH2JdbcSemicolonParams(source: string): { username?: string; password?: string; urlParams: string } {
let username: string | undefined;
let password: string | undefined;
const urlParams: string[] = [];
for (const part of source.split(";").slice(1)) {
if (!part) continue;
const [rawKey, ...rest] = part.split("=");
const key = decodeH2UrlPart(rawKey).trim().toLowerCase();
const value = decodeH2UrlPart(rest.join("=")).trim();
if (key === "user") {
username = value;
} else if (key === "password") {
password = value;
} else {
urlParams.push(part);
}
}
return { username, password, urlParams: urlParams.join(";") };
}
function hasH2JdbcSemicolonParam(source: string | undefined, name: string): boolean {
for (const part of (source || "").split(";").slice(1)) {
if (!part) continue;
const [rawKey] = part.split("=");
if (decodeH2UrlPart(rawKey).trim().toLowerCase() === name) return true;
}
return false;
}
export function h2JdbcUrlHasUserParam(source: string | undefined): boolean {
return hasH2JdbcSemicolonParam(source, "user");
}
export function h2JdbcUrlHasPasswordParam(source: string | undefined): boolean {
return hasH2JdbcSemicolonParam(source, "password");
}
function databaseNameFromPath(path: string): string | undefined {
const name = path.split(/[\\/]/).filter(Boolean).pop()?.trim();
return name || undefined;
}
function h2FileJdbcUrlPrefix(value: string): string | undefined {
const lower = value.toLowerCase();
return H2_FILE_PREFIXES.find((prefix) => lower.startsWith(prefix));
}
export function isH2FileJdbcUrl(value: string | undefined | null): boolean {
return h2FileJdbcUrlPrefix((value || "").trim()) !== undefined;
}
export function h2FilePathFromJdbcUrl(value: string | undefined | null): string {
const trimmed = (value || "").trim();
const prefix = h2FileJdbcUrlPrefix(trimmed);
if (!prefix) return "";
const rawPath = trimmed.slice(prefix.length).split(";")[0] || "";
if (prefix === H2_SPLIT_PREFIX) {
return rawPath.replace(/^\d+:/, "");
}
return rawPath;
}
export function isH2SplitJdbcUrl(value: string | undefined | null): value is string {
return (value || "").trim().toLowerCase().startsWith(H2_SPLIT_PREFIX);
}
export function h2FileJdbcUrlWithPath(source: string | undefined | null, path: string): string {
const trimmed = (source || "").trim();
if (!isH2SplitJdbcUrl(trimmed)) return h2FileJdbcUrl(path);
const rest = trimmed.slice(H2_SPLIT_PREFIX.length);
const [rawPath, ...optionParts] = rest.split(";");
const blockPrefix = rawPath.match(/^\d+:/)?.[0] || "";
const options = optionParts.length > 0 ? `;${optionParts.join(";")}` : "";
return `${H2_SPLIT_PREFIX}${blockPrefix}${h2JdbcFileBasePath(path)}${options}`;
}
export function parseH2JdbcUrl(source: string) {
const trimmed = source.trim();
if (!/^jdbc:h2:/i.test(trimmed)) return null;
const credentials = extractH2JdbcSemicolonParams(trimmed);
const filePath = h2FilePathFromJdbcUrl(trimmed);
if (filePath) {
return {
...H2_PARSED_PROFILE,
host: filePath,
port: 0,
username: credentials.username || "sa",
password: credentials.password || "",
database: databaseNameFromPath(filePath),
urlParams: credentials.urlParams,
ssl: false,
connectionString: trimmed,
};
}
const serverMatch = /^jdbc:h2:(?<protocol>tcp|ssl):\/\/(?<host>\[[^\]]+\]|[^:/;]+)(?::(?<port>\d+))?\/(?<database>[^;]*)/i.exec(trimmed);
if (serverMatch?.groups) {
return {
...H2_PARSED_PROFILE,
host: serverMatch.groups.host.replace(/^\[/, "").replace(/\]$/, ""),
port: serverMatch.groups.port ? Number(serverMatch.groups.port) : H2_DEFAULT_PORT,
username: credentials.username || "sa",
password: credentials.password || "",
database: decodeH2UrlPart(serverMatch.groups.database || "") || undefined,
urlParams: credentials.urlParams,
ssl: serverMatch.groups.protocol.toLowerCase() === "ssl",
connectionString: trimmed,
};
}
const database = trimmed.replace(/^jdbc:h2:/i, "").split(";")[0] || undefined;
return {
...H2_PARSED_PROFILE,
host: "",
port: H2_DEFAULT_PORT,
username: credentials.username || "sa",
password: credentials.password || "",
database: database ? decodeH2UrlPart(database) : undefined,
urlParams: credentials.urlParams,
ssl: false,
connectionString: trimmed,
};
}
export function h2JdbcFileBasePath(path: string): string {
const trimmed = path.trim();
if (/\.mv\.db$/i.test(trimmed)) return trimmed.slice(0, -".mv.db".length);
if (/\.h2\.db$/i.test(trimmed)) return trimmed.slice(0, -".h2.db".length);
return trimmed;
}
export function h2FileJdbcUrl(path: string): string {
return `${H2_FILE_PREFIX}${h2JdbcFileBasePath(path)};AUTO_SERVER=TRUE`;
}
export function h2ConnectionModeForConfig(config: Pick<ConnectionConfig, "db_type" | "connection_string" | "port">) {
if (config.db_type !== "h2") return "tcp";
return isH2FileJdbcUrl(config.connection_string) || config.port === 0 ? "file" : "tcp";
}
@@ -0,0 +1,89 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { isSchemaAware, usesDatabaseObjectTreeMode, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
type JdbcDialectConnection = Pick<ConnectionConfig, "db_type"> & Partial<Pick<ConnectionConfig, "driver_profile" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths">>;
const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = [
{ type: "databend", patterns: [/jdbc:databend:/i, /com\.databend\.jdbc\.DatabendDriver/i, /databend-jdbc/i] },
{ type: "starrocks", patterns: [/starrocks/i] },
{ type: "doris", patterns: [/doris/i] },
{ type: "goldendb", patterns: [/jdbc:goldendb:/i, /goldendb/i] },
{ type: "hive", patterns: [/org\.apache\.hive\.jdbc\.HiveDriver/i, /hive-jdbc/i] },
{ type: "mysql", patterns: [/jdbc:mysql:/i, /mysql/i, /mariadb/i, /kyuubi/i, /hive2/i] },
{ type: "postgres", patterns: [/jdbc:postgresql:/i, /postgres/i] },
{ type: "sqlserver", patterns: [/jdbc:sqlserver:/i, /sqlserver/i, /mssql/i] },
{ type: "oracle", patterns: [/jdbc:oracle:/i, /oracle/i] },
{ type: "clickhouse", patterns: [/jdbc:clickhouse:/i, /clickhouse/i] },
{ type: "h2", patterns: [/jdbc:h2:/i, /\bh2\b/i] },
{ type: "sqlite", patterns: [/jdbc:sqlite:/i, /sqlite/i] },
{ type: "db2", patterns: [/jdbc:db2:/i, /\bdb2\b/i] },
{ type: "informix", patterns: [/jdbc:informix/i, /informix/i] },
{ type: "iris", patterns: [/jdbc:(?:iris|cache):/i, /com\.intersystems\.jdbc\.(?:IRIS|Cache)Driver/i, /intersystems-jdbc/i] },
];
export function inferJdbcDialect(connection?: JdbcDialectConnection): DatabaseType | undefined {
if (!connection || connection.db_type !== "jdbc") return undefined;
const haystack = [connection.driver_profile, connection.connection_string, connection.jdbc_driver_class, ...(connection.jdbc_driver_paths ?? [])].filter(Boolean).join("\n");
if (!haystack) return undefined;
return JDBC_DIALECT_MATCHERS.find((matcher) => matcher.patterns.some((pattern) => pattern.test(haystack)))?.type;
}
export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
if (!connection) return undefined;
if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return "informix";
if (connection.db_type === "gbase") return "mysql";
if (connection.db_type !== "jdbc") return connection.db_type;
return inferJdbcDialect(connection) ?? "jdbc";
}
export function tableStructureDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
if (!connection) return undefined;
if (connection.db_type === "gbase" && !isGbase8sProfile(connection.driver_profile)) return "gbase";
return effectiveDatabaseTypeForConnection(connection);
}
export function connectionUsesDatabaseObjectTreeMode(connection?: JdbcDialectConnection): boolean {
if (!connection) return false;
if (connection.db_type !== "jdbc") return usesDatabaseObjectTreeMode(effectiveDatabaseTypeForConnection(connection));
const dialect = inferJdbcDialect(connection);
if (!dialect) return true;
if (dialect === "hive" || dialect === "trino") return false;
if (dialect === "databend") return true;
return !usesTreeSchemaMode(dialect);
}
export function connectionUsesSchemaExecutionContext(connection?: Pick<ConnectionConfig, "db_type" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths">): boolean {
return connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend";
}
export function connectionObjectTreeQuerySchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string {
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connectionUsesDatabaseObjectTreeMode(connection)) return "";
return schema || database;
}
export function metadataSchemaForConnection(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string {
const type = effectiveDatabaseTypeForConnection(connection);
if (type === "sqlserver") return schema || "dbo";
return connectionObjectTreeQuerySchema(connection, database, schema);
}
export function connectionObjectTreeNodeSchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string | undefined {
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connectionUsesDatabaseObjectTreeMode(connection)) return undefined;
if (schema) return schema;
const type = effectiveDatabaseTypeForConnection(connection);
return isSchemaAware(type) ? database : undefined;
}
/** Maps a database type to the corresponding CodeMirror SQL dialect name used by QueryEditor and DdlViewDialog. */
export function codeMirrorSqlDialect(dbType: DatabaseType | undefined): "mysql" | "postgres" | "sqlserver" {
if (dbType === "postgres" || dbType === "gaussdb" || dbType === "kwdb" || dbType === "opengauss") return "postgres";
if (dbType === "sqlserver") return "sqlserver";
return "mysql";
}
function isGbase8sProfile(driverProfile?: string): boolean {
return driverProfile === "gbase8s";
}
@@ -0,0 +1,31 @@
const MYSQL_CLEAR_TEXT_PARAM_ALIASES = new Set(["allowcleartextpasswords", "enable_cleartext_plugin"]);
const MYSQL_CLEAR_TEXT_PARAM = "enable_cleartext_plugin";
function normalizeKey(key: string): string {
return key.trim().toLowerCase();
}
function isTruthyUrlParam(value: string): boolean {
return ["true", "1", "yes", "on"].includes(value.trim().toLowerCase());
}
function isMysqlCleartextPasswordParam(key: string): boolean {
return MYSQL_CLEAR_TEXT_PARAM_ALIASES.has(normalizeKey(key));
}
export function mysqlCleartextPasswordAuthEnabled(params: string | undefined): boolean {
const parsed = new URLSearchParams((params || "").trim().replace(/^\?/, ""));
for (const [key, value] of parsed.entries()) {
if (isMysqlCleartextPasswordParam(key) && isTruthyUrlParam(value)) return true;
}
return false;
}
export function setMysqlCleartextPasswordAuthEnabled(params: string | undefined, enabled: boolean): string {
const parsed = new URLSearchParams((params || "").trim().replace(/^\?/, ""));
for (const key of [...parsed.keys()]) {
if (isMysqlCleartextPasswordParam(key)) parsed.delete(key);
}
if (enabled) parsed.set(MYSQL_CLEAR_TEXT_PARAM, "true");
return parsed.toString();
}
@@ -0,0 +1,26 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
export type OceanbaseSubMode = "mysql" | "oracle";
export function oceanbaseSubModeFromConfig(config: Pick<ConnectionConfig, "db_type" | "driver_profile">): OceanbaseSubMode {
return config.db_type === "oceanbase-oracle" || config.driver_profile === "oceanbase-oracle" ? "oracle" : "mysql";
}
export function oceanbaseModeConnectionPatch(mode: OceanbaseSubMode): {
db_type: DatabaseType;
driver_profile: string;
driver_label: string;
} {
if (mode === "oracle") {
return {
db_type: "oceanbase-oracle",
driver_profile: "oceanbase-oracle",
driver_label: "OceanBase Oracle Mode",
};
}
return {
db_type: "mysql",
driver_profile: "oceanbase",
driver_label: "OceanBase",
};
}
@@ -0,0 +1,31 @@
import type { AgentDriverInfo } from "@/lib/backend/api";
import type { JdbcMavenBundleInfo } from "@/types/database";
export const PRESTOSQL_DRIVER_DB_TYPE = "prestosql";
export const PRESTOSQL_JDBC_DRIVER_VERSION = "350";
export const PRESTOSQL_JDBC_DRIVER_COORDINATE = `io.prestosql:presto-jdbc:${PRESTOSQL_JDBC_DRIVER_VERSION}`;
export const PRESTOSQL_JDBC_DRIVER_REPOSITORY = "https://repo.maven.apache.org/maven2/";
export function prestoSqlMavenBundle(bundles: JdbcMavenBundleInfo[]): JdbcMavenBundleInfo | undefined {
return bundles.find((bundle) => bundle.coordinate === PRESTOSQL_JDBC_DRIVER_COORDINATE);
}
export function prestoSqlBuiltinDriverPaths(bundles: JdbcMavenBundleInfo[]): string[] {
return (prestoSqlMavenBundle(bundles)?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean);
}
export function prestoSqlBuiltinDriverRow(bundles: JdbcMavenBundleInfo[]): AgentDriverInfo {
const installedBundle = prestoSqlMavenBundle(bundles);
return {
db_type: PRESTOSQL_DRIVER_DB_TYPE,
label: "PrestoSQL",
version: PRESTOSQL_JDBC_DRIVER_VERSION,
size: installedBundle ? installedBundle.artifacts.reduce((total, artifact) => total + Number(artifact.size || 0), 0) : 0,
installed: Boolean(installedBundle),
installed_version: installedBundle ? PRESTOSQL_JDBC_DRIVER_VERSION : null,
update_available: false,
requires_java_runtime: true,
jre: "21",
jre_installed: true,
};
}
@@ -0,0 +1,41 @@
import type { TreeNode } from "@/types/database";
const SQLSERVER_LINKED_SCHEMA_PREFIX = "__dbx_sqlserver_linked__:";
export interface SqlServerLinkedSchemaRef {
server: string;
catalog: string;
schema: string;
}
export function encodeSqlServerLinkedSchema(ref: SqlServerLinkedSchemaRef): string {
return `${SQLSERVER_LINKED_SCHEMA_PREFIX}${[ref.server, ref.catalog, ref.schema].map(encodeURIComponent).join("|")}`;
}
export function parseSqlServerLinkedSchema(schema: string | undefined): SqlServerLinkedSchemaRef | null {
if (!schema?.startsWith(SQLSERVER_LINKED_SCHEMA_PREFIX)) return null;
const parts = schema.slice(SQLSERVER_LINKED_SCHEMA_PREFIX.length).split("|").map(decodeURIComponent);
if (parts.length !== 3 || parts.some((part) => !part.trim())) return null;
return { server: parts[0], catalog: parts[1], schema: parts[2] };
}
export function isSqlServerLinkedNode(node: Pick<TreeNode, "linkedServer" | "linkedCatalog" | "linkedSchema" | "schema">): boolean {
return !!node.linkedServer || !!node.linkedCatalog || !!node.linkedSchema || !!parseSqlServerLinkedSchema(node.schema);
}
export function sqlServerLinkedTableSchema(node: Pick<TreeNode, "linkedServer" | "linkedCatalog" | "linkedSchema">): string | undefined {
if (!node.linkedServer || !node.linkedCatalog || !node.linkedSchema) return undefined;
return encodeSqlServerLinkedSchema({
server: node.linkedServer,
catalog: node.linkedCatalog,
schema: node.linkedSchema,
});
}
export function quoteSqlServerIdentifier(name: string): string {
return `[${name.replace(/\]/g, "]]")}]`;
}
export function sqlServerLinkedTableName(ref: SqlServerLinkedSchemaRef, tableName: string): string {
return [ref.server, ref.catalog, ref.schema, tableName].map(quoteSqlServerIdentifier).join(".");
}
@@ -0,0 +1,18 @@
import type { TreeNode } from "@/types/database";
import { sortSidebarNames } from "@/lib/database/databaseTree";
export const SQLSERVER_DEFAULT_SCHEMA = "dbo";
export function buildSqlServerDatabaseTreeNodes(connectionId: string, database: string, schemas: string[]): TreeNode[] {
const databaseNodeId = `${connectionId}:${database}`;
return sortSidebarNames(schemas).map((schema) => ({
id: `${databaseNodeId}:${schema}`,
label: schema,
type: "schema" as const,
connectionId,
database,
schema,
isExpanded: false,
children: [],
}));
}
@@ -0,0 +1,140 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>> = {
mysql: new Set(["information_schema", "mysql", "performance_schema", "sys"]),
doris: new Set(["information_schema", "mysql", "performance_schema", "sys"]),
starrocks: new Set(["information_schema", "mysql", "performance_schema", "sys"]),
manticoresearch: new Set(["information_schema", "mysql", "performance_schema", "sys"]),
goldendb: new Set(["information_schema", "mysql", "performance_schema", "sys"]),
gbase: new Set(["information_schema", "mysql", "performance_schema", "sys"]),
postgres: new Set(["template0", "template1"]),
gaussdb: new Set(["template0", "template1"]),
kwdb: new Set(["template0", "template1"]),
opengauss: new Set(["template0", "template1"]),
questdb: new Set(["template0", "template1"]),
kingbase: new Set(["template0", "template1"]),
highgo: new Set(["template0", "template1"]),
vastbase: new Set(["template0", "template1"]),
redshift: new Set(["template0", "template1"]),
clickhouse: new Set(["information_schema", "system"]),
tdengine: new Set(["information_schema", "performance_schema"]),
sqlserver: new Set(["master", "model", "msdb", "tempdb"]),
mongodb: new Set(["admin", "config", "local"]),
oracle: new Set([
"anonymous",
"appqossys",
"audsys",
"ctxsys",
"dbsnmp",
"dip",
"dvf",
"dvsys",
"exfsys",
"flows_files",
"gsmadmin_internal",
"mddata",
"mdsys",
"mgmt_view",
"olapsys",
"orddata",
"ordplugins",
"ordsys",
"outln",
"owbsys",
"remote_scheduler_agent",
"si_informtn_schema",
"sys",
"sysback",
"sysdg",
"syskm",
"system",
"wmsys",
"xdb",
"xs$null",
]),
dameng: new Set(["ctisys", "dba", "sys", "sysauditor", "syssso", "system"]),
saphana: new Set(["_sys_afl", "_sys_bi", "_sys_bic", "_sys_repo", "_sys_statistics", "sys"]),
cassandra: new Set(["system", "system_auth", "system_distributed", "system_schema", "system_traces", "system_views", "system_virtual_schema"]),
neo4j: new Set(["system"]),
snowflake: new Set(["snowflake", "snowflake_sample_data"]),
};
export function visibleDatabaseFilterIsEnabled(visibleDatabases: string[] | undefined): boolean {
return Array.isArray(visibleDatabases);
}
export function canSaveVisibleDatabaseSelection(selectedNames: string[]): boolean {
return selectedNames.length > 0;
}
export function filterVisibleDatabaseNames(databaseNames: string[], visibleDatabases: string[] | undefined): string[] {
if (!visibleDatabaseFilterIsEnabled(visibleDatabases)) return databaseNames;
const visible = new Set(visibleDatabases);
return databaseNames.filter((name) => visible.has(name));
}
export function normalizeVisibleDatabaseSelection(selectedNames: string[], databaseNames: string[]): string[] {
const available = new Set(databaseNames);
const seen = new Set<string>();
return selectedNames.filter((name) => {
if (!available.has(name) || seen.has(name)) return false;
seen.add(name);
return true;
});
}
export function isSystemDatabaseName(databaseType: DatabaseType | undefined, databaseName: string): boolean {
if (!databaseType) return false;
return SYSTEM_DATABASE_RULES[databaseType]?.has(databaseName.toLowerCase()) ?? false;
}
export function filterDatabaseNamesForConnection(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile" | "visible_databases"> | undefined): string[] {
const visibleDatabases = connection?.visible_databases;
if (visibleDatabaseFilterIsEnabled(visibleDatabases)) {
return filterVisibleDatabaseNames(databaseNames, visibleDatabases);
}
return filterDatabaseNamesForVisiblePicker(databaseNames, connection);
}
export function filterDatabaseNamesForVisiblePicker(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile"> | undefined): string[] {
if (connection?.db_type === "gbase" && connection.driver_profile === "gbase8s") {
return databaseNames;
}
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
}
export function connectionUsesVisibleSchemaFilter(connection: Pick<ConnectionConfig, "db_type"> | undefined): boolean {
return connection?.db_type === "oracle" || connection?.db_type === "dameng" || connection?.db_type === "oceanbase-oracle";
}
export function visibleSchemaFilterIsEnabled(visibleSchemas: Record<string, string[]> | undefined, database: string): boolean {
return Array.isArray(visibleSchemas?.[database]);
}
export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases"> | undefined, database: string): string[] {
const visibleSchemas = connection?.visible_schemas;
if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) {
if (connectionUsesVisibleSchemaFilter(connection) && visibleDatabaseFilterIsEnabled(connection?.visible_databases)) {
return filterVisibleDatabaseNames(schemaNames, connection?.visible_databases);
}
return schemaNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
}
const visible = new Set(visibleSchemas![database]);
return schemaNames.filter((name) => visible.has(name));
}
export function normalizeVisibleSchemaSelection(selectedNames: string[], schemaNames: string[]): string[] {
const available = new Set(schemaNames);
const seen = new Set<string>();
return selectedNames.filter((name) => {
if (!available.has(name) || seen.has(name)) return false;
seen.add(name);
return true;
});
}
const DRAFT_VISIBLE_SCHEMAS_PREFIX = "__visible_schema_draft_";
export function buildDraftVisibleSchemasConnectionId(seed: string): string {
return `${DRAFT_VISIBLE_SCHEMAS_PREFIX}${seed}`;
}