From 28ce32ce9101bd9c0aaf9b5b73897a9fa8dcf22f Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Wed, 9 Sep 2026 18:01:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20MariaDB=20type=20layer=20=E2=80=94=20un?= =?UTF-8?q?ion,=20capabilities,=20icons,=20ports,=20URI=20parse/copy,=20fi?= =?UTF-8?q?lter=20(Task=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../connections/NewConnectionScreen.tsx | 1 + .../src/components/editor/QueryToolbar.tsx | 1 + .../layout/DbTypeFilterDropdown.tsx | 1 + desktop/src/lib/connectionString.test.ts | 40 +++++++++++++++++++ desktop/src/lib/connectionString.ts | 4 ++ desktop/src/lib/dbCapabilities.test.ts | 10 +++++ desktop/src/lib/dbCapabilities.ts | 1 + desktop/src/lib/dbIcons.tsx | 6 ++- desktop/src/lib/providers.test.ts | 11 ++++- desktop/src/lib/providers.ts | 3 +- desktop/src/lib/types.ts | 2 +- desktop/src/lib/utils.ts | 2 +- 12 files changed, 76 insertions(+), 6 deletions(-) diff --git a/desktop/src/components/connections/NewConnectionScreen.tsx b/desktop/src/components/connections/NewConnectionScreen.tsx index 2ce0407..224a4f5 100644 --- a/desktop/src/components/connections/NewConnectionScreen.tsx +++ b/desktop/src/components/connections/NewConnectionScreen.tsx @@ -31,6 +31,7 @@ type Stage = "entry" | "configured"; const FALLBACK_PORTS: Record = { postgresql: 5432, mysql: 3306, + mariadb: 3306, redis: 6379, }; diff --git a/desktop/src/components/editor/QueryToolbar.tsx b/desktop/src/components/editor/QueryToolbar.tsx index 4d78224..b94c1d8 100644 --- a/desktop/src/components/editor/QueryToolbar.tsx +++ b/desktop/src/components/editor/QueryToolbar.tsx @@ -9,6 +9,7 @@ import type { DbType } from "../../lib/types"; const DB_TYPE_LABELS: Record = { postgresql: "PostgreSQL", mysql: "MySQL", + mariadb: "MariaDB", sqlite: "SQLite", redis: "Redis", }; diff --git a/desktop/src/components/layout/DbTypeFilterDropdown.tsx b/desktop/src/components/layout/DbTypeFilterDropdown.tsx index c9524dd..b11b92b 100644 --- a/desktop/src/components/layout/DbTypeFilterDropdown.tsx +++ b/desktop/src/components/layout/DbTypeFilterDropdown.tsx @@ -7,6 +7,7 @@ import type { DbType } from "../../lib/types"; const DB_TYPES: { value: DbType; label: string }[] = [ { value: "postgresql", label: "PostgreSQL" }, { value: "mysql", label: "MySQL" }, + { value: "mariadb", label: "MariaDB" }, { value: "sqlite", label: "SQLite" }, { value: "redis", label: "Redis" }, ]; diff --git a/desktop/src/lib/connectionString.test.ts b/desktop/src/lib/connectionString.test.ts index 2ee320c..fd8ce99 100644 --- a/desktop/src/lib/connectionString.test.ts +++ b/desktop/src/lib/connectionString.test.ts @@ -125,6 +125,30 @@ describe("parseConnectionString", () => { }); }); + it("parses mariadb:// with the 3306 default port", () => { + const result = parseConnectionString("mariadb://root:pw@localhost/app"); + expect(result).toEqual({ + db_type: "mariadb", + host: "localhost", + port: 3306, + username: "root", + password: "pw", + database: "app", + }); + }); + + it("keeps mysql:// mapped to the mysql db_type", () => { + const result = parseConnectionString("mysql://root:pw@localhost:3307/app"); + expect(result).toEqual({ + db_type: "mysql", + host: "localhost", + port: 3307, + username: "root", + password: "pw", + database: "app", + }); + }); + it("defaults Redis port to 6379 when omitted", () => { const result = parseConnectionString("redis://localhost"); expect(result).toEqual({ @@ -212,6 +236,22 @@ describe("buildConnectionUrl", () => { expect(url).toBe("mysql://root:pw@127.0.0.1:3306/app"); }); + it("emits mariadb:// for a mariadb connection", () => { + const url = buildConnectionUrl( + makeConn({ db_type: "mariadb", host: "127.0.0.1", port: 3306, username: "root", database: "app" }), + "pw", + ); + expect(url).toBe("mariadb://root:pw@127.0.0.1:3306/app"); + }); + + it("omits the password in the mariadb no-password variant", () => { + const url = buildConnectionUrl( + makeConn({ db_type: "mariadb", host: "127.0.0.1", port: 3306, username: "root", database: "app" }), + null, + ); + expect(url).toBe("mariadb://root@127.0.0.1:3306/app"); + }); + it("builds a Redis URL with password but no username", () => { const url = buildConnectionUrl( makeConn({ db_type: "redis", host: "localhost", port: 6379, username: null, database: "0" }), diff --git a/desktop/src/lib/connectionString.ts b/desktop/src/lib/connectionString.ts index a3740e8..cd550b1 100644 --- a/desktop/src/lib/connectionString.ts +++ b/desktop/src/lib/connectionString.ts @@ -13,6 +13,7 @@ const DB_PROTOCOLS: Record = { "postgresql:": "postgresql", "postgres:": "postgresql", "mysql:": "mysql", + "mariadb:": "mariadb", "sqlite:": "sqlite", "file:": "sqlite", "redis:": "redis", @@ -22,6 +23,7 @@ const DB_PROTOCOLS: Record = { const DEFAULT_PORTS: Record = { postgresql: 5432, mysql: 3306, + mariadb: 3306, sqlite: null, redis: 6379, }; @@ -127,6 +129,8 @@ export function buildConnectionUrl(conn: Connection, password: string | null): s } case "mysql": return `mysql://${auth}${conn.host}${port}${db}`; + case "mariadb": + return `mariadb://${auth}${conn.host}${port}${db}`; case "sqlite": // host stores the file path; `sqlite:///abs/path` round-trips through // parseConnectionString (pathname → host). diff --git a/desktop/src/lib/dbCapabilities.test.ts b/desktop/src/lib/dbCapabilities.test.ts index e45b1d9..6209467 100644 --- a/desktop/src/lib/dbCapabilities.test.ts +++ b/desktop/src/lib/dbCapabilities.test.ts @@ -23,6 +23,16 @@ describe("dbCapabilities", () => { expect(c.visualizer).toBe(false); }); + it("gives MariaDB the same capability set as MySQL", () => { + expect(DB_CAPABILITIES.mariadb).toEqual(DB_CAPABILITIES.mysql); + }); + + it("covers exactly the five DbTypes", () => { + expect(Object.keys(DB_CAPABILITIES).sort()).toEqual( + ["mariadb", "mysql", "postgresql", "redis", "sqlite"], + ); + }); + it("gives SQLite explorer/queries/visualizer/editing/import/ddl/tools/tableManagement but not objects", () => { const c = DB_CAPABILITIES.sqlite; expect(c.explorer).toBe(true); diff --git a/desktop/src/lib/dbCapabilities.ts b/desktop/src/lib/dbCapabilities.ts index 9302643..2543c18 100644 --- a/desktop/src/lib/dbCapabilities.ts +++ b/desktop/src/lib/dbCapabilities.ts @@ -36,6 +36,7 @@ const ALL_FALSE: DbCapabilities = { export const DB_CAPABILITIES: Record = { postgresql: { ...ALL_FALSE, explorer: true, queries: true, objects: true, visualizer: true, tools: true, editing: true, import: true, ddl: true, objectCrud: true, maintenance: true, roles: true, tableManagement: true }, mysql: { ...ALL_FALSE, explorer: true, queries: true, editing: true, import: true, ddl: true, tools: true }, + mariadb: { ...ALL_FALSE, explorer: true, queries: true, editing: true, import: true, ddl: true, tools: true }, sqlite: { ...ALL_FALSE, explorer: true, queries: true, visualizer: true, editing: true, import: true, ddl: true, tools: true, tableManagement: true }, redis: { ...ALL_FALSE }, }; diff --git a/desktop/src/lib/dbIcons.tsx b/desktop/src/lib/dbIcons.tsx index 6013cba..8bd8c21 100644 --- a/desktop/src/lib/dbIcons.tsx +++ b/desktop/src/lib/dbIcons.tsx @@ -1,10 +1,11 @@ -import { siPostgresql, siMysql, siSqlite, siRedis, siSupabase, siNeon } from "simple-icons"; +import { siPostgresql, siMysql, siMariadb, siSqlite, siRedis, siSupabase, siNeon } from "simple-icons"; import type { DbType } from "./types"; // Brand colors from simple-icons const DB_COLORS: Record = { postgresql: `#${siPostgresql.hex}`, mysql: `#${siMysql.hex}`, + mariadb: `#${siMariadb.hex}`, redis: `#${siRedis.hex}`, sqlite: `#${siSqlite.hex}`, }; @@ -13,6 +14,7 @@ const DB_COLORS: Record = { const DB_PATHS: Record = { postgresql: siPostgresql.path, mysql: siMysql.path, + mariadb: siMariadb.path, redis: siRedis.path, sqlite: siSqlite.path, }; @@ -20,6 +22,7 @@ const DB_PATHS: Record = { export const DB_LABELS: Record = { postgresql: "PostgreSQL", mysql: "MySQL", + mariadb: "MariaDB", redis: "Redis", sqlite: "SQLite", }; @@ -54,6 +57,7 @@ export function DbIcon({ type, size = 20, className }: DbIconProps) { export const DB_ICONS: Record = { postgresql: "🐘", mysql: "🐬", + mariadb: "🦭", redis: "⚡", sqlite: "🗄️", }; diff --git a/desktop/src/lib/providers.test.ts b/desktop/src/lib/providers.test.ts index 8811ff0..2242b28 100644 --- a/desktop/src/lib/providers.test.ts +++ b/desktop/src/lib/providers.test.ts @@ -2,12 +2,19 @@ import { describe, it, expect } from "vitest"; import { PROVIDER_TABS, getProviderById, SETUP_GUIDES } from "./providers"; describe("providers", () => { - it("defines exactly 6 provider tabs in order", () => { + it("defines exactly 7 provider tabs in order", () => { expect(PROVIDER_TABS.map((p) => p.id)).toEqual([ - "postgresql", "mysql", "sqlite", "redis", "supabase", "neon", + "postgresql", "mysql", "mariadb", "sqlite", "redis", "supabase", "neon", ]); }); + it("includes a plain (non-managed) MariaDB tab persisting db_type mariadb", () => { + const tab = getProviderById("mariadb")!; + expect(tab.dbType).toBe("mariadb"); + expect(tab.isManagedPreset).toBe(false); + expect(tab.label).toBe("MariaDB"); + }); + it("marks only supabase and neon as managed presets mapping to postgresql", () => { const pg = getProviderById("postgresql")!; expect(pg.dbType).toBe("postgresql"); diff --git a/desktop/src/lib/providers.ts b/desktop/src/lib/providers.ts index ab0f427..6789753 100644 --- a/desktop/src/lib/providers.ts +++ b/desktop/src/lib/providers.ts @@ -1,6 +1,6 @@ import type { DbType } from "./types"; -export type ProviderId = "postgresql" | "mysql" | "sqlite" | "redis" | "supabase" | "neon"; +export type ProviderId = "postgresql" | "mysql" | "mariadb" | "sqlite" | "redis" | "supabase" | "neon"; export interface ProviderTab { id: ProviderId; @@ -15,6 +15,7 @@ export interface ProviderTab { export const PROVIDER_TABS: ProviderTab[] = [ { id: "postgresql", label: "PostgreSQL", dbType: "postgresql", isManagedPreset: false }, { id: "mysql", label: "MySQL", dbType: "mysql", isManagedPreset: false }, + { id: "mariadb", label: "MariaDB", dbType: "mariadb", isManagedPreset: false }, { id: "sqlite", label: "SQLite", dbType: "sqlite", isManagedPreset: false }, { id: "redis", label: "Redis", dbType: "redis", isManagedPreset: false }, { id: "supabase", label: "Supabase", dbType: "postgresql", isManagedPreset: true }, diff --git a/desktop/src/lib/types.ts b/desktop/src/lib/types.ts index 6c9d206..721472a 100644 --- a/desktop/src/lib/types.ts +++ b/desktop/src/lib/types.ts @@ -1,4 +1,4 @@ -export type DbType = "postgresql" | "mysql" | "sqlite" | "redis"; +export type DbType = "postgresql" | "mysql" | "mariadb" | "sqlite" | "redis"; export type Theme = "dark" | "light" | "system"; export type FontSize = "small" | "medium" | "large"; diff --git a/desktop/src/lib/utils.ts b/desktop/src/lib/utils.ts index 7851b29..d6cd31b 100644 --- a/desktop/src/lib/utils.ts +++ b/desktop/src/lib/utils.ts @@ -31,7 +31,7 @@ export function abbreviateType(dataType: string): string { return TYPE_ABBREV[lower] ?? dataType; } -const VALID_DB_TYPES: DbType[] = ["postgresql", "mysql", "sqlite", "redis"]; +const VALID_DB_TYPES: DbType[] = ["postgresql", "mysql", "mariadb", "sqlite", "redis"]; export function validateConnectionInput(input: ConnectionInput): ValidationResult { if (!input.name || input.name.length === 0) return { ok: false, error: "name is required" };