Files
dbx/apps/desktop/src/lib/database/sqlExecutionTargetCapabilities.ts
T

153 lines
7.8 KiB
TypeScript

import type { ConnectionConfig, DatabaseType } from "@/types/database";
import type { MultiDbExecutionTarget } from "@/types/sqlExecution";
import { isSchemaAware, isSingleDatabase, supportsConnectionScopedQueryExecution, supportsQueryTargetDatabaseListing, usesConnectionOnlyQueryTarget } from "@/lib/database/databaseFeatureSupport";
import { supportsConnectionLevelSqlExecution } from "@/lib/connection/connectionLevelDatabaseBootstrap";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { supportsQueryExecution } from "@/lib/database/databaseFeatureSupport";
import { registeredSqlExecutionTargetAllowsEmptyMetadataDefault, registeredSqlExecutionTargetDefaultDatabase, resolveSqlExecutionTargetProvider, supportsRegisteredCatalogTarget, type SqlExecutionTargetProvider, type SqlExecutionTargetScope } from "@/lib/database/sqlExecutionTargetRegistry";
/**
* Defaults that are part of the driver contract rather than values inferred
* from a possibly incomplete metadata response. A configured connection
* database is handled separately below and is also considered a stable
* default for that connection.
*/
export interface SqlExecutionTargetCapabilities {
databaseType: DatabaseType;
scope: SqlExecutionTargetScope;
provider: SqlExecutionTargetProvider;
supportsCatalog: boolean;
supportsDatabase: boolean;
supportsSchema: boolean;
/** Whether a database-like namespace is required to construct a target. */
databaseRequired: boolean;
/** Whether the namespace is enumerated by the existing metadata path. */
databaseListable: boolean;
/** Whether the connection itself is a valid fallback target. */
connectionFallback: boolean;
/** Whether an empty database is a valid target value for this scope. */
allowsEmptyDatabaseTarget: boolean;
/** Stable default used only before metadata has been loaded or by registered fallback rules. */
defaultDatabase?: string;
/** A loaded empty metadata list must not be widened to this fallback implicitly. */
allowDefaultWhenDatabaseListEmpty: boolean;
}
/**
* Resolves the target shape for every query-capable connection surface.
*
* This is the single extension point for multi-database target semantics. UI,
* validation and persistence consumers should use this result instead of
* maintaining database-type allow-lists of their own.
*/
export function sqlExecutionTargetCapabilities(connection: ConnectionConfig | undefined): SqlExecutionTargetCapabilities | undefined {
const databaseType = effectiveDatabaseTypeForConnection(connection);
if (!connection || !databaseType || !supportsQueryExecution(connection.db_type)) return undefined;
const connectionOnly = usesConnectionOnlyQueryTarget(databaseType);
const connectionScoped = supportsConnectionScopedQueryExecution(databaseType);
const supportsCatalog = supportsRegisteredCatalogTarget(connection, databaseType);
const supportsSchema = !connectionOnly && isSchemaAware(databaseType);
const supportsDatabase = !connectionOnly;
const databaseListable = !connectionOnly;
const namespaceTarget = connectionScoped && !connectionOnly;
const defaultDatabase = defaultExecutionTargetDatabase(connection, databaseType);
const connectionLevelExecution = supportsConnectionLevelSqlExecution(connection);
const hasStableDefault = defaultDatabase.length > 0 && (registeredSqlExecutionTargetAllowsEmptyMetadataDefault(databaseType) || !!connection.database?.trim());
const scope: SqlExecutionTargetScope = connectionOnly ? "connection" : supportsCatalog ? "catalog" : namespaceTarget ? "namespace" : "database";
return {
databaseType,
scope,
provider: resolveSqlExecutionTargetProvider(scope, databaseType),
supportsCatalog,
supportsDatabase,
supportsSchema,
databaseRequired: connectionOnly ? false : namespaceTarget ? true : !isSingleDatabase(databaseType) && !connectionLevelExecution,
databaseListable,
connectionFallback: connectionOnly,
allowsEmptyDatabaseTarget: connectionOnly || isSingleDatabase(databaseType),
defaultDatabase,
allowDefaultWhenDatabaseListEmpty: hasStableDefault,
};
}
function defaultExecutionTargetDatabase(connection: ConnectionConfig, databaseType: DatabaseType): string {
return registeredSqlExecutionTargetDefaultDatabase(connection, databaseType);
}
export function targetUsesConnectionOnlyScope(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.scope === "connection";
}
export function targetUsesNamespaceScope(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.scope === "namespace";
}
export function targetUsesConnectionFallback(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.connectionFallback === true;
}
export function targetAllowsEmptyDatabase(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.allowsEmptyDatabaseTarget === true;
}
/**
* Normalizes a target at the boundary where it is created from a live
* connection. This prevents an old tab namespace from leaking into a
* connection-scoped target and makes all producers use the same shape.
* Persisted targets are intentionally not silently normalized on load; they
* must be validated and shown as invalid when their stored shape is stale.
*/
export function normalizeSqlExecutionTarget(connection: ConnectionConfig | undefined, target: MultiDbExecutionTarget): MultiDbExecutionTarget {
const capabilities = sqlExecutionTargetCapabilities(connection);
if (!capabilities) return { ...target };
if (capabilities.scope === "connection") return { connectionId: target.connectionId, database: "" };
return {
connectionId: target.connectionId,
database: capabilities.supportsDatabase ? target.database : "",
...(capabilities.supportsCatalog && target.catalog ? { catalog: target.catalog } : {}),
...(capabilities.supportsSchema && target.schema ? { schema: target.schema } : {}),
};
}
export function targetIsSingleDatabase(connection: ConnectionConfig | undefined): boolean {
const capabilities = sqlExecutionTargetCapabilities(connection);
return capabilities ? isSingleDatabase(capabilities.databaseType) : false;
}
export function targetDefaultDatabase(connection: ConnectionConfig | undefined): string {
return sqlExecutionTargetCapabilities(connection)?.defaultDatabase ?? "";
}
export function targetCanUseDefaultWhenDatabaseListEmpty(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.allowDefaultWhenDatabaseListEmpty === true;
}
export function targetDatabaseListIsRequired(connection: ConnectionConfig | undefined): boolean {
const capabilities = sqlExecutionTargetCapabilities(connection);
return !!capabilities?.databaseRequired;
}
export function targetDatabaseMetadataIsListable(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.databaseListable === true;
}
export function targetSupportsSchema(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.supportsSchema === true;
}
export function targetSupportsCatalog(connection: ConnectionConfig | undefined): boolean {
return sqlExecutionTargetCapabilities(connection)?.supportsCatalog === true;
}
/**
* Namespace lists are already backed by the existing database-options loader
* for document/vector query surfaces. Keep this helper explicit so a future
* target provider can register a different loader without changing consumers.
*/
export function targetNamespaceListIsRegistered(connection: ConnectionConfig | undefined): boolean {
const databaseType = sqlExecutionTargetCapabilities(connection)?.databaseType;
return !!databaseType && supportsQueryTargetDatabaseListing(databaseType);
}