Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8118140
Fix #211: Execute only selected SQL text and strip SQLite comments (#…
ZhuchkaTriplesix Jun 21, 2026
7bb0e97
feat(extensions): implement LocalExtensionRegistry (EXT-2) (#214)
ZhuchkaTriplesix Jun 21, 2026
a2c81dc
Test: Unit Tests for Extension Registry (EXT-4) (#215)
ZhuchkaTriplesix Jun 21, 2026
6c4a0e1
Feat: Migrate Custom Themes to Extension Registry (EXT-3) (#216)
ZhuchkaTriplesix Jun 21, 2026
d42e756
test(settings): fix preferences appearance section widget test by moc…
ZhuchkaTriplesix Jun 21, 2026
002058e
Fix TOCTOU, ID collisions, redundant scans, and remove legacy theme c…
ZhuchkaTriplesix Jun 21, 2026
8fc7c76
Merge branch 'fix/215-preferences-appearance-menu-test' into dev
ZhuchkaTriplesix Jun 21, 2026
3872164
chore(release): prepare release 0.4.7
ZhuchkaTriplesix Jun 21, 2026
7b7c09e
feat(menu): implement File -> Exit to close the application window (#…
ZhuchkaTriplesix Jun 22, 2026
f631eac
feat(menu): implement File -> New, Open..., Save actions (#231)
ZhuchkaTriplesix Jun 22, 2026
a59d153
feat: implement connection management improvements and read-only mode…
ZhuchkaTriplesix Jun 22, 2026
cc39154
docs: document SQLite support, read-only mode, and connection actions
ZhuchkaTriplesix Jun 22, 2026
e795dbc
Merge branch 'origin/main' into dev
ZhuchkaTriplesix Jun 22, 2026
c2d0a54
chore(release): prepare pre-release 0.4.7-a
ZhuchkaTriplesix Jun 22, 2026
44ddb80
feat(ui): Extension Manager View (#239)
ZhuchkaTriplesix Jun 24, 2026
23cef19
feat(ui): Extension Card Component (#240)
ZhuchkaTriplesix Jul 6, 2026
85637a4
feat(storage): implement atomic updateConnection in LocalDb (#240) (#…
ZhuchkaTriplesix Jul 8, 2026
d38ce73
fix(sqlite): support RETURNING clauses in write queries (#243) (#246)
ZhuchkaTriplesix Jul 8, 2026
dbd5f14
security(mysql): replace string concatenation with parameterized quer…
ZhuchkaTriplesix Jul 8, 2026
998b2bd
... (#248)
ZhuchkaTriplesix Jul 10, 2026
56512c2
fix(connections): validate PostgreSQL sslmode and map useSSL correctl…
ZhuchkaTriplesix Jul 10, 2026
cb50ca7
fix(connections): surface PostgreSQL connection errors instead of swa…
ZhuchkaTriplesix Jul 10, 2026
e992cbe
fix(connections): improve PostgreSQL connection UX and error typing (…
ZhuchkaTriplesix Jul 10, 2026
fe07746
feat(connections): add SSL certificate file pickers to PostgreSQL for…
ZhuchkaTriplesix Jul 10, 2026
3dc8ee1
chore(release): prepare 0.4.9 release
ZhuchkaTriplesix Jul 10, 2026
b4b6fe4
Merge branch 'origin/main' into dev
ZhuchkaTriplesix Jul 10, 2026
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.9] - 2026-07-10

PostgreSQL connection reliability and TLS improvements, plus menu and URI import polish.

### Added

- **Connection → New Connection from URL (#228)** — create a connection by pasting a database URI for PostgreSQL, MySQL, MongoDB, Redis, or SQLite. The URL is parsed, validated, and saved to `LocalDb`.
- **Help → About & Documentation (#229)** — added an About dialog showing the app version, MIT license, and repository link; the Documentation menu item opens the project docs in the browser.
- **PostgreSQL SSL certificate file pickers (#260)** — added optional file pickers for `sslrootcert`, `sslcert`, and `sslkey` in the PostgreSQL connection form. Paths are merged into the connection URI or used to generate a URI when in host/port mode.

### Fixed

- **PostgreSQL URL parser / SSL (#249, #253, #254, #257, #258)** — validate `sslmode` (reject `prefer` and unknown values), correctly map `useSSL` for `disable`/`require`/`verify-ca`/`verify-full`, use driver default ports when omitted, and preserve display host/port for URI-only connections.
- **PostgreSQL connection error reporting (#250, #251, #252)** — `testConnection` now returns the underlying error message, the form shows the real failure reason, and the connection tree displays the full exception text instead of a generic "Error" label.
- **PostgreSQL connection error typing (#256, #259)** — `connect()` throws `PostgresConnectionException` with the original cause and stack trace; the pool wraps unexpected factory errors in the same typed exception.

## [0.4.8] - 2026-07-08

### Fixed

- **SQLite / RETURNING clause support (#243)** — support RETURNING clauses for INSERT, UPDATE, and DELETE DML queries in the SQLite database driver, returning the resulting rows to the client.
- **Security / MySQL Injection Fix (#241)** — replaced manual escaping and string concatenation in schema introspection methods (`listViews`, `listColumnNames`, `listTables`) in the MySQL database driver with parameterized queries using parameter binding.

## [0.4.7-a] - 2026-06-22

### Added
Expand Down
14 changes: 14 additions & 0 deletions docs/planned-0.4.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Milestone 0.4.8: Extension Manager UI

**Theme**: Разработка пользовательского интерфейса менеджера расширений (Marketplace Client). Мы создаем вкладку в настройках или отдельное окно, где пользователь сможет просматривать установленные расширения, искать новые в маркетплейсе, скачивать, обновлять и удалять их. Бэкенд маркетплейса пока будет замокан или использоваться статический JSON для тестов.

| ID | Scope | Summary |
|----|-------|---------|
| **UI-EXT-1** | `ui`, `extensions` | **Extension Manager View** — Создать страницу/диалог для менеджера расширений. Вкладки: Installed, Marketplace, Updates. |
| **UI-EXT-2** | `ui`, `extensions` | **Extension Card Component** — Виджет карточки расширения (иконка, название, автор, версия, описание, кнопка Install/Uninstall). |
| **UI-EXT-3** | `core`, `extensions` | **MarketplaceRepository Mock** — Реализовать моковый репозиторий для симуляции запросов к API маркетплейса (пока бэкенд не готов). |
| **UI-EXT-4** | `core`, `extensions` | **Download & Install Flow** — Интеграция UI с процессом скачивания (прогресс-бар), валидации sha256 и распаковки в `~/.querya/extensions/`. |
| **UI-EXT-5** | `core`, `extensions` | **Uninstall & Update** — Механизмы удаления и обновления локальных расширений через UI менеджера. |

## Заметки
Этот релиз фокусируется на пользовательском опыте (UX/UI) взаимодействия с расширениями. Мы не реализуем сам Marketplace API (он будет в 0.5.0), но закладываем сетевой слой клиента (`MarketplaceRepository`) и мокаем данные для того, чтобы UI можно было использовать и тестировать. Под капотом используются механизмы локального обнаружения, заложенные в релизе 0.4.7.
9 changes: 9 additions & 0 deletions lib/core/app/app_links.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Canonical external URLs for Querya Desktop.
abstract final class AppLinks {
static const repository =
'https://github.com/QueryaHub/Querya-Desktop';
static const documentation =
'https://github.com/QueryaHub/Querya-Desktop/blob/main/docs/README.md';
static const license =
'https://github.com/QueryaHub/Querya-Desktop/blob/main/LICENSE';
}
13 changes: 13 additions & 0 deletions lib/core/app/external_link.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:querya_desktop/core/app/app_links.dart';
import 'package:url_launcher/url_launcher.dart';

/// Opens [url] in the system browser.
Future<bool> launchExternalUrl(String url) {
final uri = Uri.parse(url);
return launchUrl(uri, mode: LaunchMode.externalApplication);
}

Future<bool> launchRepositoryUrl() => launchExternalUrl(AppLinks.repository);

Future<bool> launchDocumentationUrl() =>
launchExternalUrl(AppLinks.documentation);
20 changes: 10 additions & 10 deletions lib/core/database/mysql_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ class MysqlConnection {
bool get _usesConnectionString =>
connectionString != null && connectionString!.trim().isNotEmpty;

static String _escapeSqlString(String s) {
return s.replaceAll(r'\', r'\\').replaceAll("'", "''");
}


/// MySQL identifier quoting (backticks).
static String quoteIdentifier(String id) {
Expand Down Expand Up @@ -284,11 +282,11 @@ class MysqlConnection {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
final s = _escapeSqlString(schema);
final rs = await execute(
'SELECT TABLE_NAME FROM information_schema.TABLES '
"WHERE TABLE_SCHEMA = '$s' AND TABLE_TYPE = 'VIEW' "
"WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'VIEW' "
'ORDER BY TABLE_NAME',
{'schema': schema},
);
return rs.rows.map((r) => r.colAt(0)!).toList();
}
Expand All @@ -301,12 +299,14 @@ class MysqlConnection {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
final d = _escapeSqlString(database);
final t = _escapeSqlString(table);
final rs = await execute(
'SELECT COLUMN_NAME FROM information_schema.COLUMNS '
"WHERE TABLE_SCHEMA = '$d' AND TABLE_NAME = '$t' "
"WHERE TABLE_SCHEMA = :database AND TABLE_NAME = :table "
'ORDER BY ORDINAL_POSITION',
{
'database': database,
'table': table,
},
);
return rs.rows.map((r) => r.colAt(0)!).toList();
}
Expand All @@ -316,11 +316,11 @@ class MysqlConnection {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
final s = _escapeSqlString(schema);
final rs = await execute(
'SELECT TABLE_NAME FROM information_schema.TABLES '
"WHERE TABLE_SCHEMA = '$s' AND TABLE_TYPE = 'BASE TABLE' "
"WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE' "
'ORDER BY TABLE_NAME',
{'schema': schema},
);
return rs.rows.map((r) => r.colAt(0)!).toList();
}
Expand Down
34 changes: 26 additions & 8 deletions lib/core/database/postgres_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,17 @@ class PostgresConnection {
);
}
_isConnected = true;
} catch (e) {
} catch (e, st) {
_isConnected = false;
_conn = null;
rethrow;
Error.throwWithStackTrace(
PostgresConnectionException(
'Failed to connect to PostgreSQL${name.isNotEmpty ? ' ($name)' : ''}: $e',
cause: e,
stackTrace: st,
),
st,
);
}
}

Expand Down Expand Up @@ -174,16 +181,19 @@ class PostgresConnection {
);
}

Future<bool> testConnection() async {
/// Tests connectivity and returns a result with an optional error message.
Future<({bool ok, String? error})> testConnection() async {
try {
await connect();
if (_conn != null) {
await _conn!.execute('SELECT 1');
return true;
return (ok: true, error: null);
}
return false;
} catch (_) {
return false;
return (ok: false, error: 'Connection could not be established.');
} on PostgresConnectionException catch (e) {
return (ok: false, error: e.message);
} catch (e) {
return (ok: false, error: e.toString());
} finally {
await disconnect();
}
Expand Down Expand Up @@ -769,8 +779,16 @@ class PostgresSequenceDetails {
}

class PostgresConnectionException implements Exception {
PostgresConnectionException(this.message);
PostgresConnectionException(
this.message, {
this.cause,
this.stackTrace,
});

final String message;
final Object? cause;
final StackTrace? stackTrace;

@override
String toString() => message;
}
21 changes: 17 additions & 4 deletions lib/core/database/postgres_connection_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,23 @@ class PostgresConnectionPool {

_evictIfNeededBeforeNewSlot();

final conn = await createAndConnect(row, database: database, mode: mode);
entry = _PoolEntry(conn)..refs = 1;
_pool[k] = entry;
return PgLease._(this, k, conn);
try {
final conn = await createAndConnect(row, database: database, mode: mode);
entry = _PoolEntry(conn)..refs = 1;
_pool[k] = entry;
return PgLease._(this, k, conn);
} on PostgresConnectionException {
rethrow;
} catch (e, st) {
Error.throwWithStackTrace(
PostgresConnectionException(
'Failed to acquire PostgreSQL connection for database "$database": $e',
cause: e,
stackTrace: st,
),
st,
);
}
}

/// Drops idle LRU slots until there is room for one more key.
Expand Down
13 changes: 8 additions & 5 deletions lib/core/database/sqlite_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,21 @@ class SqliteConnection {
.toLowerCase();

// SQLite can execute PRAGMA, SELECT, EXPLAIN statements, which return data
final isQuery = sqlLower.startsWith('select') ||
final isReadOnlyQuery = sqlLower.startsWith('select') ||
sqlLower.startsWith('pragma') ||
sqlLower.startsWith('explain') ||
sqlLower.startsWith('with') ||
sqlLower.startsWith('values');

if (isQuery) {
final hasReturning = RegExp(r'\breturning\b').hasMatch(sqlLower);

if (readOnly && !isReadOnlyQuery) {
throw StateError('Database connection is read-only');
}

if (isReadOnlyQuery || hasReturning) {
return await _db!.rawQuery(sql, arguments);
} else {
if (readOnly) {
throw StateError('Database connection is read-only');
}
await _db!.execute(sql, arguments);
return [];
}
Expand Down
65 changes: 62 additions & 3 deletions lib/core/extensions/models/extension_manifest.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import '../../theme/theme_definition.dart';
import 'extension_type.dart';

class ExtensionManifest {
static const typeTheme = ExtensionType.theme;

final String id;
final String name;
final String version;
Expand All @@ -11,6 +14,13 @@ class ExtensionManifest {
final String? icon;
final String? description;
final String? installPath;
final String? downloadUrl;
final String? sha256Checksum;
final String? author;
final String? homepage;
final String? license;
final String? preview;
final List<String> tags;

const ExtensionManifest({
required this.id,
Expand All @@ -23,20 +33,62 @@ class ExtensionManifest {
this.icon,
this.description,
this.installPath,
this.downloadUrl,
this.sha256Checksum,
this.author,
this.homepage,
this.license,
this.preview,
this.tags = const [],
});

/// Maps a registry [ThemeDefinition] into marketplace field names.
factory ExtensionManifest.fromThemeDefinition(
ThemeDefinition definition, {
String downloadUrl = '',
String sha256Checksum = '',
ExtensionType type = typeTheme,
}) {
final metadata = definition.metadata;
return ExtensionManifest(
id: definition.id,
name: definition.name,
publisher: metadata?.author ?? 'Unknown',
type: type,
version: metadata?.version ?? '0.0.0',
engines: const {'querya_desktop': '^0.4.7'},
downloadUrl: downloadUrl,
sha256Checksum: sha256Checksum.isNotEmpty
? sha256Checksum
: (definition.contentHash ?? ''),
author: metadata?.author,
description: metadata?.description,
homepage: metadata?.homepage,
license: metadata?.license,
preview: metadata?.preview,
tags: metadata?.tags ?? const [],
);
}

factory ExtensionManifest.fromJson(Map<String, dynamic> json, {String? installPath}) {
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),
version: json['version'] as String? ?? '0.0.0',
publisher: json['publisher'] as String? ?? json['author'] as String? ?? 'Unknown',
type: ExtensionType.fromString(json['type'] as String? ?? ''),
engines: Map<String, String>.from(json['engines'] as Map? ?? {}),
main: json['main'] as String?,
icon: json['icon'] as String?,
description: json['description'] as String?,
installPath: installPath,
downloadUrl: json['downloadUrl'] as String?,
sha256Checksum: json['sha256Checksum'] as String? ?? json['sha256'] as String?,
author: json['author'] as String?,
homepage: json['homepage'] as String?,
license: json['license'] as String?,
preview: json['preview'] as String?,
tags: List<String>.from(json['tags'] as List? ?? []),
);
}

Expand All @@ -51,6 +103,13 @@ class ExtensionManifest {
if (main != null) 'main': main,
if (icon != null) 'icon': icon,
if (description != null) 'description': description,
if (downloadUrl != null) 'downloadUrl': downloadUrl,
if (sha256Checksum != null) 'sha256Checksum': sha256Checksum,
if (author != null) 'author': author,
if (homepage != null) 'homepage': homepage,
if (license != null) 'license': license,
if (preview != null) 'preview': preview,
if (tags.isNotEmpty) 'tags': tags,
};
}
}
Loading
Loading