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
6 changes: 6 additions & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ POSTGRES_PORT=5432
MYSQL_PORT=3306
REDIS_PORT=6379
MONGO_PORT=27017
CLICKHOUSE_HTTP_PORT=8123
CLICKHOUSE_NATIVE_PORT=9000

POSTGRES_USER=querya
POSTGRES_PASSWORD=querya
Expand All @@ -17,3 +19,7 @@ MYSQL_PASSWORD=querya

MONGO_INITDB_ROOT_USERNAME=querya
MONGO_INITDB_ROOT_PASSWORD=querya

CLICKHOUSE_DB=querya
CLICKHOUSE_USER=querya
CLICKHOUSE_PASSWORD=querya
112 changes: 112 additions & 0 deletions docker/clickhouse/init/01_seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
-- Demo OLAP data for Querya ClickHouse extension testing.
-- Runs once on first container start via /docker-entrypoint-initdb.d

CREATE DATABASE IF NOT EXISTS querya;

CREATE TABLE IF NOT EXISTS querya.customers
(
id UInt32,
name String,
email String,
city LowCardinality(String),
created_at DateTime
)
ENGINE = MergeTree
ORDER BY id;

CREATE TABLE IF NOT EXISTS querya.products
(
id UInt32,
sku String,
title String,
category LowCardinality(String),
price Decimal(10, 2)
)
ENGINE = MergeTree
ORDER BY id;

CREATE TABLE IF NOT EXISTS querya.orders
(
id UInt64,
customer_id UInt32,
status LowCardinality(String),
total Decimal(12, 2),
placed_at DateTime
)
ENGINE = MergeTree
ORDER BY (placed_at, id);

CREATE TABLE IF NOT EXISTS querya.order_lines
(
order_id UInt64,
product_id UInt32,
qty UInt16,
unit_price Decimal(10, 2)
)
ENGINE = MergeTree
ORDER BY (order_id, product_id);

CREATE TABLE IF NOT EXISTS querya.events
(
event_id UUID,
event_time DateTime,
user_id UInt32,
event_type LowCardinality(String),
path String,
country LowCardinality(String),
revenue Decimal(12, 4)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id);

-- 500 customers
INSERT INTO querya.customers
SELECT
toUInt32(number + 1) AS id,
concat('Customer ', toString(number + 1)) AS name,
concat('user', toString(number + 1), '@example.com') AS email,
['Berlin', 'London', 'Madrid', 'Paris', 'Tokyo', 'New York', 'São Paulo', 'Cairo'][number % 8 + 1] AS city,
now() - toIntervalDay(number % 365) AS created_at
FROM numbers(500);

-- 80 products
INSERT INTO querya.products
SELECT
toUInt32(number + 1) AS id,
concat('SKU-', leftPad(toString(number + 1), 4, '0')) AS sku,
concat('Product ', toString(number + 1)) AS title,
['Electronics', 'Books', 'Home', 'Sports', 'Fashion'][number % 5 + 1] AS category,
toDecimal64(round(4.99 + (number % 200) * 1.37, 2), 2) AS price
FROM numbers(80);

-- 2_000 orders
INSERT INTO querya.orders
SELECT
toUInt64(number + 1) AS id,
toUInt32((number % 500) + 1) AS customer_id,
['new', 'paid', 'shipped', 'cancelled', 'refunded'][number % 5 + 1] AS status,
toDecimal64(round(9.99 + (number % 150) * 2.41, 2), 2) AS total,
now() - toIntervalHour(number % (24 * 120)) AS placed_at
FROM numbers(2000);

-- ~6_000 order lines (1–4 lines per order)
INSERT INTO querya.order_lines
SELECT
toUInt64((number % 2000) + 1) AS order_id,
toUInt32((number % 80) + 1) AS product_id,
toUInt16((number % 5) + 1) AS qty,
toDecimal64(round(4.99 + (number % 200) * 1.37, 2), 2) AS unit_price
FROM numbers(6000);

-- 50_000 analytics events
INSERT INTO querya.events
SELECT
generateUUIDv4() AS event_id,
now() - toIntervalSecond(number % (86400 * 30)) AS event_time,
toUInt32((number % 500) + 1) AS user_id,
['page_view', 'add_to_cart', 'purchase', 'search', 'login'][number % 5 + 1] AS event_type,
concat('/app/', ['home', 'catalog', 'product', 'checkout', 'account'][number % 5 + 1]) AS path,
['DE', 'GB', 'ES', 'FR', 'JP', 'US', 'BR', 'EG'][number % 8 + 1] AS country,
if(number % 5 = 2, toDecimal64(round((number % 100) * 1.25, 4), 4), toDecimal64(0, 4)) AS revenue
FROM numbers(50000);
33 changes: 33 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# Redis port 6379 no auth keys prefix querya:*
# MongoDB port 27017 db querya user querya password querya
# auth source: admin collections: users, products, orders
# ClickHouse HTTP 8123 / native 9000 db querya user querya password querya
# tables: customers, products, orders, order_lines, events
# SQLite local file ./sqlite/data/querya.db
# tables: users, products, orders
# ─────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -137,6 +139,36 @@ services:
retries: 20
start_period: 30s

clickhouse:
image: clickhouse/clickhouse-server:24.8
container_name: querya-clickhouse
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_DB:-querya}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-querya}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-querya}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
ports:
- "${CLICKHOUSE_HTTP_PORT:-8123}:8123"
- "${CLICKHOUSE_NATIVE_PORT:-9000}:9000"
volumes:
- clickhouse_data:/var/lib/clickhouse
- ./clickhouse/init:/docker-entrypoint-initdb.d:ro
ulimits:
nofile:
soft: 262144
hard: 262144
healthcheck:
test:
[
"CMD-SHELL",
"clickhouse-client --user $${CLICKHOUSE_USER:-querya} --password $${CLICKHOUSE_PASSWORD:-querya} --query 'SELECT 1'",
]
interval: 5s
timeout: 5s
retries: 20
start_period: 40s

sqlite-seed:
image: alpine:latest
container_name: querya-sqlite-seed
Expand All @@ -152,3 +184,4 @@ volumes:
mysql_data:
redis_data:
mongo_data:
clickhouse_data:
5 changes: 3 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ day-to-day usage, and [Security](security.md) for how credentials are stored.
## Local dev databases (optional)

The repo includes a Docker Compose stack under [`docker/`](../docker/) with
PostgreSQL, MySQL, MongoDB, and Redis plus seed data:
PostgreSQL, MySQL, MongoDB, Redis, ClickHouse, and SQLite seed data:

```bash
cp docker/.env.example docker/.env # optional overrides
cd docker && docker compose up -d
```

Default credentials: user/password **`querya`**, database **`querya`**
(MongoDB auth source: **`admin`**). Stop with `docker compose down`.
(MongoDB auth source: **`admin`**; ClickHouse HTTP **`8123`**, native **`9000`**).
Stop with `docker compose down`.
5 changes: 3 additions & 2 deletions lib/app/app_shutdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import 'package:querya_desktop/core/database/mysql_service.dart';
import 'package:querya_desktop/core/database/postgres_service.dart';
import 'package:querya_desktop/core/database/redis_service.dart';
import 'package:querya_desktop/core/database/sqlite_service.dart';
import 'package:querya_desktop/core/extensions/extension_driver_session.dart';

/// Disconnects all pooled / cached client connections (PostgreSQL pool, MySQL,
/// Mongo, Redis, SQLite). Safe to call when no connections exist.
/// Mongo, Redis, SQLite, extension drivers). Safe to call when no connections exist.
Future<void> disconnectAllExternalServices() async {
await PostgresService.instance.disconnectAll();
await MysqlService.instance.disconnectAll();
await MongoService.instance.disconnectAll();
await RedisService.instance.disconnectAll();
await SqliteService.instance.disconnectAll();
await ExtensionDriverSession.instance.disconnectAll();
}

8 changes: 6 additions & 2 deletions lib/core/actions/sql_connection_types.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import 'package:querya_desktop/core/extensions/extension_driver_catalog.dart';
import 'package:querya_desktop/core/storage/local_db.dart';

const kSqlCapableConnectionTypes = {'postgresql', 'mysql', 'sqlite'};

bool isSqlCapableConnection(ConnectionRow? connection) =>
connection != null && kSqlCapableConnectionTypes.contains(connection.type);
bool isSqlCapableConnection(ConnectionRow? connection) {
if (connection == null) return false;
if (kSqlCapableConnectionTypes.contains(connection.type)) return true;
return ExtensionDriverCatalog.isExtensionDriverConnection(connection);
}
2 changes: 1 addition & 1 deletion lib/core/actions/sql_editor_global_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class SqlEditorGlobalActions extends StatelessWidget {
final Widget child;

static const _noSqlConnectionMessage =
'Select a PostgreSQL, MySQL, or SQLite connection to edit SQL files.';
'Select a SQL-capable connection (PostgreSQL, MySQL, SQLite, or an installed driver) to edit SQL files.';

@override
Widget build(BuildContext context) {
Expand Down
99 changes: 99 additions & 0 deletions lib/core/extensions/extension_driver_catalog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import 'package:querya_desktop/core/extensions/local_extension_registry.dart';
import 'package:querya_desktop/core/extensions/models/extension_manifest.dart';
import 'package:querya_desktop/core/extensions/models/extension_type.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/connection_type_choice.dart';
import 'package:querya_desktop/features/connections/new_connection_dialog.dart';

/// Built-in + installed extension drivers for New Connection / Driver Manager.
class ExtensionDriverCatalog {
ExtensionDriverCatalog._();

static const builtInChoices = <ConnectionTypeChoice>[
BuiltInConnectionType(ConnectionType.postgresql),
BuiltInConnectionType(ConnectionType.mysql),
BuiltInConnectionType(ConnectionType.sqlite),
BuiltInConnectionType(ConnectionType.redis),
BuiltInConnectionType(ConnectionType.mongodb),
];

static const sqlBuiltIns = <ConnectionTypeChoice>[
BuiltInConnectionType(ConnectionType.postgresql),
BuiltInConnectionType(ConnectionType.mysql),
BuiltInConnectionType(ConnectionType.sqlite),
];

static const noSqlBuiltIns = <ConnectionTypeChoice>[
BuiltInConnectionType(ConnectionType.redis),
BuiltInConnectionType(ConnectionType.mongodb),
];

/// Extension drivers currently loaded in [LocalExtensionRegistry].
static List<ExtensionDriverChoice> extensionChoices([
LocalExtensionRegistry? registry,
]) {
final manifests = (registry ?? LocalExtensionRegistry.instance).manifests;
final out = <ExtensionDriverChoice>[];
for (final manifest in manifests) {
if (manifest.type != ExtensionType.databaseDriver) continue;
for (final driver in manifest.contributedDrivers) {
if (driver.driverId.trim().isEmpty) continue;
out.add(ExtensionDriverChoice(manifest: manifest, driver: driver));
}
}
out.sort((a, b) => a.label.toLowerCase().compareTo(b.label.toLowerCase()));
return out;
}

/// All choices for the "All databases" category.
static List<ConnectionTypeChoice> allChoices([
LocalExtensionRegistry? registry,
]) =>
[...builtInChoices, ...extensionChoices(registry)];

static List<ConnectionTypeChoice> sqlChoices([
LocalExtensionRegistry? registry,
]) =>
[...sqlBuiltIns, ...extensionChoices(registry)];

static List<ConnectionTypeChoice> noSqlChoices() => noSqlBuiltIns;

/// True when [row] is backed by an installed extension driver package.
static bool isExtensionDriverConnection(ConnectionRow row) {
final extId = row.extensionId?.trim();
if (extId != null && extId.isNotEmpty) return true;
return manifestForConnection(row) != null;
}

/// Resolves the installed manifest for a saved connection row.
static ExtensionManifest? manifestForConnection(
ConnectionRow row, [
LocalExtensionRegistry? registry,
]) {
final reg = registry ?? LocalExtensionRegistry.instance;
final extId = row.extensionId?.trim();
if (extId != null && extId.isNotEmpty) {
for (final manifest in reg.manifests) {
if (manifest.id == extId) return manifest;
}
}
final type = row.type.trim().toLowerCase();
if (type.isEmpty) return null;
for (final manifest in reg.manifests) {
if (manifest.type != ExtensionType.databaseDriver) continue;
for (final driver in manifest.contributedDrivers) {
if (driver.driverId.trim().toLowerCase() == type) {
return manifest;
}
}
}
return null;
}

/// Packaged icon path for a saved connection, when available on disk.
static String? iconFileForConnection(
ConnectionRow row, [
LocalExtensionRegistry? registry,
]) =>
manifestForConnection(row, registry)?.resolvedIconPath;
}
Loading
Loading