Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions lib/core/extensions/extension_driver_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import 'package:path/path.dart' as p;
import 'package:querya_desktop/core/extensions/extension_driver_catalog.dart';
import 'package:querya_desktop/core/extensions/extension_support.dart';
import 'package:querya_desktop/core/extensions/local_extension_registry.dart';
import 'package:querya_desktop/core/extensions/models/extension_driver_capabilities.dart';
import 'package:querya_desktop/core/extensions/models/extension_manifest.dart';
import 'package:querya_desktop/core/extensions/models/extension_object_metadata.dart';
import 'package:querya_desktop/core/extensions/models/extension_server_stats.dart';
import 'package:querya_desktop/core/extensions/rpc/plugin_rpc_bridge.dart';
import 'package:querya_desktop/core/sdui/sdui_tree_schema.dart';
import 'package:querya_desktop/core/storage/connection_secrets_store.dart';
Expand Down Expand Up @@ -271,6 +274,68 @@ class ExtensionDriverSession {
return _nodesFromResult(result);
}

Future<ExtensionDriverCapabilities> getCapabilities(ConnectionRow row) async {
final bridge = await ensureConnected(row);
try {
final result = await bridge.sendRequest('db.getCapabilities', {
'connectionId': row.id,
});
return ExtensionDriverCapabilities.fromRpc(result);
} catch (e) {
debugPrint('ExtensionDriverSession getCapabilities fallback ($e)');
return const ExtensionDriverCapabilities();
}
}

Future<ExtensionServerStats> getServerStats(ConnectionRow row) async {
final bridge = await ensureConnected(row);
try {
final result = await bridge.sendRequest('db.getServerStats', {
'connectionId': row.id,
});
return ExtensionServerStats.fromRpc(result);
} catch (e) {
debugPrint('ExtensionDriverSession getServerStats fallback ($e)');
return const ExtensionServerStats();
}
}

Future<ExtensionObjectMetadata> getObjectMetadata(
ConnectionRow row, {
required String nodeId,
required String nodeType,
}) async {
final bridge = await ensureConnected(row);
try {
final result = await bridge.sendRequest('db.getObjectMetadata', {
'connectionId': row.id,
'nodeId': nodeId,
'nodeType': nodeType,
});
return ExtensionObjectMetadata.fromRpc(result);
} catch (e) {
debugPrint('ExtensionDriverSession getObjectMetadata fallback ($e)');
return ExtensionObjectMetadata(nodeId: nodeId, nodeType: nodeType);
}
}

Future<bool> cancelQuery(ConnectionRow row, {required String queryId}) async {
final bridge = await ensureConnected(row);
try {
final result = await bridge.sendRequest('db.cancelQuery', {
'connectionId': row.id,
'queryId': queryId,
});
if (result is Map) {
return result['success'] == true || result['cancelled'] == true;
}
return true;
} catch (e) {
debugPrint('ExtensionDriverSession cancelQuery failed ($e)');
return false;
}
}

Future<void> disconnect(int connectionId) async {
final bridge = _bridges.remove(connectionId);
_manifests.remove(connectionId);
Expand Down
76 changes: 76 additions & 0 deletions lib/core/extensions/models/extension_driver_capabilities.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/// Feature flags reported by an extension database driver via `db.getCapabilities`.
class ExtensionDriverCapabilities {
const ExtensionDriverCapabilities({
this.supportsTransactions = false,
this.supportsCancel = false,
this.supportsDDLInspection = false,
this.supportsPrivileges = false,
this.hasServerStats = false,
});

/// True if `db.query` supports transaction control queries (BEGIN, COMMIT, ROLLBACK).
final bool supportsTransactions;

/// True if the driver supports `db.cancelQuery`.
final bool supportsCancel;

/// True if the driver supports `db.getObjectDDL` / `db.getObjectMetadata`.
final bool supportsDDLInspection;

/// True if the driver supports privilege inspection/grant management.
final bool supportsPrivileges;

/// True if the driver supports `db.getServerStats`.
final bool hasServerStats;

factory ExtensionDriverCapabilities.fromRpc(Object? raw) {
if (raw is! Map) return const ExtensionDriverCapabilities();
final map = raw is Map<String, dynamic>
? raw
: Map<String, dynamic>.from(raw);
return ExtensionDriverCapabilities(
supportsTransactions:
map['supportsTransactions'] == true ||
map['supports_transactions'] == true,
supportsCancel:
map['supportsCancel'] == true || map['supports_cancel'] == true,
supportsDDLInspection:
map['supportsDDLInspection'] == true ||
map['supports_ddl_inspection'] == true,
supportsPrivileges:
map['supportsPrivileges'] == true ||
map['supports_privileges'] == true,
hasServerStats:
map['hasServerStats'] == true || map['has_server_stats'] == true,
);
}

Map<String, Object?> toJson() => {
'supportsTransactions': supportsTransactions,
'supportsCancel': supportsCancel,
'supportsDDLInspection': supportsDDLInspection,
'supportsPrivileges': supportsPrivileges,
'hasServerStats': hasServerStats,
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ExtensionDriverCapabilities &&
runtimeType == other.runtimeType &&
supportsTransactions == other.supportsTransactions &&
supportsCancel == other.supportsCancel &&
supportsDDLInspection == other.supportsDDLInspection &&
supportsPrivileges == other.supportsPrivileges &&
hasServerStats == other.hasServerStats;

@override
int get hashCode =>
Object.hash(
supportsTransactions,
supportsCancel,
supportsDDLInspection,
supportsPrivileges,
hasServerStats,
);
}
108 changes: 108 additions & 0 deletions lib/core/extensions/models/extension_object_metadata.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/// Represents a column in a database object inspected by an extension driver.
class ExtensionObjectColumn {
const ExtensionObjectColumn({
required this.name,
required this.dataType,
this.isNullable = true,
this.defaultValue,
this.comment,
});

final String name;
final String dataType;
final bool isNullable;
final String? defaultValue;
final String? comment;

factory ExtensionObjectColumn.fromRpc(Object? raw) {
if (raw is! Map) {
return const ExtensionObjectColumn(name: '', dataType: '');
}
final map = raw is Map<String, dynamic>
? raw
: Map<String, dynamic>.from(raw);
return ExtensionObjectColumn(
name: '${map['name'] ?? ''}'.trim(),
dataType:
'${map['dataType'] ?? map['data_type'] ?? map['type'] ?? ''}'.trim(),
isNullable: map['isNullable'] == false ||
map['is_nullable'] == false ||
map['nullable'] == false
? false
: true,
defaultValue: map['defaultValue']?.toString() ??
map['default_value']?.toString(),
comment: map['comment']?.toString(),
);
}

Map<String, Object?> toJson() => {
'name': name,
'dataType': dataType,
'isNullable': isNullable,
if (defaultValue != null) 'defaultValue': defaultValue,
if (comment != null) 'comment': comment,
};
}

/// DDL and structural metadata returned by `db.getObjectDDL` / `db.getObjectMetadata`.
class ExtensionObjectMetadata {
const ExtensionObjectMetadata({
this.nodeId = '',
this.nodeType = '',
this.ddl,
this.columns = const [],
this.properties = const {},
});

final String nodeId;
final String nodeType;

/// The SQL creation statement (`CREATE TABLE ...`, `CREATE FUNCTION ...`).
final String? ddl;

/// Structural columns if `nodeType` is a table or view.
final List<ExtensionObjectColumn> columns;

/// Additional key-value properties (e.g., engine, row count, comment).
final Map<String, Object?> properties;

factory ExtensionObjectMetadata.fromRpc(Object? raw) {
if (raw is! Map) return const ExtensionObjectMetadata();
final map = raw is Map<String, dynamic>
? raw
: Map<String, dynamic>.from(raw);

final cols = <ExtensionObjectColumn>[];
final rawCols = map['columns'];
if (rawCols is List) {
for (final col in rawCols) {
cols.add(ExtensionObjectColumn.fromRpc(col));
}
}

final props = <String, Object?>{};
final rawProps = map['properties'];
if (rawProps is Map) {
rawProps.forEach((k, v) {
if (v != null) props['$k'] = v;
});
}

return ExtensionObjectMetadata(
nodeId: '${map['nodeId'] ?? map['node_id'] ?? ''}'.trim(),
nodeType: '${map['nodeType'] ?? map['node_type'] ?? ''}'.trim(),
ddl: map['ddl']?.toString() ?? map['sql']?.toString(),
columns: cols,
properties: props,
);
}

Map<String, Object?> toJson() => {
'nodeId': nodeId,
'nodeType': nodeType,
if (ddl != null) 'ddl': ddl,
'columns': columns.map((c) => c.toJson()).toList(),
'properties': properties,
};
}
87 changes: 87 additions & 0 deletions lib/core/extensions/models/extension_server_stats.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/// Normalized server statistics reported by an extension driver via `db.getServerStats`.
class ExtensionServerStats {
const ExtensionServerStats({
this.serverVersion,
this.uptimeSeconds,
this.activeConnections,
this.activeQueries,
this.memoryUsageBytes,
this.databaseSizes = const {},
this.extraMetrics = const {},
});

/// Server engine version string (e.g. "ClickHouse 24.3.1.1").
final String? serverVersion;

/// Total uptime in seconds.
final int? uptimeSeconds;

/// Number of active client connections.
final int? activeConnections;

/// Number of currently running queries on the server.
final int? activeQueries;

/// Total memory consumption by the database process in bytes.
final int? memoryUsageBytes;

/// Size in bytes for each database on the server (`{"default": 1048576, ...}`).
final Map<String, int> databaseSizes;

/// Driver-specific extra metrics (key -> string/num value).
final Map<String, Object?> extraMetrics;

factory ExtensionServerStats.fromRpc(Object? raw) {
if (raw is! Map) return const ExtensionServerStats();
final map = raw is Map<String, dynamic>
? raw
: Map<String, dynamic>.from(raw);

final dbSizes = <String, int>{};
final rawSizes = map['databaseSizes'] ?? map['database_sizes'];
if (rawSizes is Map) {
rawSizes.forEach((k, v) {
if (v is num) dbSizes['$k'] = v.toInt();
});
}

final extra = <String, Object?>{};
final rawExtra = map['extraMetrics'] ?? map['extra_metrics'];
if (rawExtra is Map) {
rawExtra.forEach((k, v) {
if (v != null) extra['$k'] = v;
});
}

int? toIntOrNull(Object? val) {
if (val is num) return val.toInt();
if (val is String) return int.tryParse(val.trim());
return null;
}

return ExtensionServerStats(
serverVersion: map['serverVersion']?.toString() ??
map['server_version']?.toString() ??
map['version']?.toString(),
uptimeSeconds: toIntOrNull(map['uptimeSeconds'] ?? map['uptime_seconds']),
activeConnections: toIntOrNull(
map['activeConnections'] ?? map['active_connections']),
activeQueries:
toIntOrNull(map['activeQueries'] ?? map['active_queries']),
memoryUsageBytes: toIntOrNull(
map['memoryUsageBytes'] ?? map['memory_usage_bytes']),
databaseSizes: dbSizes,
extraMetrics: extra,
);
}

Map<String, Object?> toJson() => {
if (serverVersion != null) 'serverVersion': serverVersion,
if (uptimeSeconds != null) 'uptimeSeconds': uptimeSeconds,
if (activeConnections != null) 'activeConnections': activeConnections,
if (activeQueries != null) 'activeQueries': activeQueries,
if (memoryUsageBytes != null) 'memoryUsageBytes': memoryUsageBytes,
'databaseSizes': databaseSizes,
'extraMetrics': extraMetrics,
};
}
Loading
Loading