feat: add Microsoft Access database support

Add Access as a new database type with agent-based connectivity,
UCanAccess JDBC URL parsing, backtick identifier quoting, file-path
connection dialog, and PNG icons for Access/GoldenDB/Vastbase.
This commit is contained in:
t8y2
2026-05-17 09:57:04 +08:00
parent 381c92ff53
commit 1bcfd02de1
26 changed files with 185 additions and 22 deletions
+21 -7
View File
@@ -130,6 +130,7 @@ const driverProfiles: Record<
redis: { type: "redis", port: 6379, user: "", label: "Redis", icon: "redis" },
sqlite: { type: "sqlite", port: 0, user: "", label: "SQLite", icon: "sqlite" },
duckdb: { type: "duckdb", port: 0, user: "", label: "DuckDB", icon: "duckdb" },
access: { type: "access", port: 0, user: "", label: "Microsoft Access", icon: "access" },
mongodb: { type: "mongodb", port: 27017, user: "", label: "MongoDB", icon: "mongodb" },
clickhouse: {
type: "clickhouse",
@@ -248,7 +249,7 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.port = profile.port;
form.value.username = profile.user;
form.value.url_params = profile.urlParams || "";
if (profile.type === "sqlite" || profile.type === "duckdb") {
if (profile.type === "sqlite" || profile.type === "duckdb" || profile.type === "access") {
form.value.host = "";
}
if (profile.type === "jdbc") {
@@ -358,6 +359,7 @@ const iconTypeMap: Record<string, string> = {
mysql: "mysql",
postgres: "postgres",
sqlite: "sqlite",
access: "access",
redis: "redis",
mongodb: "mongodb",
duckdb: "duckdb",
@@ -402,6 +404,7 @@ const dbOptions = [
{ value: "mysql", label: "MySQL" },
{ value: "postgres", label: "PostgreSQL" },
{ value: "sqlite", label: "SQLite" },
{ value: "access", label: "Microsoft Access" },
{ value: "redis", label: "Redis" },
{ value: "mongodb", label: "MongoDB" },
{ value: "duckdb", label: "DuckDB" },
@@ -467,8 +470,15 @@ const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selecte
const isJdbcConnection = computed(() => form.value.db_type === "jdbc");
const connectionUrlPlaceholder = computed(() => getUrlPlaceholder(form.value.db_type));
const canUseSsh = computed(() => form.value.db_type !== "sqlite");
const canUseProxy = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "duckdb");
const filePathPlaceholder = computed(() => {
if (form.value.db_type === "duckdb") return "/path/to/database.duckdb";
if (form.value.db_type === "access") return "/path/to/database.accdb";
return "/path/to/database.db";
});
const canUseSsh = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "access");
const canUseProxy = computed(
() => form.value.db_type !== "sqlite" && form.value.db_type !== "duckdb" && form.value.db_type !== "access",
);
const shouldShowAgentDriverInstallHint = computed(() =>
showAgentDriverInstallHint(form.value.db_type, agentDrivers.value),
);
@@ -677,7 +687,9 @@ async function browseDbFilePath() {
const filters =
form.value.db_type === "duckdb"
? [{ name: "DuckDB", extensions: ["duckdb", "db"] }]
: [{ name: "SQLite", extensions: ["db", "sqlite", "sqlite3"] }];
: form.value.db_type === "access"
? [{ name: "Microsoft Access", extensions: ["accdb", "mdb"] }]
: [{ name: "SQLite", extensions: ["db", "sqlite", "sqlite3"] }];
const selected = await open({
title: "Select Database File",
multiple: false,
@@ -1040,12 +1052,14 @@ function openExternalUrl(url: string) {
</div>
</template>
<!-- SQLite / DuckDB: file path only -->
<template v-else-if="form.db_type === 'sqlite' || form.db_type === 'duckdb'">
<!-- Local database files: file path only -->
<template
v-else-if="form.db_type === 'sqlite' || form.db_type === 'duckdb' || form.db_type === 'access'"
>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.filePath") }}</Label>
<div class="col-span-3 flex items-center gap-1">
<Input v-model="form.host" class="flex-1" placeholder="/path/to/database.db" />
<Input v-model="form.host" class="flex-1" :placeholder="filePathPlaceholder" />
<Tooltip v-if="isDesktop">
<TooltipTrigger as-child>
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="browseDbFilePath">
+10 -9
View File
@@ -22,10 +22,13 @@ const assetIcons: Record<string, string> = {
"oracle-10g": "oracle",
oracle_10g: "oracle",
sqlserver: "sqlserver",
access: "access.png",
oceanbase: "oceanbase",
opengauss: "opengauss",
gaussdb: "gaussdb",
kingbase: "kingbase",
goldendb: "goldendb.png",
vastbase: "vastbase.png",
snowflake: "snowflake",
h2: "h2",
dm: "dm",
@@ -50,24 +53,22 @@ const assetIcons: Record<string, string> = {
};
const letterIcons: Record<string, { letter: string; color: string }> = {
goldendb: { letter: "G", color: "#F59E0B" },
vastbase: { letter: "V", color: "#6D28D9" },
highgo: { letter: "瀚", color: "#005bac" },
};
const normalizedType = computed(() => props.dbType.toLowerCase().replace(/[\s-]+/g, "_"));
const assetName = computed(() => assetIcons[normalizedType.value]);
const assetSrc = computed(() => {
if (!assetName.value) return "";
return assetName.value.includes(".")
? `/icons/database/${assetName.value}`
: `/icons/database/${assetName.value}.svg`;
});
const letter = computed(() => letterIcons[normalizedType.value]);
</script>
<template>
<img
v-if="assetName"
:src="`/icons/database/${assetName}.svg`"
alt=""
class="database-logo object-contain"
aria-hidden="true"
/>
<img v-if="assetName" :src="assetSrc" alt="" class="database-logo object-contain" aria-hidden="true" />
<svg v-else-if="letter" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="12" :fill="letter.color" />
<text
+4 -1
View File
@@ -5,7 +5,7 @@ type ConnectionPresentationConfig = Pick<
"db_type" | "driver_profile" | "driver_label" | "host" | "port" | "database"
>;
const LOCAL_DATABASE_TYPES = new Set(["sqlite", "duckdb"]);
const LOCAL_DATABASE_TYPES = new Set(["sqlite", "duckdb", "access"]);
export function connectionIconType(connection?: Pick<ConnectionConfig, "db_type" | "driver_profile">): string {
return connection?.driver_profile || connection?.db_type || "postgres";
@@ -45,6 +45,9 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
case "duckdb":
return "duckdb:///absolute/path/to/database.duckdb";
case "access":
return "jdbc:ucanaccess:///absolute/path/to/database.accdb";
case "mongodb":
return "mongodb://user:password@host:port/database";
+25
View File
@@ -107,11 +107,36 @@ function parseJdbcSqlServerUrl(source: string): ParsedConnectionUrl | null {
};
}
function parseJdbcUCanAccessUrl(source: string): ParsedConnectionUrl | null {
const match = source.match(/^jdbc:ucanaccess:\/\/(.+?)(?:;.*)?$/i);
if (!match) return null;
const filePath = decodeUrlPart(match[1]);
const normalizedPath = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath) ? filePath : `/${filePath}`;
const database = normalizedPath.split(/[\\/]/).filter(Boolean).pop();
return {
dbType: "access",
driverProfile: "access",
driverLabel: "Microsoft Access",
host: normalizedPath,
port: 0,
username: "",
password: "",
database,
urlParams: "",
ssl: false,
connectionString: source,
};
}
export function parseConnectionUrl(value: string, preferredProfile?: string): ParsedConnectionUrl {
const input = value.trim();
if (!input) {
throw new Error("Connection URL is empty");
}
const jdbcUCanAccess = parseJdbcUCanAccessUrl(input);
if (jdbcUCanAccess) return jdbcUCanAccess;
const jdbcSqlServer = parseJdbcSqlServerUrl(input);
if (jdbcSqlServer) return jdbcSqlServer;
const source = input.replace(/^jdbc:/i, "");
+2 -1
View File
@@ -362,7 +362,8 @@ function buildPrimaryKeyWhere(
columns: Array<string | undefined>,
row: GridCellValue[],
): string {
if (databaseType === "hive" && primaryKeys.length === 0) return buildRowWhere(databaseType, columns, row);
if ((databaseType === "hive" || databaseType === "access") && primaryKeys.length === 0)
return buildRowWhere(databaseType, columns, row);
return primaryKeys
.map((primaryKey) => {
const value = row[columns.indexOf(primaryKey)];
+5 -1
View File
@@ -33,6 +33,7 @@ export const DIAGRAM_SUPPORTED_TYPES = new Set<DatabaseType>([
"highgo",
"vastbase",
"goldendb",
"access",
"h2",
"db2",
]);
@@ -52,6 +53,7 @@ export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set<DatabaseType>([
"highgo",
"vastbase",
"goldendb",
"access",
"h2",
"snowflake",
"trino",
@@ -83,6 +85,7 @@ export const TABLE_IMPORT_SUPPORTED_TYPES = new Set<DatabaseType>([
"highgo",
"vastbase",
"goldendb",
"access",
]);
export const TABLE_STRUCTURE_SUPPORTED_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlite", "sqlserver"]);
@@ -111,7 +114,7 @@ export const FIELD_LINEAGE_SUPPORTED_TYPES = new Set<DatabaseType>([
"gaussdb",
]);
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng"]);
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "access"]);
export const FETCH_FIRST_TYPES = new Set<DatabaseType>(["oracle", "dameng"]);
@@ -137,6 +140,7 @@ export const AGENT_DRIVER_TYPES = new Set<DatabaseType>([
"highgo",
"vastbase",
"goldendb",
"access",
"oracle",
"h2",
"snowflake",
+9
View File
@@ -35,6 +35,15 @@ const DEFAULT_CAPABILITY: DatabaseCapability = {
};
const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<DatabaseCapability>>> = {
access: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
requiresTransactionalTableForExistingRows: false,
transaction: true,
},
},
hive: {
tableData: {
insert: true,
+1 -1
View File
@@ -17,7 +17,7 @@ export interface BuildTableSelectSqlOptions {
}
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
if (databaseType === "mysql" || databaseType === "hive" || databaseType === "tdengine")
if (databaseType === "mysql" || databaseType === "hive" || databaseType === "tdengine" || databaseType === "access")
return `\`${name.replace(/`/g, "``")}\``;
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
if (databaseType === "neo4j") return quoteCypherIdentifier(name);
+1
View File
@@ -186,6 +186,7 @@ export const useConnectionStore = defineStore("connection", () => {
highgo: "瀚高 HighGo",
vastbase: "Vastbase",
goldendb: "GoldenDB",
access: "Microsoft Access",
h2: "H2",
snowflake: "Snowflake",
trino: "Trino",
+1
View File
@@ -18,6 +18,7 @@ export type DatabaseType =
| "highgo"
| "vastbase"
| "goldendb"
| "access"
| "h2"
| "snowflake"
| "trino"