Repository: Archmonger/django-dbbackup

Issue:
Error running `pg_dump`, unable to find command
Run  `python manage.py dbbackup`, get error: `[Errno 2] No such file or directory`, traceback:

```
Traceback (most recent call last):
  File "manage.py", line 23, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/lib/python2.7/site-packages/django/core/management/__init__.py", line 364, in execute_from_command_line
    utility.execute()
  File "/usr/lib/python2.7/site-packages/django/core/management/__init__.py", line 356, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/lib/python2.7/site-packages/django/core/management/base.py", line 283, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/lib/python2.7/site-packages/django/core/management/base.py", line 330, in execute
    output = self.handle(*args, **options)
  File "/usr/lib/python2.7/site-packages/dbbackup/utils.py", line 116, in wrapper
    func(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/dbbackup/management/commands/dbbackup.py", line 61, in handle
    self._save_new_backup(database)
  File "/usr/lib/python2.7/site-packages/dbbackup/management/commands/dbbackup.py", line 74, in _save_new_backup
    outputfile = self.connector.create_dump()
  File "/usr/lib/python2.7/site-packages/dbbackup/db/base.py", line 76, in create_dump
    dump = self._create_dump()
  File "/usr/lib/python2.7/site-packages/dbbackup/db/postgresql.py", line 37, in _create_dump
    stdout, stderr = self.run_command(cmd, env=self.dump_env)
  File "/usr/lib/python2.7/site-packages/dbbackup/db/postgresql.py", line 21, in run_command
    return super(PgDumpConnector, self).run_command(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/dbbackup/db/base.py", line 150, in run_command
    "Error running: {}\n{}".format(command, str(err)))
dbbackup.db.exceptions.CommandConnectorError: Error running:  pg_dump gdi --host=localhost --port=5432 --username=gdi --no-password --clean

```
Under docker compose.
Django: `1.11.7`
django-dbbackup: `3.2.0`
djangorestframework: `3.7.1`



Discussion:
Are you sure `pg_dump` is installed ?
hello, Estoy usando openshift 3, como hago para crear un backup de la base de datos en mi maquina local windows, y como lo restauro
`pg_dump` requirement should be listed on "Installation" section of ReadTheDocs
I also encountered this after attempting to run using django inside a dockerized container of alpine linux. 

As far as I can tell, Alpine's minimum  postgres dependency for `psycopg2`  is `postgres-dev`, however you must also `apk install` `postgres-client` to make `pg_dump` available on that machine.

Once you've done this, you're able to run `./manage.py dbbackup` just fine.

A few more notes and links [here](https://stackoverflow.com/a/63948100/4028977). I agree with @iAnanich about considering adding a note about this in the docs.



Patch:
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 8a9aefbe..e5d56971 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -18,6 +18,10 @@ Bootstrap, build, and test the repository:
 - `hatch run lint:check` - runs linting on the main package. Takes ~5 seconds. NEVER CANCEL.
 - `hatch run lint:format-check` - runs code formatting checks. Takes ~2 seconds. NEVER CANCEL.
 
+**Interactive Development Shell:**
+
+- `hatch shell [ENV_NAME]` - Enter an interactive shell environment with all dependencies installed. ENV_NAME is optional and defaults to the main environment. Use `hatch shell functional` for the functional test environment, `hatch shell lint` for the linting environment, etc.
+
 Build documentation:
 
 - `hatch run docs:build` - builds HTML documentation with MkDocs Material. Takes ~2 seconds. NEVER CANCEL.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1322249c..6d64f7b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ Don't forget to remove deprecated code on each major release!
 ### Changed
 
 - This repository has been transferred out of Jazzband due to logistical concerns.
+- Improve error message for missing database tools (`pg_dump`, `mysqldump`, etc.) to provide guidance instead of generic "No such file or directory" errors.
 
 ### Removed
 
diff --git a/dbbackup/db/base.py b/dbbackup/db/base.py
index 4b869fef..8545637a 100644
--- a/dbbackup/db/base.py
+++ b/dbbackup/db/base.py
@@ -230,4 +230,19 @@ def run_command(self, command, stdin=None, env=None):
             stderr.seek(0)
             return stdout, stderr
         except OSError as err:
+            # Check if this is a "command not found" error (errno 2)
+            if err.errno == 2:  # No such file or directory
+                cmd_name = shlex.split(command)[0] if command else "command"
+                error_msg = (
+                    f"Database command '{cmd_name}' not found. "
+                    f"Please ensure the required database client tools are installed.\n\n"
+                    f"For PostgreSQL: Install postgresql-client (pg_dump, psql, pg_restore)\n"
+                    f"For MySQL: Install mysql-client (mysqldump, mysql)\n"
+                    f"For MongoDB: Install mongodb-tools (mongodump, mongorestore)\n\n"
+                    f"Alternatively, you can specify custom command paths using these settings:\n"
+                    f"- DUMP_CMD: Path to the dump command\n"
+                    f"- RESTORE_CMD: Path to the restore command\n\n"
+                    f"Original error: {str(err)}"
+                )
+                raise exceptions.CommandConnectorError(error_msg)
             raise exceptions.CommandConnectorError(f"Error running: {command}\n{str(err)}")
diff --git a/docs/src/databases.md b/docs/src/databases.md
index bc3a148d..13ae8510 100644
--- a/docs/src/databases.md
+++ b/docs/src/databases.md
@@ -128,7 +128,7 @@ MySQL uses by default `dbbackup.db.mysql.MysqlDumpConnector`. It uses
 
 Postgres uses by default `dbbackup.db.postgresql.PgDumpConnector`, but
 we advise you to use `dbbackup.db.postgresql.PgDumpBinaryConnector`. The
-first one uses `pg_dump` and `pqsl` for its job, creating RAW SQL files.
+first one uses `pg_dump` and `psql` for its job, creating RAW SQL files.
 
 The second uses `pg_restore` with binary dump files.
 


Test patch:
diff --git a/tests/test_connectors/test_base.py b/tests/test_connectors/test_base.py
index 255a9102..fb4c1141 100644
--- a/tests/test_connectors/test_base.py
+++ b/tests/test_connectors/test_base.py
@@ -1,5 +1,6 @@
 import os
 from tempfile import SpooledTemporaryFile
+from unittest.mock import patch
 
 from django.test import TestCase
 
@@ -38,6 +39,43 @@ def test_run_command_error(self):
         with self.assertRaises(exceptions.CommandConnectorError):
             connector.run_command("echa 123")
 
+    def test_run_command_error_message_for_missing_command(self):
+        """Test error message when a database command is not found."""
+        connector = BaseCommandDBConnector()
+        with self.assertRaises(exceptions.CommandConnectorError) as cm:
+            connector.run_command("nonexistent_database_command_12345")
+        
+        error_message = str(cm.exception)
+        
+        # Check that the improved error message contains helpful information
+        self.assertIn("Database command 'nonexistent_database_command_12345' not found", error_message)
+        self.assertIn("Please ensure the required database client tools are installed", error_message)
+        self.assertIn("PostgreSQL: Install postgresql-client", error_message)
+        self.assertIn("MySQL: Install mysql-client", error_message)
+        self.assertIn("MongoDB: Install mongodb-tools", error_message)
+        self.assertIn("DUMP_CMD: Path to the dump command", error_message)
+        self.assertIn("RESTORE_CMD: Path to the restore command", error_message)
+
+    def test_run_command_error_message_for_other_os_errors(self):
+        """Test that non-ENOENT OSErrors still get the original error format."""
+        connector = BaseCommandDBConnector()
+        
+        # Mock OSError with a different errno to test the else branch
+        with patch('dbbackup.db.base.Popen') as mock_popen:
+            mock_popen.side_effect = OSError(13, "Permission denied")  # EACCES
+            
+            with self.assertRaises(exceptions.CommandConnectorError) as cm:
+                connector.run_command("some_command")
+            
+            error_message = str(cm.exception)
+            
+            # Should get the original error format for non-ENOENT errors
+            self.assertIn("Error running: some_command", error_message)
+            self.assertIn("Permission denied", error_message)
+            # Should NOT contain the helpful database installation message
+            self.assertNotIn("Database command", error_message)
+            self.assertNotIn("client tools are installed", error_message)
+
     def test_run_command_stdin(self):
         connector = BaseCommandDBConnector()
         stdin = SpooledTemporaryFile()