diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e0186..702b3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and --- +## [0.9.1] - 2026-03-10 + +### Fixed + +- `restore` (PostgreSQL plain, MySQL): SQL files produced by `pg_dump -C`, pgAdmin, or `mysqldump --databases` embed `\connect`, `CREATE DATABASE`, and `USE` directives that redirected the client to the source database, ignoring `--db`. The plain SQL content is now filtered to strip these directives before being piped to the database client, so the restore always targets the database specified via `--db`. + +--- + ## [0.9.0] - 2026-03-04 ### Added diff --git a/README.md b/README.md index 1d55e8f..0ab5131 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A fast, interactive CLI that removes friction from daily local database workflows, especially when juggling multiple instances and large datasets. -![Version](https://img.shields.io/badge/version-0.9.0-blue.svg) +![Version](https://img.shields.io/badge/version-0.9.1-blue.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ![Node](https://img.shields.io/badge/node-18%2B-43853d.svg) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=flat&logo=postgresql&logoColor=white) diff --git a/README.pt-BR.md b/README.pt-BR.md index ca64fe6..ef0196c 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -12,7 +12,7 @@ Uma CLI rápida e interativa que remove a fricção dos workflows diários com bancos de dados locais, especialmente ao lidar com múltiplas instâncias e grandes datasets. -![Version](https://img.shields.io/badge/version-0.9.0-blue.svg) +![Version](https://img.shields.io/badge/version-0.9.1-blue.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ![Node](https://img.shields.io/badge/node-18%2B-43853d.svg) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=flat&logo=postgresql&logoColor=white) diff --git a/package.json b/package.json index 05555a4..3b3645e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "herdux-cli", - "version": "0.9.0", + "version": "0.9.1", "description": "Modern Database workflow CLI focused on developer experience, safety, and automation.", "repository": { "type": "git", diff --git a/src/infra/command-runner.ts b/src/infra/command-runner.ts index bcb3a4d..8c47a61 100644 --- a/src/infra/command-runner.ts +++ b/src/infra/command-runner.ts @@ -12,6 +12,8 @@ export interface RunOptions { timeout?: number; /** Path to a file whose contents will be piped into stdin */ stdin?: string; + /** Raw string content to pipe into stdin directly */ + stdinContent?: string; } export async function runCommand( @@ -25,6 +27,7 @@ export async function runCommand( ...(options.cwd && { cwd: options.cwd }), ...(options.env && { env: { ...process.env, ...options.env } }), ...(options.stdin && { inputFile: options.stdin }), + ...(options.stdinContent !== undefined && { input: options.stdinContent }), }; const result = await execa(command, args, execaOptions); diff --git a/src/infra/engines/mysql/mysql.engine.ts b/src/infra/engines/mysql/mysql.engine.ts index 4027cc9..68b791f 100644 --- a/src/infra/engines/mysql/mysql.engine.ts +++ b/src/infra/engines/mysql/mysql.engine.ts @@ -6,8 +6,9 @@ import type { DatabaseInfo, HealthCheck, } from "../../../core/interfaces/database-engine.interface.js"; -import { existsSync, mkdirSync } from "fs"; +import { existsSync, mkdirSync, readFileSync } from "fs"; import { join, resolve } from "path"; +import { filterSqlDirectives } from "../sql-filter.js"; import { execa } from "execa"; import { checkMysqlClient, checkMysqlDump } from "./mysql-env.js"; @@ -385,12 +386,18 @@ export class MysqlEngine implements IDatabaseEngine { // Database might already exist — that's fine } + // Filter USE / CREATE DATABASE directives so mysql cannot redirect to a + // different database (common in mysqldump --databases exports). + const filteredSql = filterSqlDirectives( + readFileSync(resolvedPath, "utf-8"), + ); + const args = [...buildConnectionArgs(opts), dbName]; const result = await runCommand("mysql", args, { env: buildEnv(opts), timeout: 0, - stdin: resolvedPath, + stdinContent: filteredSql, }); if (result.exitCode !== 0) { diff --git a/src/infra/engines/postgres/postgres.engine.ts b/src/infra/engines/postgres/postgres.engine.ts index 2cca160..5bbd46b 100644 --- a/src/infra/engines/postgres/postgres.engine.ts +++ b/src/infra/engines/postgres/postgres.engine.ts @@ -6,8 +6,9 @@ import type { DatabaseInstance, DatabaseInfo, } from "../../../core/interfaces/database-engine.interface.js"; -import { existsSync, mkdirSync } from "fs"; +import { existsSync, mkdirSync, readFileSync } from "fs"; import { join, resolve } from "path"; +import { filterSqlDirectives } from "../sql-filter.js"; import { execa } from "execa"; import { checkPostgresClient, checkPgDump } from "./postgres-env.js"; @@ -425,17 +426,18 @@ export class PostgresEngine implements IDatabaseEngine { : resolvedPath.toLowerCase().endsWith(".sql"); if (isPlainFormat) { - const args = [ - ...buildConnectionArgs(opts), - "-d", - dbName, - "-f", - resolvedPath, - ]; + // Filter \connect / CREATE DATABASE directives so psql cannot redirect to + // a different database (common in pg_dump -C and pgAdmin exports). + const filteredSql = filterSqlDirectives( + readFileSync(resolvedPath, "utf-8"), + ); + + const args = [...buildConnectionArgs(opts), "-d", dbName]; const result = await runCommand("psql", args, { env: buildEnv(opts), timeout: 0, + stdinContent: filteredSql, }); if (result.exitCode !== 0) { diff --git a/src/infra/engines/sql-filter.ts b/src/infra/engines/sql-filter.ts new file mode 100644 index 0000000..6522f34 --- /dev/null +++ b/src/infra/engines/sql-filter.ts @@ -0,0 +1,42 @@ +/** + * Strips database-redirect directives from a plain SQL file before restore. + * + * Some dump tools (pg_dump -C, pgAdmin, mysqldump --databases) embed statements + * that redirect the connection to the source database, overriding the --db target + * specified by the user. This function removes those directives so the restore + * always targets the correct database. + * + * Stripped patterns: + * PostgreSQL: \connect / \c — psql meta-commands + * MySQL: USE ; — MySQL USE statement + * Both: CREATE DATABASE ...; — database creation block + * DROP DATABASE ...; — database drop block + */ +export function filterSqlDirectives(content: string): string { + const lines: string[] = []; + let insideBlock = false; + + for (const line of content.split("\n")) { + const trimmed = line.trim(); + + // PostgreSQL: \connect and \c meta-commands redirect psql to another DB + if (/^\\(connect|c)\b/i.test(trimmed)) continue; + + // MySQL: USE statement redirects mysql client to another DB + if (/^USE\s+/i.test(trimmed)) continue; + + // CREATE DATABASE / DROP DATABASE may span multiple lines (WITH clause, etc.) + if (/^(CREATE|DROP)\s+DATABASE\b/i.test(trimmed)) { + insideBlock = true; + } + + if (insideBlock) { + if (trimmed.endsWith(";")) insideBlock = false; + continue; + } + + lines.push(line); + } + + return lines.join("\n"); +} diff --git a/tests/unit/infra/sql-filter.test.ts b/tests/unit/infra/sql-filter.test.ts new file mode 100644 index 0000000..e50a72f --- /dev/null +++ b/tests/unit/infra/sql-filter.test.ts @@ -0,0 +1,89 @@ +import { filterSqlDirectives } from "../../../src/infra/engines/sql-filter.js"; + +describe("filterSqlDirectives()", () => { + // --- PostgreSQL directives --- + + it("removes \\connect meta-commands", () => { + const input = + "SET lock_timeout = 0;\n\\connect sateus\nCREATE TABLE t (id int);"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("\\connect"); + expect(result).toContain("SET lock_timeout = 0;"); + expect(result).toContain("CREATE TABLE t"); + }); + + it("removes \\c shorthand meta-commands", () => { + const input = "\\c sateus\nSELECT 1;"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("\\c sateus"); + expect(result).toContain("SELECT 1;"); + }); + + it("removes single-line CREATE DATABASE statement", () => { + const input = "CREATE DATABASE sateus;\nCREATE TABLE t (id int);"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("CREATE DATABASE"); + expect(result).toContain("CREATE TABLE t"); + }); + + it("removes multi-line CREATE DATABASE block", () => { + const input = [ + "CREATE DATABASE sateus", + " WITH TEMPLATE = template0", + " ENCODING = 'UTF8';", + "CREATE TABLE t (id int);", + ].join("\n"); + const result = filterSqlDirectives(input); + expect(result).not.toContain("CREATE DATABASE"); + expect(result).not.toContain("WITH TEMPLATE"); + expect(result).toContain("CREATE TABLE t"); + }); + + it("removes DROP DATABASE statement", () => { + const input = "DROP DATABASE IF EXISTS sateus;\nCREATE TABLE t (id int);"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("DROP DATABASE"); + expect(result).toContain("CREATE TABLE t"); + }); + + // --- MySQL directives --- + + it("removes USE statement", () => { + const input = "USE `sateus`;\nCREATE TABLE t (id int);"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("USE `sateus`"); + expect(result).toContain("CREATE TABLE t"); + }); + + it("removes USE statement without backticks", () => { + const input = "USE sateus;\nINSERT INTO t VALUES (1);"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("USE sateus"); + expect(result).toContain("INSERT INTO t"); + }); + + // --- Case insensitivity --- + + it("is case-insensitive for directives", () => { + const input = "create database foo;\nuse bar;\n\\Connect baz\nSELECT 1;"; + const result = filterSqlDirectives(input); + expect(result).not.toContain("create database"); + expect(result).not.toContain("use bar"); + expect(result).not.toContain("\\Connect"); + expect(result).toContain("SELECT 1;"); + }); + + // --- Passthrough --- + + it("does not alter SQL that has no redirect directives", () => { + const input = + "CREATE TABLE users (id serial, name text);\nINSERT INTO users VALUES (1, 'Alice');"; + expect(filterSqlDirectives(input)).toBe(input); + }); + + it("preserves CREATE TABLE that starts with CREATE keyword", () => { + const input = "CREATE TABLE database_config (key text, val text);"; + const result = filterSqlDirectives(input); + expect(result).toContain("CREATE TABLE database_config"); + }); +});