0.8.0: MariaDB + PlanetScale, TimescaleDB, multi-result sets, streaming export, schema diff (#27)
This commit was merged in pull request #27.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import tauriConf from "../../src-tauri/tauri.conf.json";
|
||||
|
||||
describe("tauri bundle config (v0.7.15)", () => {
|
||||
describe("tauri bundle config (v0.8.0)", () => {
|
||||
it("declares bundled pg_tools resources", () => {
|
||||
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
||||
});
|
||||
it("version is 0.7.15", () => {
|
||||
expect(tauriConf.version).toBe("0.7.15");
|
||||
it("version is 0.8.0", () => {
|
||||
expect(tauriConf.version).toBe("0.8.0");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, MySqlToolStatus, MySqlBackupOptions, MySqlRestoreOptions, SqliteBackupOptions, SqliteRestoreOptions, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo, RoleInfo, PrivilegeEntry, RebuildReadiness, MaintenanceResult, TablespaceInfo, ColumnInfo } from "./types";
|
||||
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, MySqlToolStatus, MySqlBackupOptions, MySqlRestoreOptions, SqliteBackupOptions, SqliteRestoreOptions, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo, RoleInfo, PrivilegeEntry, RebuildReadiness, MaintenanceResult, TablespaceInfo, ColumnInfo, HypertableListResponse, MultiQueryResult } from "./types";
|
||||
import type { FilterRule, SortRule } from "../stores/dbViewerStore";
|
||||
import type { ChangePayload } from "./changePayload";
|
||||
import type { DiffReport } from "./types";
|
||||
import { buildObjectDdl as buildObjectDdlImpl, type ObjectKind, type DdlParams } from "./objectCrud";
|
||||
|
||||
export { type ObjectKind, type DdlParams };
|
||||
@@ -226,6 +227,13 @@ export async function getExtensions(connectionId: string): Promise<ExtensionInfo
|
||||
return invoke<ExtensionInfo[]>("get_extensions", { connectionId });
|
||||
}
|
||||
|
||||
export async function getHypertables(
|
||||
connectionId: string,
|
||||
schema?: string,
|
||||
): Promise<HypertableListResponse> {
|
||||
return invoke<HypertableListResponse>("get_hypertables", { connectionId, schema });
|
||||
}
|
||||
|
||||
export async function getSchemaGraph(
|
||||
connectionId: string,
|
||||
schema?: string,
|
||||
@@ -279,6 +287,24 @@ export async function executeQuery(
|
||||
return invoke<QueryResult>("execute_query", { connectionId, query, page, pageSize });
|
||||
}
|
||||
|
||||
export async function executeQueryMulti(
|
||||
connectionId: string,
|
||||
query: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<MultiQueryResult> {
|
||||
return invoke<MultiQueryResult>("execute_query_multi", { connectionId, query, page, pageSize });
|
||||
}
|
||||
|
||||
export async function exportQueryToFile(
|
||||
connectionId: string,
|
||||
sql: string,
|
||||
format: string,
|
||||
path: string,
|
||||
): Promise<{ rows_written: number; path: string }> {
|
||||
return invoke("export_query_to_file", { connectionId, sql, format, path });
|
||||
}
|
||||
|
||||
export async function getQueryHistory(
|
||||
connectionId: string | null,
|
||||
limit: number,
|
||||
@@ -393,4 +419,14 @@ export async function getTablespaces(connectionId: string): Promise<TablespaceIn
|
||||
}
|
||||
export async function buildRebuildScript<C extends { name: string; type: string; nullable: boolean; default: string | null; is_pk: boolean }[]>(connectionId: string, schema: string, table: string, newColumns: C): Promise<string> {
|
||||
return invoke<string>("build_rebuild_script", { connectionId, schema, table, newColumns });
|
||||
}
|
||||
}
|
||||
export async function compareSchemas(
|
||||
sourceConnectionId: string,
|
||||
sourceSchema: string,
|
||||
targetConnectionId: string,
|
||||
targetSchema: string,
|
||||
): Promise<DiffReport> {
|
||||
return invoke<DiffReport>("compare_schemas", {
|
||||
sourceConnectionId, sourceSchema, targetConnectionId, targetSchema,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
@@ -170,6 +194,19 @@ describe("detectProviderFromHost", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectProviderFromHost", () => {
|
||||
it("detects PlanetScale access hosts", () => {
|
||||
expect(detectProviderFromHost("xxxx.us-east-2.psdb.cloud")).toBe("planetscale");
|
||||
expect(detectProviderFromHost("aws.connect.psdb.cloud")).toBe("planetscale");
|
||||
});
|
||||
|
||||
it("still detects supabase and neon", () => {
|
||||
expect(detectProviderFromHost("db.abc.supabase.co")).toBe("supabase");
|
||||
expect(detectProviderFromHost("ep-1-pooler.eu.aws.neon.tech")).toBe("neon");
|
||||
expect(detectProviderFromHost("example.com")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConnectionUrl", () => {
|
||||
it("builds a PostgreSQL URL with password and sslmode", () => {
|
||||
const url = buildConnectionUrl(
|
||||
@@ -212,6 +249,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" }),
|
||||
|
||||
@@ -13,6 +13,7 @@ const DB_PROTOCOLS: Record<string, DbType> = {
|
||||
"postgresql:": "postgresql",
|
||||
"postgres:": "postgresql",
|
||||
"mysql:": "mysql",
|
||||
"mariadb:": "mariadb",
|
||||
"sqlite:": "sqlite",
|
||||
"file:": "sqlite",
|
||||
"redis:": "redis",
|
||||
@@ -22,6 +23,7 @@ const DB_PROTOCOLS: Record<string, DbType> = {
|
||||
const DEFAULT_PORTS: Record<DbType, number | null> = {
|
||||
postgresql: 5432,
|
||||
mysql: 3306,
|
||||
mariadb: 3306,
|
||||
sqlite: null,
|
||||
redis: 6379,
|
||||
};
|
||||
@@ -83,10 +85,13 @@ export function looksLikeConnectionString(input: string): boolean {
|
||||
/** Detect a managed-PostgreSQL provider from a connection host, purely for
|
||||
* UI highlighting. Returns `"supabase"` / `"neon"` / `null`. db_type is
|
||||
* unaffected (both presets persist as `postgresql`). */
|
||||
export function detectProviderFromHost(host: string): "supabase" | "neon" | null {
|
||||
export function detectProviderFromHost(
|
||||
host: string,
|
||||
): "supabase" | "neon" | "planetscale" | null {
|
||||
const h = host.toLowerCase();
|
||||
if (h.endsWith(".supabase.co")) return "supabase";
|
||||
if (h.endsWith(".neon.tech")) return "neon";
|
||||
if (h.endsWith(".psdb.cloud")) return "planetscale";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -127,6 +132,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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,7 @@ const ALL_FALSE: 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 },
|
||||
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 },
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { siPostgresql, siMysql, siSqlite, siRedis, siSupabase, siNeon } from "simple-icons";
|
||||
import { siPostgresql, siMysql, siMariadb, siSqlite, siRedis, siSupabase, siNeon, siPlanetscale } from "simple-icons";
|
||||
import type { DbType } from "./types";
|
||||
|
||||
// Brand colors from simple-icons
|
||||
const DB_COLORS: Record<DbType, string> = {
|
||||
postgresql: `#${siPostgresql.hex}`,
|
||||
mysql: `#${siMysql.hex}`,
|
||||
mariadb: `#${siMariadb.hex}`,
|
||||
redis: `#${siRedis.hex}`,
|
||||
sqlite: `#${siSqlite.hex}`,
|
||||
};
|
||||
@@ -13,6 +14,7 @@ const DB_COLORS: Record<DbType, string> = {
|
||||
const DB_PATHS: Record<DbType, string> = {
|
||||
postgresql: siPostgresql.path,
|
||||
mysql: siMysql.path,
|
||||
mariadb: siMariadb.path,
|
||||
redis: siRedis.path,
|
||||
sqlite: siSqlite.path,
|
||||
};
|
||||
@@ -20,6 +22,7 @@ const DB_PATHS: Record<DbType, string> = {
|
||||
export const DB_LABELS: Record<DbType, string> = {
|
||||
postgresql: "PostgreSQL",
|
||||
mysql: "MySQL",
|
||||
mariadb: "MariaDB",
|
||||
redis: "Redis",
|
||||
sqlite: "SQLite",
|
||||
};
|
||||
@@ -54,22 +57,25 @@ export function DbIcon({ type, size = 20, className }: DbIconProps) {
|
||||
export const DB_ICONS: Record<DbType, string> = {
|
||||
postgresql: "🐘",
|
||||
mysql: "🐬",
|
||||
mariadb: "🦭",
|
||||
redis: "⚡",
|
||||
sqlite: "🗄️",
|
||||
};
|
||||
|
||||
// ── Managed-PostgreSQL provider icons (Supabase, NeonDB) ──────────
|
||||
// These are NOT DbType values; connections persist as db_type="postgresql".
|
||||
type ProviderIconId = "supabase" | "neon";
|
||||
type ProviderIconId = "supabase" | "neon" | "planetscale";
|
||||
|
||||
const PROVIDER_ICON_DATA: Record<ProviderIconId, { hex: string; path: string }> = {
|
||||
supabase: { hex: `#${siSupabase.hex}`, path: siSupabase.path },
|
||||
neon: { hex: `#${siNeon.hex}`, path: siNeon.path },
|
||||
planetscale: { hex: `#${siPlanetscale.hex}`, path: siPlanetscale.path },
|
||||
};
|
||||
|
||||
export const PROVIDER_LABELS: Record<ProviderIconId, string> = {
|
||||
supabase: "Supabase",
|
||||
neon: "NeonDB",
|
||||
planetscale: "PlanetScale",
|
||||
};
|
||||
|
||||
interface ProviderIconProps {
|
||||
|
||||
@@ -5,7 +5,7 @@ import agents from "../../../AGENTS.md?raw";
|
||||
import readme from "../../../README.md?raw";
|
||||
import landing from "../../../www/public/landing.js?raw";
|
||||
|
||||
describe("v0.7.15 docs coverage", () => {
|
||||
describe("v0.8.0 docs coverage", () => {
|
||||
it("AGENTS.md marks inline cell editing complete", () => {
|
||||
expect(agents).toContain("Inline cell editing");
|
||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||
@@ -25,8 +25,8 @@ describe("v0.7.15 docs coverage", () => {
|
||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||
});
|
||||
it("README declares v0.7.15", () => {
|
||||
expect(readme).toContain("0.7.15");
|
||||
it("README declares v0.8.0", () => {
|
||||
expect(readme).toContain("0.8.0");
|
||||
});
|
||||
it("AGENTS.md marks schema CRUD complete", () => {
|
||||
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
||||
@@ -65,12 +65,12 @@ describe("v0.7.15 docs coverage", () => {
|
||||
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
||||
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
||||
});
|
||||
it("README links to v0.7.15 assets in both download tables", () => {
|
||||
expect(readme).toContain("releases/download/v0.7.15/");
|
||||
expect(readme).toContain("Gridline_0.7.15_aarch64.dmg");
|
||||
expect(readme).toContain("Gridline-0.7.15-1.x86_64.rpm");
|
||||
it("README links to v0.8.0 assets in both download tables", () => {
|
||||
expect(readme).toContain("releases/download/v0.8.0/");
|
||||
expect(readme).toContain("Gridline_0.8.0_aarch64.dmg");
|
||||
expect(readme).toContain("Gridline-0.8.0-1.x86_64.rpm");
|
||||
});
|
||||
it("landing.js platform download links point to v0.7.15", () => {
|
||||
expect(landing).toContain('const version = "0.7.15"');
|
||||
it("landing.js platform download links point to v0.8.0", () => {
|
||||
expect(landing).toContain('const version = "0.8.0"');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { exportData } from "./exportData";
|
||||
import { exportData, csvCell, qualifiedTableSql } from "./exportData";
|
||||
import type { ColumnInfo } from "./types";
|
||||
|
||||
const columns: ColumnInfo[] = [
|
||||
@@ -48,4 +48,26 @@ describe("exportData", () => {
|
||||
globalThis.URL.revokeObjectURL = origRevokeObjectURL;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("csvCell (client CSV parity with streamed export)", () => {
|
||||
it("quotes and guards like the Rust writer", () => {
|
||||
expect(csvCell("plain")).toBe("plain");
|
||||
expect(csvCell("a,b")).toBe('"a,b"');
|
||||
expect(csvCell('a"b')).toBe('"a""b"');
|
||||
expect(csvCell("=SUM(A1)")).toBe("'=SUM(A1)");
|
||||
expect(csvCell("+x")).toBe("'+x");
|
||||
expect(csvCell("-1")).toBe("'-1");
|
||||
expect(csvCell("@cmd")).toBe("'@cmd");
|
||||
expect(csvCell(null)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifiedTableSql", () => {
|
||||
it("double-quotes for PG/SQLite and backticks for MySQL/MariaDB", () => {
|
||||
expect(qualifiedTableSql("postgresql", "public", "users")).toBe('SELECT * FROM "public"."users"');
|
||||
expect(qualifiedTableSql("sqlite", "main", "users")).toBe('SELECT * FROM "main"."users"');
|
||||
expect(qualifiedTableSql("mysql", "app", "users")).toBe("SELECT * FROM `app`.`users`");
|
||||
expect(qualifiedTableSql("mariadb", "app", "users")).toBe("SELECT * FROM `app`.`users`");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,27 @@
|
||||
import { buildXlsx } from "./xlsx";
|
||||
import type { ColumnInfo } from "./types";
|
||||
|
||||
/** Quote one CSV cell with formula-injection protection — mirrors the Rust
|
||||
* `csv_cell` from the streaming export (Task 10) byte-for-byte. */
|
||||
export function csvCell(value: unknown): string {
|
||||
const s = value === null || value === undefined ? "" : String(value);
|
||||
const guarded = /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
|
||||
if (/[,"\n\r]/.test(guarded)) {
|
||||
return `"${guarded.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return guarded;
|
||||
}
|
||||
|
||||
/** SQL selecting every row of a table, dialect-quoted. */
|
||||
export function qualifiedTableSql(dbType: string, schema: string, table: string): string {
|
||||
if (dbType === "mysql" || dbType === "mariadb") {
|
||||
const q = (s: string) => s.replace(/`/g, "``");
|
||||
return `SELECT * FROM \`${q(schema)}\`.\`${q(table)}\``;
|
||||
}
|
||||
const q = (s: string) => s.replace(/"/g, '""');
|
||||
return `SELECT * FROM "${q(schema)}"."${q(table)}"`;
|
||||
}
|
||||
|
||||
export function exportData(
|
||||
rows: unknown[][],
|
||||
columns: ColumnInfo[],
|
||||
@@ -34,14 +55,9 @@ export function exportData(
|
||||
break;
|
||||
}
|
||||
case "csv": {
|
||||
const csvRows = [headers.map((h) => `"${h.replace(/"/g, '""')}"`).join(",")];
|
||||
const csvRows = [headers.map((h) => csvCell(h)).join(",")];
|
||||
for (const row of rows) {
|
||||
csvRows.push(
|
||||
row.map((cell) => {
|
||||
const s = cell === null || cell === undefined ? "" : String(cell);
|
||||
return `"${s.replace(/"/g, '""')}"`;
|
||||
}).join(","),
|
||||
);
|
||||
csvRows.push(row.map((cell) => csvCell(cell)).join(","));
|
||||
}
|
||||
content = csvRows.join("\n");
|
||||
mime = "text/csv";
|
||||
|
||||
@@ -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 8 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", "planetscale",
|
||||
]);
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -33,4 +40,20 @@ describe("providers", () => {
|
||||
expect(SETUP_GUIDES.supabase.sslRequired).toBe(true);
|
||||
expect(SETUP_GUIDES.neon.sslRequired).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PlanetScale preset", () => {
|
||||
it("registers as a managed MySQL preset persisting db_type mysql", () => {
|
||||
const tab = PROVIDER_TABS.find((p) => p.id === "planetscale");
|
||||
expect(tab).toBeDefined();
|
||||
expect(tab!.dbType).toBe("mysql");
|
||||
expect(tab!.isManagedPreset).toBe(true);
|
||||
});
|
||||
|
||||
it("ships a setup guide requiring SSL with concrete steps", () => {
|
||||
const guide = SETUP_GUIDES.planetscale;
|
||||
expect(guide.sslRequired).toBe(true);
|
||||
expect(guide.steps.length).toBeGreaterThanOrEqual(4);
|
||||
expect(guide.steps.some((s) => s.detail.includes("psdb.cloud"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -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" | "planetscale";
|
||||
|
||||
export interface ProviderTab {
|
||||
id: ProviderId;
|
||||
@@ -15,10 +15,12 @@ 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 },
|
||||
{ id: "neon", label: "NeonDB", dbType: "postgresql", isManagedPreset: true },
|
||||
{ id: "planetscale", label: "PlanetScale", dbType: "mysql", isManagedPreset: true },
|
||||
];
|
||||
|
||||
export function getProviderById(id: string): ProviderTab | undefined {
|
||||
@@ -39,7 +41,7 @@ export interface SetupGuide {
|
||||
|
||||
/** Static, researched setup instructions. No network calls at runtime.
|
||||
* Content reviewed against the official docs (fetched 2026-08-04, see spec §9). */
|
||||
export const SETUP_GUIDES: Record<"supabase" | "neon", SetupGuide> = {
|
||||
export const SETUP_GUIDES: Record<"supabase" | "neon" | "planetscale", SetupGuide> = {
|
||||
supabase: {
|
||||
blurb: "Managed PostgreSQL with a pooled connection string.",
|
||||
sslRequired: true,
|
||||
@@ -65,6 +67,17 @@ export const SETUP_GUIDES: Record<"supabase" | "neon", SetupGuide> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
planetscale: {
|
||||
blurb: "Managed MySQL (Vitess) with database branching.",
|
||||
sslRequired: true,
|
||||
steps: [
|
||||
{ title: "Create a branch password", detail: "In the PlanetScale dashboard open your database → Connect → pick a branch → Create password. The password (pscale_pw_…) is shown ONCE and works only for that branch." },
|
||||
{ title: "Copy the credentials", detail: "Note the Access Host URL (e.g. aws.connect.psdb.cloud or xxxx.us-east-2.psdb.cloud), the username, the password, and your database name." },
|
||||
{ title: "Fill Host / Port / Database", detail: "Host is the access host, port is 3306, Database is your database name. Save stores the password in your OS keychain." },
|
||||
{ title: "SSL is mandatory", detail: "Gridline pre-selects Verify Full — PlanetScale requires TLS with identity verification." },
|
||||
{ title: "Vitess caveats", detail: "Passwords are branch-scoped; deleting one disconnects its clients within 5 minutes. With Safe Migrations ON, schema changes go through deploy requests, so the changes queue may be blocked on production branches. Foreign keys follow your database's foreign_key_constraint setting." },
|
||||
],
|
||||
},
|
||||
neon: {
|
||||
blurb: "Serverless Postgres with pooled or direct endpoints.",
|
||||
sslRequired: 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 FontSize = "small" | "medium" | "large";
|
||||
@@ -189,6 +189,19 @@ export interface QueryResult {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface StatementNotice {
|
||||
statement_index: number;
|
||||
kind: "dml" | "ddl" | "error" | string;
|
||||
text: string;
|
||||
affected: number | null;
|
||||
}
|
||||
|
||||
export interface MultiQueryResult {
|
||||
result_sets: QueryResult[];
|
||||
notices: StatementNotice[];
|
||||
execution_time_ms: number;
|
||||
}
|
||||
|
||||
export type ChangeStatus = "pending" | "applied" | "error";
|
||||
|
||||
export type ChangeItemType =
|
||||
@@ -367,6 +380,7 @@ export type ObjectType =
|
||||
| "sequences"
|
||||
| "enums"
|
||||
| "extensions"
|
||||
| "hypertables"
|
||||
| "indexes"
|
||||
| "constraints"
|
||||
| "procedures"
|
||||
@@ -378,6 +392,21 @@ export interface ObjectSearchHit {
|
||||
object_type: string; // TABLE | VIEW | MATERIALIZED VIEW | FUNCTION | PROCEDURE | TRIGGER | SEQUENCE | ENUM | EXTENSION | INDEX | CONSTRAINT
|
||||
}
|
||||
|
||||
export interface HypertableInfo {
|
||||
name: string;
|
||||
schema: string;
|
||||
num_dimensions: number;
|
||||
compression_enabled: boolean;
|
||||
num_chunks: number;
|
||||
total_size_bytes: number | null;
|
||||
}
|
||||
|
||||
export interface HypertableListResponse {
|
||||
available: boolean;
|
||||
reason: string | null;
|
||||
items: HypertableInfo[];
|
||||
}
|
||||
|
||||
export interface DependencyInfo {
|
||||
deptype: string;
|
||||
class: string;
|
||||
@@ -477,4 +506,25 @@ export interface Relationship {
|
||||
export interface SchemaGraph {
|
||||
tables: TableNode[];
|
||||
relationships: Relationship[];
|
||||
}
|
||||
}
|
||||
export interface DiffDetailLine {
|
||||
label: string;
|
||||
old: string;
|
||||
new: string;
|
||||
}
|
||||
|
||||
export interface DiffItem {
|
||||
object_type: string;
|
||||
name: string;
|
||||
kind: "added" | "removed" | "changed";
|
||||
detail: DiffDetailLine[];
|
||||
sync_sql: string[];
|
||||
destructive: boolean;
|
||||
}
|
||||
|
||||
export interface DiffReport {
|
||||
items: DiffItem[];
|
||||
source_label: string;
|
||||
target_label: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
describe("version", () => {
|
||||
it("declares v0.7.15 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.7.15");
|
||||
it("declares v0.8.0 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.8.0");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user