From 393cc17f97f4e3aa43193740eca18a2f5ed3b7cf Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 21 Jun 2026 13:16:33 +0300 Subject: [PATCH] Fix #211: Execute only selected SQL text and strip SQLite comments --- lib/core/database/sqlite_connection.dart | 6 +- .../extensions/models/extension_manifest.dart | 53 +++++++++++++ .../extensions/models/extension_type.dart | 15 ++++ lib/features/mysql/mysql_sql_workspace.dart | 8 +- .../postgresql/postgres_sql_workspace.dart | 8 +- lib/features/sqlite/sqlite_sql_workspace.dart | 8 +- .../models/extension_manifest_test.dart | 79 +++++++++++++++++++ 7 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 lib/core/extensions/models/extension_manifest.dart create mode 100644 lib/core/extensions/models/extension_type.dart create mode 100644 test/core/extensions/models/extension_manifest_test.dart diff --git a/lib/core/database/sqlite_connection.dart b/lib/core/database/sqlite_connection.dart index bfdcf9ac..d4585d4a 100644 --- a/lib/core/database/sqlite_connection.dart +++ b/lib/core/database/sqlite_connection.dart @@ -90,7 +90,11 @@ class SqliteConnection { if (!isConnected || _db == null) { throw StateError('Not connected to SQLite'); } - final sqlLower = sql.trim().toLowerCase(); + final sqlLower = sql + .replaceAll(RegExp(r'--.*$', multiLine: true), '') + .replaceAll(RegExp(r'/\*.*?\*/', dotAll: true), '') + .trim() + .toLowerCase(); // SQLite can execute PRAGMA, SELECT, EXPLAIN statements, which return data final isQuery = sqlLower.startsWith('select') || diff --git a/lib/core/extensions/models/extension_manifest.dart b/lib/core/extensions/models/extension_manifest.dart new file mode 100644 index 00000000..dc15d7e5 --- /dev/null +++ b/lib/core/extensions/models/extension_manifest.dart @@ -0,0 +1,53 @@ +import 'extension_type.dart'; + +class ExtensionManifest { + final String id; + final String name; + final String version; + final String publisher; + final ExtensionType type; + final Map engines; + final String? main; + final String? icon; + final String? description; + + const ExtensionManifest({ + required this.id, + required this.name, + required this.version, + required this.publisher, + required this.type, + required this.engines, + this.main, + this.icon, + this.description, + }); + + factory ExtensionManifest.fromJson(Map json) { + return ExtensionManifest( + id: json['id'] as String, + name: json['name'] as String, + version: json['version'] as String, + publisher: json['publisher'] as String, + type: ExtensionType.fromString(json['type'] as String), + engines: Map.from(json['engines'] as Map? ?? {}), + main: json['main'] as String?, + icon: json['icon'] as String?, + description: json['description'] as String?, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'version': version, + 'publisher': publisher, + 'type': type.value, + 'engines': engines, + if (main != null) 'main': main, + if (icon != null) 'icon': icon, + if (description != null) 'description': description, + }; + } +} diff --git a/lib/core/extensions/models/extension_type.dart b/lib/core/extensions/models/extension_type.dart new file mode 100644 index 00000000..dc6eeb0f --- /dev/null +++ b/lib/core/extensions/models/extension_type.dart @@ -0,0 +1,15 @@ +enum ExtensionType { + databaseDriver('database_driver'), + theme('theme'), + unknown('unknown'); + + final String value; + const ExtensionType(this.value); + + static ExtensionType fromString(String value) { + return ExtensionType.values.firstWhere( + (e) => e.value == value, + orElse: () => ExtensionType.unknown, + ); + } +} diff --git a/lib/features/mysql/mysql_sql_workspace.dart b/lib/features/mysql/mysql_sql_workspace.dart index 23fd3f71..e2778fa2 100644 --- a/lib/features/mysql/mysql_sql_workspace.dart +++ b/lib/features/mysql/mysql_sql_workspace.dart @@ -122,7 +122,13 @@ class _MysqlSqlWorkspaceState extends material.State { } Future _execute() async { - final userSql = _sqlController.text.trim(); + final selection = _sqlController.selection; + String userSql; + if (selection.isValid && !selection.isCollapsed) { + userSql = selection.textInside(_sqlController.text).trim(); + } else { + userSql = _sqlController.text.trim(); + } if (userSql.isEmpty) return; setState(() { diff --git a/lib/features/postgresql/postgres_sql_workspace.dart b/lib/features/postgresql/postgres_sql_workspace.dart index 3377009f..d72b79a9 100644 --- a/lib/features/postgresql/postgres_sql_workspace.dart +++ b/lib/features/postgresql/postgres_sql_workspace.dart @@ -265,7 +265,13 @@ class _PostgresSqlWorkspaceState extends material.State { } Future _execute() async { - final userSql = _sqlController.text.trim(); + final selection = _sqlController.selection; + String userSql; + if (selection.isValid && !selection.isCollapsed) { + userSql = selection.textInside(_sqlController.text).trim(); + } else { + userSql = _sqlController.text.trim(); + } if (userSql.isEmpty) return; var sql = injectSqlLimit(userSql, _resultMaxRows); diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index 39eba4b2..b2449b70 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -95,7 +95,13 @@ class _SqliteSqlWorkspaceState extends material.State { } Future _execute() async { - final userSql = _sqlController.text.trim(); + final selection = _sqlController.selection; + String userSql; + if (selection.isValid && !selection.isCollapsed) { + userSql = selection.textInside(_sqlController.text).trim(); + } else { + userSql = _sqlController.text.trim(); + } if (userSql.isEmpty) return; setState(() { diff --git a/test/core/extensions/models/extension_manifest_test.dart b/test/core/extensions/models/extension_manifest_test.dart new file mode 100644 index 00000000..e367a2bd --- /dev/null +++ b/test/core/extensions/models/extension_manifest_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/extensions/models/extension_manifest.dart'; +import 'package:querya_desktop/core/extensions/models/extension_type.dart'; + +void main() { + group('ExtensionManifest', () { + test('parses valid manifest correctly', () { + final json = { + 'id': 'queryahub.clickhouse-driver', + 'name': 'ClickHouse Database Driver', + 'version': '1.0.0', + 'publisher': 'QueryaHub', + 'type': 'database_driver', + 'engines': { + 'querya_desktop': '^0.5.0' + }, + 'main': 'bin/clickhouse_plugin', + 'icon': 'assets/icon.svg', + 'description': 'Full support for ClickHouse databases' + }; + + final manifest = ExtensionManifest.fromJson(json); + + expect(manifest.id, 'queryahub.clickhouse-driver'); + expect(manifest.name, 'ClickHouse Database Driver'); + expect(manifest.version, '1.0.0'); + expect(manifest.publisher, 'QueryaHub'); + expect(manifest.type, ExtensionType.databaseDriver); + expect(manifest.engines, {'querya_desktop': '^0.5.0'}); + expect(manifest.main, 'bin/clickhouse_plugin'); + expect(manifest.icon, 'assets/icon.svg'); + expect(manifest.description, 'Full support for ClickHouse databases'); + }); + + test('handles missing optional fields', () { + final json = { + 'id': 'queryahub.my-theme', + 'name': 'My Theme', + 'version': '1.0.0', + 'publisher': 'QueryaHub', + 'type': 'theme', + 'engines': { + 'querya_desktop': '^0.5.0' + } + }; + + final manifest = ExtensionManifest.fromJson(json); + + expect(manifest.id, 'queryahub.my-theme'); + expect(manifest.type, ExtensionType.theme); + expect(manifest.main, isNull); + expect(manifest.icon, isNull); + expect(manifest.description, isNull); + }); + + test('falls back to unknown type for unrecognized extension types', () { + final json = { + 'id': 'queryahub.future-plugin', + 'name': 'Future Plugin', + 'version': '1.0.0', + 'publisher': 'QueryaHub', + 'type': 'future_formatter', + 'engines': {} + }; + + final manifest = ExtensionManifest.fromJson(json); + + expect(manifest.type, ExtensionType.unknown); + }); + + test('throws type error on completely invalid json structure', () { + final json = { + 'id': 'missing_everything_else' + }; + + expect(() => ExtensionManifest.fromJson(json), throwsA(isA())); + }); + }); +}