feat(execution): multi-database SQL execution with scoped targets
This commit is contained in:
1 parent
328a2aa107
commit
71ccb3d97d
46 files changed
+4421
-182
No files matched your search
@@ -0,0 +1,152 @@
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user