0.8.0: MariaDB + PlanetScale, TimescaleDB, multi-result sets, streaming export, schema diff (#27)
Release / windows (push) Successful in 17m5s
Release / linux (push) Successful in 24m10s

This commit was merged in pull request #27.
This commit is contained in:
2026-09-11 17:20:41 +00:00
parent 60d472dde2
commit 5607773299
74 changed files with 5182 additions and 239 deletions
+3 -3
View File
@@ -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");
});
});
+38 -2
View File
@@ -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,
});
}
+53
View File
@@ -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" }),
+8 -1
View File
@@ -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).
+10
View File
@@ -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);
+1
View File
@@ -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 },
};
+8 -2
View File
@@ -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 {
+9 -9
View File
@@ -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"');
});
});
+23 -1
View File
@@ -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`");
});
});
+23 -7
View File
@@ -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";
+25 -2
View File
@@ -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);
});
});
+15 -2
View File
@@ -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,
+52 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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 -2
View File
@@ -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");
});
});