refactor(desktop): organize lib modules by domain
This commit is contained in:
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)]),
|
||||
};
|
||||
@@ -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}`;
|
||||
}
|
||||
Reference in new issue
Block a user