Skip to content

Commit f506b5c

Browse files
Merge pull request #43 from herdux/fix/restore-strip-sql-directives
fix(restore): strip database-redirect directives from plain SQL before restore
2 parents e7bdcc1 + 289e840 commit f506b5c

File tree

9 files changed

+164
-13
lines changed

9 files changed

+164
-13
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
66

77
---
88

9+
## [0.9.1] - 2026-03-10
10+
11+
### Fixed
12+
13+
- `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`.
14+
15+
---
16+
917
## [0.9.0] - 2026-03-04
1018

1119
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
A fast, interactive CLI that removes friction from daily local database workflows, especially when juggling multiple instances and large datasets.
1414

15-
![Version](https://img.shields.io/badge/version-0.9.0-blue.svg)
15+
![Version](https://img.shields.io/badge/version-0.9.1-blue.svg)
1616
![License](https://img.shields.io/badge/license-MIT-green.svg)
1717
![Node](https://img.shields.io/badge/node-18%2B-43853d.svg)
1818
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=flat&logo=postgresql&logoColor=white)

README.pt-BR.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
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.
1414

15-
![Version](https://img.shields.io/badge/version-0.9.0-blue.svg)
15+
![Version](https://img.shields.io/badge/version-0.9.1-blue.svg)
1616
![License](https://img.shields.io/badge/license-MIT-green.svg)
1717
![Node](https://img.shields.io/badge/node-18%2B-43853d.svg)
1818
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=flat&logo=postgresql&logoColor=white)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "herdux-cli",
3-
"version": "0.9.0",
3+
"version": "0.9.1",
44
"description": "Modern Database workflow CLI focused on developer experience, safety, and automation.",
55
"repository": {
66
"type": "git",

src/infra/command-runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface RunOptions {
1212
timeout?: number;
1313
/** Path to a file whose contents will be piped into stdin */
1414
stdin?: string;
15+
/** Raw string content to pipe into stdin directly */
16+
stdinContent?: string;
1517
}
1618

1719
export async function runCommand(
@@ -25,6 +27,7 @@ export async function runCommand(
2527
...(options.cwd && { cwd: options.cwd }),
2628
...(options.env && { env: { ...process.env, ...options.env } }),
2729
...(options.stdin && { inputFile: options.stdin }),
30+
...(options.stdinContent !== undefined && { input: options.stdinContent }),
2831
};
2932
const result = await execa(command, args, execaOptions);
3033

src/infra/engines/mysql/mysql.engine.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
DatabaseInfo,
77
HealthCheck,
88
} from "../../../core/interfaces/database-engine.interface.js";
9-
import { existsSync, mkdirSync } from "fs";
9+
import { existsSync, mkdirSync, readFileSync } from "fs";
1010
import { join, resolve } from "path";
11+
import { filterSqlDirectives } from "../sql-filter.js";
1112
import { execa } from "execa";
1213
import { checkMysqlClient, checkMysqlDump } from "./mysql-env.js";
1314

@@ -385,12 +386,18 @@ export class MysqlEngine implements IDatabaseEngine {
385386
// Database might already exist — that's fine
386387
}
387388

389+
// Filter USE / CREATE DATABASE directives so mysql cannot redirect to a
390+
// different database (common in mysqldump --databases exports).
391+
const filteredSql = filterSqlDirectives(
392+
readFileSync(resolvedPath, "utf-8"),
393+
);
394+
388395
const args = [...buildConnectionArgs(opts), dbName];
389396

390397
const result = await runCommand("mysql", args, {
391398
env: buildEnv(opts),
392399
timeout: 0,
393-
stdin: resolvedPath,
400+
stdinContent: filteredSql,
394401
});
395402

396403
if (result.exitCode !== 0) {

src/infra/engines/postgres/postgres.engine.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
DatabaseInstance,
77
DatabaseInfo,
88
} from "../../../core/interfaces/database-engine.interface.js";
9-
import { existsSync, mkdirSync } from "fs";
9+
import { existsSync, mkdirSync, readFileSync } from "fs";
1010
import { join, resolve } from "path";
11+
import { filterSqlDirectives } from "../sql-filter.js";
1112
import { execa } from "execa";
1213
import { checkPostgresClient, checkPgDump } from "./postgres-env.js";
1314

@@ -425,17 +426,18 @@ export class PostgresEngine implements IDatabaseEngine {
425426
: resolvedPath.toLowerCase().endsWith(".sql");
426427

427428
if (isPlainFormat) {
428-
const args = [
429-
...buildConnectionArgs(opts),
430-
"-d",
431-
dbName,
432-
"-f",
433-
resolvedPath,
434-
];
429+
// Filter \connect / CREATE DATABASE directives so psql cannot redirect to
430+
// a different database (common in pg_dump -C and pgAdmin exports).
431+
const filteredSql = filterSqlDirectives(
432+
readFileSync(resolvedPath, "utf-8"),
433+
);
434+
435+
const args = [...buildConnectionArgs(opts), "-d", dbName];
435436

436437
const result = await runCommand("psql", args, {
437438
env: buildEnv(opts),
438439
timeout: 0,
440+
stdinContent: filteredSql,
439441
});
440442

441443
if (result.exitCode !== 0) {

src/infra/engines/sql-filter.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Strips database-redirect directives from a plain SQL file before restore.
3+
*
4+
* Some dump tools (pg_dump -C, pgAdmin, mysqldump --databases) embed statements
5+
* that redirect the connection to the source database, overriding the --db target
6+
* specified by the user. This function removes those directives so the restore
7+
* always targets the correct database.
8+
*
9+
* Stripped patterns:
10+
* PostgreSQL: \connect <db> / \c <db> — psql meta-commands
11+
* MySQL: USE <db>; — MySQL USE statement
12+
* Both: CREATE DATABASE ...; — database creation block
13+
* DROP DATABASE ...; — database drop block
14+
*/
15+
export function filterSqlDirectives(content: string): string {
16+
const lines: string[] = [];
17+
let insideBlock = false;
18+
19+
for (const line of content.split("\n")) {
20+
const trimmed = line.trim();
21+
22+
// PostgreSQL: \connect and \c meta-commands redirect psql to another DB
23+
if (/^\\(connect|c)\b/i.test(trimmed)) continue;
24+
25+
// MySQL: USE statement redirects mysql client to another DB
26+
if (/^USE\s+/i.test(trimmed)) continue;
27+
28+
// CREATE DATABASE / DROP DATABASE may span multiple lines (WITH clause, etc.)
29+
if (/^(CREATE|DROP)\s+DATABASE\b/i.test(trimmed)) {
30+
insideBlock = true;
31+
}
32+
33+
if (insideBlock) {
34+
if (trimmed.endsWith(";")) insideBlock = false;
35+
continue;
36+
}
37+
38+
lines.push(line);
39+
}
40+
41+
return lines.join("\n");
42+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { filterSqlDirectives } from "../../../src/infra/engines/sql-filter.js";
2+
3+
describe("filterSqlDirectives()", () => {
4+
// --- PostgreSQL directives ---
5+
6+
it("removes \\connect meta-commands", () => {
7+
const input =
8+
"SET lock_timeout = 0;\n\\connect sateus\nCREATE TABLE t (id int);";
9+
const result = filterSqlDirectives(input);
10+
expect(result).not.toContain("\\connect");
11+
expect(result).toContain("SET lock_timeout = 0;");
12+
expect(result).toContain("CREATE TABLE t");
13+
});
14+
15+
it("removes \\c shorthand meta-commands", () => {
16+
const input = "\\c sateus\nSELECT 1;";
17+
const result = filterSqlDirectives(input);
18+
expect(result).not.toContain("\\c sateus");
19+
expect(result).toContain("SELECT 1;");
20+
});
21+
22+
it("removes single-line CREATE DATABASE statement", () => {
23+
const input = "CREATE DATABASE sateus;\nCREATE TABLE t (id int);";
24+
const result = filterSqlDirectives(input);
25+
expect(result).not.toContain("CREATE DATABASE");
26+
expect(result).toContain("CREATE TABLE t");
27+
});
28+
29+
it("removes multi-line CREATE DATABASE block", () => {
30+
const input = [
31+
"CREATE DATABASE sateus",
32+
" WITH TEMPLATE = template0",
33+
" ENCODING = 'UTF8';",
34+
"CREATE TABLE t (id int);",
35+
].join("\n");
36+
const result = filterSqlDirectives(input);
37+
expect(result).not.toContain("CREATE DATABASE");
38+
expect(result).not.toContain("WITH TEMPLATE");
39+
expect(result).toContain("CREATE TABLE t");
40+
});
41+
42+
it("removes DROP DATABASE statement", () => {
43+
const input = "DROP DATABASE IF EXISTS sateus;\nCREATE TABLE t (id int);";
44+
const result = filterSqlDirectives(input);
45+
expect(result).not.toContain("DROP DATABASE");
46+
expect(result).toContain("CREATE TABLE t");
47+
});
48+
49+
// --- MySQL directives ---
50+
51+
it("removes USE statement", () => {
52+
const input = "USE `sateus`;\nCREATE TABLE t (id int);";
53+
const result = filterSqlDirectives(input);
54+
expect(result).not.toContain("USE `sateus`");
55+
expect(result).toContain("CREATE TABLE t");
56+
});
57+
58+
it("removes USE statement without backticks", () => {
59+
const input = "USE sateus;\nINSERT INTO t VALUES (1);";
60+
const result = filterSqlDirectives(input);
61+
expect(result).not.toContain("USE sateus");
62+
expect(result).toContain("INSERT INTO t");
63+
});
64+
65+
// --- Case insensitivity ---
66+
67+
it("is case-insensitive for directives", () => {
68+
const input = "create database foo;\nuse bar;\n\\Connect baz\nSELECT 1;";
69+
const result = filterSqlDirectives(input);
70+
expect(result).not.toContain("create database");
71+
expect(result).not.toContain("use bar");
72+
expect(result).not.toContain("\\Connect");
73+
expect(result).toContain("SELECT 1;");
74+
});
75+
76+
// --- Passthrough ---
77+
78+
it("does not alter SQL that has no redirect directives", () => {
79+
const input =
80+
"CREATE TABLE users (id serial, name text);\nINSERT INTO users VALUES (1, 'Alice');";
81+
expect(filterSqlDirectives(input)).toBe(input);
82+
});
83+
84+
it("preserves CREATE TABLE that starts with CREATE keyword", () => {
85+
const input = "CREATE TABLE database_config (key text, val text);";
86+
const result = filterSqlDirectives(input);
87+
expect(result).toContain("CREATE TABLE database_config");
88+
});
89+
});

0 commit comments

Comments
 (0)