Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/infra/command-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions src/infra/engines/mysql/mysql.engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 10 additions & 8 deletions src/infra/engines/postgres/postgres.engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down
42 changes: 42 additions & 0 deletions src/infra/engines/sql-filter.ts
Original file line number Diff line number Diff line change
@@ -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 <db> / \c <db> — psql meta-commands
* MySQL: USE <db>; — 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");
}
89 changes: 89 additions & 0 deletions tests/unit/infra/sql-filter.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});