0.8.0: MariaDB + PlanetScale, TimescaleDB, multi-result sets, streaming export, schema diff #27
@@ -31,6 +31,7 @@ type Stage = "entry" | "configured";
|
|||||||
const FALLBACK_PORTS: Record<string, number> = {
|
const FALLBACK_PORTS: Record<string, number> = {
|
||||||
postgresql: 5432,
|
postgresql: 5432,
|
||||||
mysql: 3306,
|
mysql: 3306,
|
||||||
|
mariadb: 3306,
|
||||||
redis: 6379,
|
redis: 6379,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { DbType } from "../../lib/types";
|
|||||||
const DB_TYPE_LABELS: Record<DbType, string> = {
|
const DB_TYPE_LABELS: Record<DbType, string> = {
|
||||||
postgresql: "PostgreSQL",
|
postgresql: "PostgreSQL",
|
||||||
mysql: "MySQL",
|
mysql: "MySQL",
|
||||||
|
mariadb: "MariaDB",
|
||||||
sqlite: "SQLite",
|
sqlite: "SQLite",
|
||||||
redis: "Redis",
|
redis: "Redis",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { DbType } from "../../lib/types";
|
|||||||
const DB_TYPES: { value: DbType; label: string }[] = [
|
const DB_TYPES: { value: DbType; label: string }[] = [
|
||||||
{ value: "postgresql", label: "PostgreSQL" },
|
{ value: "postgresql", label: "PostgreSQL" },
|
||||||
{ value: "mysql", label: "MySQL" },
|
{ value: "mysql", label: "MySQL" },
|
||||||
|
{ value: "mariadb", label: "MariaDB" },
|
||||||
{ value: "sqlite", label: "SQLite" },
|
{ value: "sqlite", label: "SQLite" },
|
||||||
{ value: "redis", label: "Redis" },
|
{ value: "redis", label: "Redis" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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", () => {
|
it("defaults Redis port to 6379 when omitted", () => {
|
||||||
const result = parseConnectionString("redis://localhost");
|
const result = parseConnectionString("redis://localhost");
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@@ -212,6 +236,22 @@ describe("buildConnectionUrl", () => {
|
|||||||
expect(url).toBe("mysql://root:pw@127.0.0.1:3306/app");
|
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", () => {
|
it("builds a Redis URL with password but no username", () => {
|
||||||
const url = buildConnectionUrl(
|
const url = buildConnectionUrl(
|
||||||
makeConn({ db_type: "redis", host: "localhost", port: 6379, username: null, database: "0" }),
|
makeConn({ db_type: "redis", host: "localhost", port: 6379, username: null, database: "0" }),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const DB_PROTOCOLS: Record<string, DbType> = {
|
|||||||
"postgresql:": "postgresql",
|
"postgresql:": "postgresql",
|
||||||
"postgres:": "postgresql",
|
"postgres:": "postgresql",
|
||||||
"mysql:": "mysql",
|
"mysql:": "mysql",
|
||||||
|
"mariadb:": "mariadb",
|
||||||
"sqlite:": "sqlite",
|
"sqlite:": "sqlite",
|
||||||
"file:": "sqlite",
|
"file:": "sqlite",
|
||||||
"redis:": "redis",
|
"redis:": "redis",
|
||||||
@@ -22,6 +23,7 @@ const DB_PROTOCOLS: Record<string, DbType> = {
|
|||||||
const DEFAULT_PORTS: Record<DbType, number | null> = {
|
const DEFAULT_PORTS: Record<DbType, number | null> = {
|
||||||
postgresql: 5432,
|
postgresql: 5432,
|
||||||
mysql: 3306,
|
mysql: 3306,
|
||||||
|
mariadb: 3306,
|
||||||
sqlite: null,
|
sqlite: null,
|
||||||
redis: 6379,
|
redis: 6379,
|
||||||
};
|
};
|
||||||
@@ -127,6 +129,8 @@ export function buildConnectionUrl(conn: Connection, password: string | null): s
|
|||||||
}
|
}
|
||||||
case "mysql":
|
case "mysql":
|
||||||
return `mysql://${auth}${conn.host}${port}${db}`;
|
return `mysql://${auth}${conn.host}${port}${db}`;
|
||||||
|
case "mariadb":
|
||||||
|
return `mariadb://${auth}${conn.host}${port}${db}`;
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
// host stores the file path; `sqlite:///abs/path` round-trips through
|
// host stores the file path; `sqlite:///abs/path` round-trips through
|
||||||
// parseConnectionString (pathname → host).
|
// parseConnectionString (pathname → host).
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ describe("dbCapabilities", () => {
|
|||||||
expect(c.visualizer).toBe(false);
|
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", () => {
|
it("gives SQLite explorer/queries/visualizer/editing/import/ddl/tools/tableManagement but not objects", () => {
|
||||||
const c = DB_CAPABILITIES.sqlite;
|
const c = DB_CAPABILITIES.sqlite;
|
||||||
expect(c.explorer).toBe(true);
|
expect(c.explorer).toBe(true);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const ALL_FALSE: DbCapabilities = {
|
|||||||
export const DB_CAPABILITIES: Record<DbType, DbCapabilities> = {
|
export const DB_CAPABILITIES: Record<DbType, DbCapabilities> = {
|
||||||
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 },
|
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 },
|
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 },
|
sqlite: { ...ALL_FALSE, explorer: true, queries: true, visualizer: true, editing: true, import: true, ddl: true, tools: true, tableManagement: true },
|
||||||
redis: { ...ALL_FALSE },
|
redis: { ...ALL_FALSE },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
import type { DbType } from "./types";
|
||||||
|
|
||||||
// Brand colors from simple-icons
|
// Brand colors from simple-icons
|
||||||
const DB_COLORS: Record<DbType, string> = {
|
const DB_COLORS: Record<DbType, string> = {
|
||||||
postgresql: `#${siPostgresql.hex}`,
|
postgresql: `#${siPostgresql.hex}`,
|
||||||
mysql: `#${siMysql.hex}`,
|
mysql: `#${siMysql.hex}`,
|
||||||
|
mariadb: `#${siMariadb.hex}`,
|
||||||
redis: `#${siRedis.hex}`,
|
redis: `#${siRedis.hex}`,
|
||||||
sqlite: `#${siSqlite.hex}`,
|
sqlite: `#${siSqlite.hex}`,
|
||||||
};
|
};
|
||||||
@@ -13,6 +14,7 @@ const DB_COLORS: Record<DbType, string> = {
|
|||||||
const DB_PATHS: Record<DbType, string> = {
|
const DB_PATHS: Record<DbType, string> = {
|
||||||
postgresql: siPostgresql.path,
|
postgresql: siPostgresql.path,
|
||||||
mysql: siMysql.path,
|
mysql: siMysql.path,
|
||||||
|
mariadb: siMariadb.path,
|
||||||
redis: siRedis.path,
|
redis: siRedis.path,
|
||||||
sqlite: siSqlite.path,
|
sqlite: siSqlite.path,
|
||||||
};
|
};
|
||||||
@@ -20,6 +22,7 @@ const DB_PATHS: Record<DbType, string> = {
|
|||||||
export const DB_LABELS: Record<DbType, string> = {
|
export const DB_LABELS: Record<DbType, string> = {
|
||||||
postgresql: "PostgreSQL",
|
postgresql: "PostgreSQL",
|
||||||
mysql: "MySQL",
|
mysql: "MySQL",
|
||||||
|
mariadb: "MariaDB",
|
||||||
redis: "Redis",
|
redis: "Redis",
|
||||||
sqlite: "SQLite",
|
sqlite: "SQLite",
|
||||||
};
|
};
|
||||||
@@ -54,6 +57,7 @@ export function DbIcon({ type, size = 20, className }: DbIconProps) {
|
|||||||
export const DB_ICONS: Record<DbType, string> = {
|
export const DB_ICONS: Record<DbType, string> = {
|
||||||
postgresql: "🐘",
|
postgresql: "🐘",
|
||||||
mysql: "🐬",
|
mysql: "🐬",
|
||||||
|
mariadb: "🦭",
|
||||||
redis: "⚡",
|
redis: "⚡",
|
||||||
sqlite: "🗄️",
|
sqlite: "🗄️",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,19 @@ import { describe, it, expect } from "vitest";
|
|||||||
import { PROVIDER_TABS, getProviderById, SETUP_GUIDES } from "./providers";
|
import { PROVIDER_TABS, getProviderById, SETUP_GUIDES } from "./providers";
|
||||||
|
|
||||||
describe("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([
|
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", () => {
|
it("marks only supabase and neon as managed presets mapping to postgresql", () => {
|
||||||
const pg = getProviderById("postgresql")!;
|
const pg = getProviderById("postgresql")!;
|
||||||
expect(pg.dbType).toBe("postgresql");
|
expect(pg.dbType).toBe("postgresql");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DbType } from "./types";
|
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 {
|
export interface ProviderTab {
|
||||||
id: ProviderId;
|
id: ProviderId;
|
||||||
@@ -15,6 +15,7 @@ export interface ProviderTab {
|
|||||||
export const PROVIDER_TABS: ProviderTab[] = [
|
export const PROVIDER_TABS: ProviderTab[] = [
|
||||||
{ id: "postgresql", label: "PostgreSQL", dbType: "postgresql", isManagedPreset: false },
|
{ id: "postgresql", label: "PostgreSQL", dbType: "postgresql", isManagedPreset: false },
|
||||||
{ id: "mysql", label: "MySQL", dbType: "mysql", 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: "sqlite", label: "SQLite", dbType: "sqlite", isManagedPreset: false },
|
||||||
{ id: "redis", label: "Redis", dbType: "redis", isManagedPreset: false },
|
{ id: "redis", label: "Redis", dbType: "redis", isManagedPreset: false },
|
||||||
{ id: "supabase", label: "Supabase", dbType: "postgresql", isManagedPreset: true },
|
{ id: "supabase", label: "Supabase", dbType: "postgresql", isManagedPreset: true },
|
||||||
|
|||||||
@@ -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 Theme = "dark" | "light" | "system";
|
||||||
export type FontSize = "small" | "medium" | "large";
|
export type FontSize = "small" | "medium" | "large";
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function abbreviateType(dataType: string): string {
|
|||||||
return TYPE_ABBREV[lower] ?? dataType;
|
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 {
|
export function validateConnectionInput(input: ConnectionInput): ValidationResult {
|
||||||
if (!input.name || input.name.length === 0) return { ok: false, error: "name is required" };
|
if (!input.name || input.name.length === 0) return { ok: false, error: "name is required" };
|
||||||
|
|||||||
Reference in New Issue
Block a user