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:
parent
381c92ff53
commit
1bcfd02de1
|
|
@ -239,6 +239,7 @@ impl AppState {
|
|||
| DatabaseType::Kylin
|
||||
| DatabaseType::Sundb
|
||||
| DatabaseType::Tdengine
|
||||
| DatabaseType::Access
|
||||
| DatabaseType::Gaussdb => {
|
||||
let mut client =
|
||||
self.agent_manager.spawn(&db_config.db_type, db_config.driver_profile.as_deref()).await?;
|
||||
|
|
@ -437,6 +438,7 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da
|
|||
"username": config.username,
|
||||
"password": config.password,
|
||||
"url_params": config.url_params.as_deref().unwrap_or(""),
|
||||
"connection_string": config.connection_string.as_deref().unwrap_or(""),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ pub fn agent_key(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option
|
|||
DatabaseType::Gaussdb => Some("gaussdb"),
|
||||
DatabaseType::MongoDb => Some("mongodb"),
|
||||
DatabaseType::Tdengine => Some("tdengine"),
|
||||
DatabaseType::Access => Some("access"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +45,7 @@ pub fn is_single_connection_pool(db_type: &DatabaseType) -> bool {
|
|||
| DatabaseType::Highgo
|
||||
| DatabaseType::Vastbase
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Access
|
||||
| DatabaseType::Jdbc
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ pub enum DatabaseType {
|
|||
Vastbase,
|
||||
Goldendb,
|
||||
Gaussdb,
|
||||
Access,
|
||||
#[serde(rename = "h2")]
|
||||
H2,
|
||||
Snowflake,
|
||||
|
|
@ -213,6 +214,7 @@ impl ConnectionConfig {
|
|||
DatabaseType::Sqlite | DatabaseType::DuckDb => {
|
||||
format!("{}?mode=rwc", self.host)
|
||||
}
|
||||
DatabaseType::Access => self.host.clone(),
|
||||
DatabaseType::Redis => {
|
||||
let scheme = if self.ssl { "rediss" } else { "redis" };
|
||||
format!("{scheme}://{host}:{port}/")
|
||||
|
|
@ -284,6 +286,7 @@ impl ConnectionConfig {
|
|||
DatabaseType::Sqlite | DatabaseType::DuckDb => {
|
||||
format!("{}?mode=rwc", self.host)
|
||||
}
|
||||
DatabaseType::Access => self.host.clone(),
|
||||
DatabaseType::Redis => {
|
||||
let scheme = if self.ssl { "rediss" } else { "redis" };
|
||||
if self.username.is_empty() && self.password.is_empty() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ fn maps_agent_database_types_to_driver_keys() {
|
|||
assert_eq!(agent_key(&DatabaseType::Hive, None), Some("hive"));
|
||||
assert_eq!(agent_key(&DatabaseType::Gaussdb, None), Some("gaussdb"));
|
||||
assert_eq!(agent_key(&DatabaseType::Tdengine, None), Some("tdengine"));
|
||||
assert_eq!(agent_key(&DatabaseType::Access, None), Some("access"));
|
||||
assert_eq!(agent_key(&DatabaseType::Oracle, None), Some("oracle"));
|
||||
assert_eq!(agent_key(&DatabaseType::Oracle, Some("oracle-10g")), Some("oracle-10g"));
|
||||
assert_eq!(agent_key(&DatabaseType::Postgres, None), None);
|
||||
|
|
@ -20,6 +21,7 @@ fn classifies_agent_database_types() {
|
|||
assert!(is_agent_type(&DatabaseType::Trino));
|
||||
assert!(is_agent_type(&DatabaseType::Hive));
|
||||
assert!(is_agent_type(&DatabaseType::Tdengine));
|
||||
assert!(is_agent_type(&DatabaseType::Access));
|
||||
assert!(!is_agent_type(&DatabaseType::Mysql));
|
||||
assert!(!is_agent_type(&DatabaseType::Jdbc));
|
||||
}
|
||||
|
|
@ -30,6 +32,7 @@ fn identifies_single_connection_pool_types() {
|
|||
assert!(is_single_connection_pool(&DatabaseType::DuckDb));
|
||||
assert!(is_single_connection_pool(&DatabaseType::Oracle));
|
||||
assert!(is_single_connection_pool(&DatabaseType::Dameng));
|
||||
assert!(is_single_connection_pool(&DatabaseType::Access));
|
||||
assert!(is_single_connection_pool(&DatabaseType::Jdbc));
|
||||
assert!(!is_single_connection_pool(&DatabaseType::Trino));
|
||||
assert!(!is_single_connection_pool(&DatabaseType::Postgres));
|
||||
|
|
@ -49,6 +52,7 @@ fn skips_tcp_probe_for_local_file_plugin_and_agent_types() {
|
|||
assert!(skips_tcp_probe(&DatabaseType::Sqlite));
|
||||
assert!(skips_tcp_probe(&DatabaseType::DuckDb));
|
||||
assert!(skips_tcp_probe(&DatabaseType::Jdbc));
|
||||
assert!(skips_tcp_probe(&DatabaseType::Access));
|
||||
assert!(skips_tcp_probe(&DatabaseType::Trino));
|
||||
assert!(skips_tcp_probe(&DatabaseType::Oracle));
|
||||
assert!(skips_tcp_probe(&DatabaseType::Tdengine));
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 837 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
|
|
@ -21,6 +21,7 @@ const AGENT_TYPES: &[(&str, &str)] = &[
|
|||
("highgo", "瀚高 HighGo"),
|
||||
("vastbase", "Vastbase"),
|
||||
("goldendb", "GoldenDB"),
|
||||
("access", "Microsoft Access"),
|
||||
("oracle", "Oracle"),
|
||||
("oracle-10g", "Oracle 10g"),
|
||||
("h2", "H2"),
|
||||
|
|
@ -104,13 +105,14 @@ mod tests {
|
|||
use dbx_core::agent_manager::AgentManager;
|
||||
|
||||
#[test]
|
||||
fn built_in_agent_list_includes_tdengine() {
|
||||
fn built_in_agent_list_includes_access_and_tdengine() {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-agent-list-test-{}", uuid::Uuid::new_v4()));
|
||||
let manager = AgentManager::new_with_base_dir(dir.clone());
|
||||
|
||||
let agents = build_agent_list(&manager, None);
|
||||
|
||||
assert!(agents.iter().any(|agent| agent.db_type == "tdengine" && agent.label == "TDengine"));
|
||||
assert!(agents.iter().any(|agent| agent.db_type == "access" && agent.label == "Microsoft Access"));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
highgo: "瀚高 HighGo",
|
||||
vastbase: "Vastbase",
|
||||
goldendb: "GoldenDB",
|
||||
access: "Microsoft Access",
|
||||
h2: "H2",
|
||||
snowflake: "Snowflake",
|
||||
trino: "Trino",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type DatabaseType =
|
|||
| "highgo"
|
||||
| "vastbase"
|
||||
| "goldendb"
|
||||
| "access"
|
||||
| "h2"
|
||||
| "snowflake"
|
||||
| "trino"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ test("shows the agent driver install hint for TDengine when missing", () => {
|
|||
assert.equal(showAgentDriverInstallHint("tdengine", [{ db_type: "tdengine", installed: false }]), true);
|
||||
});
|
||||
|
||||
test("shows the agent driver install hint for Access when missing", () => {
|
||||
assert.equal(showAgentDriverInstallHint("access", [{ db_type: "access", installed: false }]), true);
|
||||
});
|
||||
|
||||
test("does not show agent driver install hints for built-in database types", () => {
|
||||
assert.equal(showAgentDriverInstallHint("mysql", [{ db_type: "informix", installed: false }]), false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,4 +37,15 @@ test("uses file path as endpoint for local database connections", () => {
|
|||
};
|
||||
|
||||
assert.equal(connectionOptionSubtitle(sqliteConnection), "SQLite · /tmp/local.db");
|
||||
|
||||
const accessConnection: ConnectionConfig = {
|
||||
...baseConnection,
|
||||
db_type: "access",
|
||||
driver_profile: "access",
|
||||
driver_label: "Microsoft Access",
|
||||
host: "/tmp/Northwind.accdb",
|
||||
port: 0,
|
||||
};
|
||||
|
||||
assert.equal(connectionOptionSubtitle(accessConnection), "Microsoft Access · /tmp/Northwind.accdb");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,6 +66,18 @@ test("parses TDengine WebSocket JDBC URLs", () => {
|
|||
assert.equal(parsed.urlParams, "timezone=UTC");
|
||||
});
|
||||
|
||||
test("parses UCanAccess JDBC URLs as Access database files", () => {
|
||||
const parsed = parseConnectionUrl("jdbc:ucanaccess:///Users/me/data/Northwind.accdb;memory=false");
|
||||
|
||||
assert.equal(parsed.dbType, "access");
|
||||
assert.equal(parsed.driverProfile, "access");
|
||||
assert.equal(parsed.driverLabel, "Microsoft Access");
|
||||
assert.equal(parsed.host, "/Users/me/data/Northwind.accdb");
|
||||
assert.equal(parsed.port, 0);
|
||||
assert.equal(parsed.database, "Northwind.accdb");
|
||||
assert.equal(parsed.connectionString, "jdbc:ucanaccess:///Users/me/data/Northwind.accdb;memory=false");
|
||||
});
|
||||
|
||||
test("parses SQL Server JDBC URLs with semicolon properties", () => {
|
||||
const parsed = parseConnectionUrl(
|
||||
"jdbc:sqlserver://sql.example.com:1434;databaseName=erp;user=sa;password=s%40cret;encrypt=true",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const expected: Record<string, string> = {
|
|||
redis: "redis://:password@host:port/0",
|
||||
sqlite: "sqlite:///absolute/path/to/database.db",
|
||||
duckdb: "duckdb:///absolute/path/to/database.duckdb",
|
||||
access: "jdbc:ucanaccess:///absolute/path/to/database.accdb",
|
||||
mongodb: "mongodb://user:password@host:port/database",
|
||||
clickhouse: "clickhouse://user:password@host:port/database",
|
||||
sqlserver: "mssql://user:password@host:port/database",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,47 @@ test("builds SQL Server grid save statements with schema and bracket quoting", (
|
|||
]);
|
||||
});
|
||||
|
||||
test("builds Access grid save statements with backtick identifiers", () => {
|
||||
const statements = buildDataGridSaveStatements({
|
||||
databaseType: "access",
|
||||
tableMeta: {
|
||||
tableName: "Order Details",
|
||||
primaryKeys: ["Order ID"],
|
||||
},
|
||||
columns: ["Order ID", "Product Name", "Active"],
|
||||
rows: [[42, "Old", true]],
|
||||
dirtyRows: [[0, [[1, "Ready"]]]],
|
||||
deletedRows: [0],
|
||||
newRows: [[43, "New", false]],
|
||||
});
|
||||
|
||||
assert.deepEqual(statements, [
|
||||
"UPDATE `Order Details` SET `Product Name` = 'Ready' WHERE `Order ID` = 42;",
|
||||
"DELETE FROM `Order Details` WHERE `Order ID` = 42;",
|
||||
"INSERT INTO `Order Details` (`Order ID`, `Product Name`, `Active`) VALUES (43, 'New', FALSE);",
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds Access grid save statements with row predicates when primary keys are unavailable", () => {
|
||||
const statements = buildDataGridSaveStatements({
|
||||
databaseType: "access",
|
||||
tableMeta: {
|
||||
tableName: "orders",
|
||||
primaryKeys: [],
|
||||
},
|
||||
columns: ["id", "quantity", "status", "shipped_at"],
|
||||
rows: [[1, 3, "pending", null]],
|
||||
dirtyRows: [[0, [[1, 4]]]],
|
||||
deletedRows: [0],
|
||||
newRows: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(statements, [
|
||||
"UPDATE `orders` SET `quantity` = 4 WHERE `id` = 1 AND `quantity` = 3 AND `status` = 'pending' AND `shipped_at` IS NULL;",
|
||||
"DELETE FROM `orders` WHERE `id` = 1 AND `quantity` = 3 AND `status` = 'pending' AND `shipped_at` IS NULL;",
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds grid save statements through source columns for aliased query results", () => {
|
||||
const statements = buildDataGridSaveStatements({
|
||||
databaseType: "postgres",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ test("treats TDengine databases as schema tree roots and agent driver databases"
|
|||
assert.equal(supportsDriverManagement("tdengine"), true);
|
||||
});
|
||||
|
||||
test("treats Access as a local single-database agent driver", () => {
|
||||
assert.equal(SCHEMA_AWARE_TYPES.has("access"), false);
|
||||
assert.equal(supportsDriverManagement("access"), true);
|
||||
assert.equal(supportsDatabaseSearch("access"), true);
|
||||
assert.equal(supportsTableImport("access"), true);
|
||||
});
|
||||
|
||||
test("describes schema tree mode through the capability helper", () => {
|
||||
assert.equal(usesTreeSchemaMode("trino"), true);
|
||||
assert.equal(usesTreeSchemaMode("h2"), true);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ test("uses tbname and timestamp as TDengine editable keys", () => {
|
|||
});
|
||||
|
||||
test("allows Hive table data editing even without declared primary keys", () => {
|
||||
assert.equal(isTableDataEditable("access", []), true);
|
||||
assert.equal(isTableDataEditable("hive", []), true);
|
||||
assert.equal(isTableDataEditable("trino", []), true);
|
||||
assert.equal(isTableDataEditable("informix", []), true);
|
||||
|
|
@ -60,7 +61,9 @@ test("does not use transactional grid saves for Hive", () => {
|
|||
assert.equal(supportsDataGridTransaction("postgres"), true);
|
||||
});
|
||||
|
||||
test("allows existing row edits for Hive only when the table is transactional", () => {
|
||||
test("allows existing row edits according to database-specific key requirements", () => {
|
||||
assert.equal(canEditExistingTableRows("access", undefined, []), true);
|
||||
assert.equal(canEditExistingTableRows("access", undefined, ["ID"]), true);
|
||||
assert.equal(canEditExistingTableRows("hive", true), true);
|
||||
assert.equal(canEditExistingTableRows("hive", false), false);
|
||||
assert.equal(canEditExistingTableRows("hive", undefined), false);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,18 @@ test("builds Informix table data queries without database-qualified delimited id
|
|||
assert.equal(sql, "SELECT * FROM dbx_grid_edit_probe ORDER BY id ASC LIMIT 100;");
|
||||
});
|
||||
|
||||
test("builds Access table data queries with backtick identifiers", () => {
|
||||
const sql = buildTableSelectSql({
|
||||
databaseType: "access",
|
||||
tableName: "Order Details",
|
||||
primaryKeys: ["Order ID"],
|
||||
limit: 100,
|
||||
offset: 200,
|
||||
});
|
||||
|
||||
assert.equal(sql, "SELECT * FROM `Order Details` ORDER BY `Order ID` ASC LIMIT 100 OFFSET 200;");
|
||||
});
|
||||
|
||||
test("expands Hive table data queries into aliased table columns", () => {
|
||||
const sql = buildTableSelectSql({
|
||||
databaseType: "hive",
|
||||
|
|
|
|||
Loading…
Reference in New Issue