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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## [2.2.8] - 2026-01-04

### Added
- **PostgreSQL module**: Complete rewrite with feature-based architecture
- **Feature system**: Enable only the features you need via `pg_features` variable
- Available features: `core`, `sql`, `db`, `role`, `conn`, `session`, `dump`, `extension`, `table`, `index`, `maintenance`
- Default features: `core sql db dump maintenance`
- **Execution mode support** via `pg_invoke` wrapper function:
- `local`: Direct binary execution on host
- `docker`: Run via `docker exec` in `pg_dk_container`
- `docker-compose`: Run via `docker compose exec` in `pg_dc_service`
- **New targets**:
- Connection monitoring: `pg/conn/list`, `pg/conn/count`, `pg/conn/long-running`
- Session management: `pg/session/kill/%`, `pg/session/kill-idle`, `pg/session/cancel/%`
- Extension management: `pg/extension/list`, `pg/extension/available`, `pg/extension/create/%`, `pg/extension/drop/%`
- Index management: `pg/index/list`, `pg/index/unused`, `pg/index/reindex`, `pg/index/reindex/%`
- Additional: `pg/dump/info`, `pg/table/stats`, `pg/vacuum/%`, `pg/analyze`, `pg/bloat`
- Configuration via `mod_config.mk` with sensible defaults
- Tests for configuration, command building, and feature loading
- **`mb_exec_with_mode` enhancements**:
- Added `<prefix>_env` support to all mode handlers for passing environment variables
- Improved documentation explaining extensibility and how to create custom mode handlers
- Handlers (`mb_exec_with_mode_local`, `mb_exec_with_mode_docker`, `mb_exec_with_mode_docker-compose`) now automatically prepend `<prefix>_env` to commands

## [2.2.7] - 2026-01-04

### Added
Expand Down
32 changes: 24 additions & 8 deletions core/functions.mk
Original file line number Diff line number Diff line change
Expand Up @@ -563,19 +563,33 @@ $(strip
endef

## @function mb_exec_with_mode
## @desc Execute command with mode selection (local/docker/docker-compose)
## @desc Reads <prefix>_exec_mode to determine execution mode and delegates to appropriate handler.
## @desc Supports three modes: local (uses <prefix>_bin), docker (uses dk_shellc with <prefix>_dk_container),
## @desc and docker-compose (uses dc_shellc with <prefix>_dc_service).
## @desc Execute command with mode selection via extensible handler system.
## @desc Reads <prefix>_exec_mode to determine which handler to call: mb_exec_with_mode_<mode>
## @desc
## @desc Built-in modes:
## @desc - local: Runs command directly (handler in core/functions.mk)
## @desc - docker: Runs via docker exec (handler in docker module)
## @desc - docker-compose: Runs via docker compose exec (handler in docker_compose module)
## @desc
## @desc Creating custom modes:
## @desc Define a handler function named mb_exec_with_mode_<yourmode> that accepts:
## @desc @arg 1: command - The command to execute
## @desc @arg 2: prefix - Variable prefix for config lookup
## @desc The handler can look up any <prefix>_* variables it needs.
## @desc Example: mb_exec_with_mode_ssh could look up <prefix>_ssh_host, <prefix>_ssh_user, etc.
## @desc
## @desc Environment variables:
## @desc Handlers should check for <prefix>_env and prepend it to commands.
## @desc Example: pg_env := PGPASSWORD=$(pg_pass)
## @desc This allows passing credentials or other env vars to the execution context.
## @desc
## @arg 1: command (required) - Command to execute
## @arg 2: prefix (required) - Variable prefix for config lookup (e.g., "php", "localstack", "pg")
## @example $(call mb_exec_with_mode,bash,localstack)
## @example $(call mb_exec_with_mode,php --version,php)
## @returns Command output via mode-specific handler function
## @group exec_mode
## @see mb_exec_with_mode_local, mb_invoke
## @note Mode handlers are defined by modules: docker adds mb_exec_with_mode_docker,
## docker_compose adds mb_exec_with_mode_docker-compose, etc.
## @see mb_exec_with_mode_local, mb_exec_with_mode_docker, mb_exec_with_mode_docker-compose
define mb_exec_with_mode
$(strip
$(eval $0_arg1_cmd := $(if $(value 1),$(strip $1),$(call mb_printf_error,$0: command argument required)))
Expand All @@ -597,6 +611,7 @@ endef
## @arg 1: command (required) - Command to execute
## @arg 2: prefix (required) - Variable prefix for config lookup
## @requires <prefix>_bin - Path to the binary
## @optional <prefix>_env - Environment variables to prepend (e.g., "PGPASSWORD=xxx")
## @group exec_mode
## @see mb_exec_with_mode
define mb_exec_with_mode_local
Expand All @@ -605,7 +620,8 @@ $(strip
$(eval $0_arg2_prefix := $(if $(value 2),$(strip $2),$(call mb_printf_error,$0: prefix argument required)))

$(eval $0_bin := $(call mb_require_var,$($0_arg2_prefix)_bin,$0: $($0_arg2_prefix)_bin not defined for local mode))
$(call mb_invoke,$($0_bin) $($0_arg1_cmd))
$(eval $0_env := $(if $(value $($0_arg2_prefix)_env),$($($0_arg2_prefix)_env)))
$(call mb_invoke,$($0_env) $($0_bin) $($0_arg1_cmd))
)
endef

Expand Down
4 changes: 3 additions & 1 deletion modules/containers/docker/functions.mk
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ endef
## @requires <prefix>_dk_container - Container name/id
## @optional <prefix>_dk_shell - Shell to use (default: dk_shell_default)
## @optional <prefix>_dk_tty - TTY flags (default: dk_exec_default_tty)
## @optional <prefix>_env - Environment variables to prepend (e.g., "PGPASSWORD=xxx")
## @group exec_mode
## @see mb_exec_with_mode, dk_shellc
define mb_exec_with_mode_docker
Expand All @@ -300,6 +301,7 @@ $(strip
$(eval $0_container := $(call mb_require_var,$($0_arg2_prefix)_dk_container,$0: $($0_arg2_prefix)_dk_container not defined for docker mode))
$(eval $0_shell := $(if $(value $($0_arg2_prefix)_dk_shell),$($($0_arg2_prefix)_dk_shell),$(dk_shell_default)))
$(eval $0_tty := $(if $(value $($0_arg2_prefix)_dk_tty),$($($0_arg2_prefix)_dk_tty),$(dk_exec_default_tty)))
$(call dk_shellc,$($0_container),$($0_arg1_cmd),$($0_shell),$($0_tty))
$(eval $0_env := $(if $(value $($0_arg2_prefix)_env),$($($0_arg2_prefix)_env)))
$(call dk_shellc,$($0_container),$($0_env) $($0_arg1_cmd),$($0_shell),$($0_tty))
)
endef
8 changes: 5 additions & 3 deletions modules/containers/docker_compose/docker_compose.mk
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ endef
# $1 = service
# $2 = commands to run inside the container (quotes will be added automatically)
# $3 = shell to load /bin/sh or /bin/bash (optional, default: dc_default_shell_bin)
# $4 = docker compose command to be used exec or run (optional, default: dc_shellc_default_cmd)
# $4 = docker compose command to be used, exec or run (optional, default: dc_shellc_default_cmd)
# $5 = extra options to pass to docker compose (optional, default: none)
define dc_shellc
$(strip
Expand Down Expand Up @@ -169,6 +169,7 @@ endef
## @optional <prefix>_dc_shell - Shell to use (default: dc_shellc_default_shell_bin)
## @optional <prefix>_dc_cmd - Docker compose command: exec or run (default: dc_shellc_default_cmd)
## @optional <prefix>_dc_options - Extra options (default: dc_shellc_default_extra_options)
## @optional <prefix>_env - Environment variables to prepend (e.g., "PGPASSWORD=xxx")
## @group exec_mode
## @see mb_exec_with_mode, dc_shellc
define mb_exec_with_mode_docker-compose
Expand All @@ -178,9 +179,10 @@ $(strip

$(eval $0_service := $(call mb_require_var,$($0_arg2_prefix)_dc_service,$0: $($0_arg2_prefix)_dc_service not defined for docker-compose mode))
$(eval $0_shell := $(if $(value $($0_arg2_prefix)_dc_shell),$($($0_arg2_prefix)_dc_shell),$(dc_shellc_default_shell_bin)))
$(eval $0_cmd := $(if $(value $($0_arg2_prefix)_dc_cmd),$($($0_arg2_prefix)_dc_cmd),$(dc_shellc_default_cmd)))
$(eval $0_dc_cmd := $(if $(value $($0_arg2_prefix)_dc_cmd),$($($0_arg2_prefix)_dc_cmd),$(dc_shellc_default_cmd)))
$(eval $0_options := $(if $(value $($0_arg2_prefix)_dc_options),$($($0_arg2_prefix)_dc_options),$(dc_shellc_default_extra_options)))
$(call dc_shellc,$($0_service),$($0_arg1_cmd),$($0_shell),$($0_cmd),$($0_options))
$(eval $0_env := $(if $(value $($0_arg2_prefix)_env),$($($0_arg2_prefix)_env)))
$(call dc_shellc,$($0_service),$($0_env) $($0_arg1_cmd),$($0_shell),$($0_dc_cmd),$($0_options))
)
endef

Expand Down
30 changes: 30 additions & 0 deletions modules/databases/postgresql/features/conn.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## PostgreSQL Connection Monitoring Feature
## Targets: pg/conn/list, pg/conn/count, pg/conn/long-running
ifndef __MB_PG_FEATURE_CONN__
__MB_PG_FEATURE_CONN__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/conn/list: ## List all active connections with details
$(call mb_printf_info,Listing active connections)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT pid$(mb_comma) usename$(mb_comma) datname$(mb_comma) client_addr$(mb_comma) state$(mb_comma) query_start$(mb_comma) left(query$(mb_comma) 50) as query \
FROM pg_stat_activity WHERE pid <> pg_backend_pid() ORDER BY query_start;")

pg/conn/count: ## Count connections by state
$(call mb_printf_info,Connection count by state)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT state$(mb_comma) count(*) FROM pg_stat_activity GROUP BY state ORDER BY count DESC;")

pg/conn/long-running: ## Find queries running longer than pg_long_query_threshold seconds (default: 60)
$(call mb_printf_info,Queries running longer than $(pg_long_query_threshold) seconds)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT pid$(mb_comma) usename$(mb_comma) datname$(mb_comma) now() - query_start AS duration$(mb_comma) state$(mb_comma) left(query$(mb_comma) 80) as query \
FROM pg_stat_activity \
WHERE state = 'active' AND query_start < now() - interval '$(pg_long_query_threshold) seconds' \
AND pid <> pg_backend_pid() \
ORDER BY query_start;")

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_CONN__
31 changes: 31 additions & 0 deletions modules/databases/postgresql/features/core.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## PostgreSQL Core Feature
## Targets: pg/ping, pg/whoami, pg/info, pg/version, pg/psql
ifndef __MB_PG_FEATURE_CORE__
__MB_PG_FEATURE_CORE__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/ping: ## Quick SELECT 1 to verify connectivity
$(call mb_printf_info,Checking PostgreSQL connectivity to '$(pg_db)' as '$(pg_user)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "SELECT 1;")

pg/whoami: ## Show current_user and database
$(call mb_printf_info,Current database and user)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "SELECT current_database()$(mb_comma) current_user;")

pg/info: ## Server version + list databases
$(call mb_printf_info,Server version and databases)
$(call pg_invoke,$(pg_psql) -Atc "SHOW server_version;")
$(call pg_invoke,$(pg_psql) -l)

pg/version: ## Show PostgreSQL server version
$(call mb_printf_info,PostgreSQL server version)
$(call pg_invoke,$(pg_psql) -Atc "SELECT version();")

pg/psql: ## Interactive psql session
$(call mb_printf_info,Opening interactive psql to '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db))

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_CORE__
40 changes: 40 additions & 0 deletions modules/databases/postgresql/features/db.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## PostgreSQL Database Management Feature
## Targets: pg/db/create/%, pg/db/drop/%, pg/db/reset/%, pg/db/exists/%, pg/db/list, pg/size
ifndef __MB_PG_FEATURE_DB__
__MB_PG_FEATURE_DB__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/db/create/%: ## Create database <name> (idempotent)
$(call mb_printf_info,Creating database: $*)
$(call pg_invoke,$(pg_psql) -d postgres -Atc \
"DO \$$\$$ BEGIN IF NOT EXISTS (SELECT FROM pg_database WHERE datname='$*') THEN CREATE DATABASE \"$*\"; END IF; END \$$\$$;")

pg/db/drop/%: ## Drop database <name> (terminates sessions first)
$(call mb_printf_info,Dropping database: $*)
$(call pg_invoke,$(pg_psql) -d postgres -Atc \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$*' AND pid <> pg_backend_pid();")
$(call pg_invoke,$(pg_psql) -d postgres -Atc "DROP DATABASE IF EXISTS \"$*\";")

pg/db/reset/%: pg/db/drop/% ## Drop then create database <name>
$(call pg_invoke,$(pg_psql) -d postgres -Atc "CREATE DATABASE \"$*\";")

pg/db/exists/%: ## Check if database <name> exists
$(call mb_printf_info,Checking if database exists: $*)
@$(pg_env_pass) $(pg_psql) -d postgres -Atc "SELECT 1 FROM pg_database WHERE datname='$*';" | grep -q 1 && \
echo "Database '$*' exists" || \
echo "Database '$*' does NOT exist"

pg/db/list: ## List all databases with size
$(call mb_printf_info,Listing all databases)
$(call pg_invoke,$(pg_psql) -d postgres -c \
"SELECT datname$(mb_comma) pg_size_pretty(pg_database_size(datname)) as size FROM pg_database ORDER BY pg_database_size(datname) DESC;")

pg/size: ## Show size of current database
$(call mb_printf_info,Database size for '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc \
"SELECT pg_size_pretty(pg_database_size(current_database()));")

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_DB__
35 changes: 35 additions & 0 deletions modules/databases/postgresql/features/dump.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## PostgreSQL Dump & Restore Feature
## Targets: pg/dump, pg/restore, pg/dump/list, pg/dump/info
ifndef __MB_PG_FEATURE_DUMP__
__MB_PG_FEATURE_DUMP__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/dump: ## Dump database to file (pg_dump_file=... to override path)
$(call mb_printf_info,Dumping '$(pg_db)' with format=$(pg_dump_format))
$(eval $@_file := $(if $(value pg_dump_file),$(pg_dump_file),$(pg_dump_dir)/$(pg_db)_$(pg_now).dump))
$(call mb_invoke,mkdir -p $(pg_dump_dir))
$(call pg_invoke,$(pg_dump) -F $(pg_dump_format) $(pg_dump_flags) -f "$($@_file)" "$(pg_db)")
$(call mb_printf_info,Created $($@_file))

pg/restore: ## Restore from dump file (pg_dump_file=<path> required)
$(call mb_printf_info,Restoring into '$(pg_db)' from $(pg_dump_file))
$(if $(value pg_dump_file),,$(error Please provide pg_dump_file=<path to dump>))
$(call pg_invoke,$(pg_restore) $(pg_restore_flags) -d "$(pg_db)" "$(pg_dump_file)")

pg/dump/list: ## List available dump files
$(call mb_printf_info,Listing dumps in $(pg_dump_dir))
$(if $(wildcard $(pg_dump_dir)),\
$(if $(wildcard $(pg_dump_dir)/*.dump),\
$(call mb_invoke,ls -lh $(pg_dump_dir)/*.dump),\
$(info No dumps found in $(pg_dump_dir))),\
$(info Dump directory $(pg_dump_dir) does not exist))

pg/dump/info: ## Show contents of a dump file (pg_dump_file=<path> required)
$(call mb_printf_info,Listing contents of $(pg_dump_file))
$(if $(value pg_dump_file),,$(error Please provide pg_dump_file=<path to dump>))
$(call mb_invoke,pg_restore -l "$(pg_dump_file)")

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_DUMP__
28 changes: 28 additions & 0 deletions modules/databases/postgresql/features/extension.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## PostgreSQL Extension Management Feature
## Targets: pg/extension/list, pg/extension/available, pg/extension/create/%, pg/extension/drop/%
ifndef __MB_PG_FEATURE_EXTENSION__
__MB_PG_FEATURE_EXTENSION__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/extension/list: ## List installed extensions
$(call mb_printf_info,Listing installed extensions in '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT extname$(mb_comma) extversion$(mb_comma) extnamespace::regnamespace as schema FROM pg_extension ORDER BY extname;")

pg/extension/available: ## List available (installable) extensions
$(call mb_printf_info,Listing available extensions)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT name$(mb_comma) default_version$(mb_comma) comment FROM pg_available_extensions ORDER BY name;")

pg/extension/create/%: ## Create/install extension (make pg/extension/create/uuid-ossp)
$(call mb_printf_info,Creating extension: $*)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "CREATE EXTENSION IF NOT EXISTS \"$*\";")

pg/extension/drop/%: ## Drop extension
$(call mb_printf_info,Dropping extension: $*)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "DROP EXTENSION IF EXISTS \"$*\";")

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_EXTENSION__
36 changes: 36 additions & 0 deletions modules/databases/postgresql/features/index.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## PostgreSQL Index Management Feature
## Targets: pg/index/list, pg/index/unused, pg/index/reindex, pg/index/reindex/%
ifndef __MB_PG_FEATURE_INDEX__
__MB_PG_FEATURE_INDEX__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/index/list: ## List indexes with size and usage stats
$(call mb_printf_info,Listing indexes in '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT schemaname$(mb_comma) tablename$(mb_comma) indexname$(mb_comma) \
pg_size_pretty(pg_relation_size(schemaname||'.'||indexname)) as size$(mb_comma) \
idx_scan as scans \
FROM pg_stat_user_indexes \
ORDER BY pg_relation_size(schemaname||'.'||indexname) DESC;")

pg/index/unused: ## List potentially unused indexes (0 scans)
$(call mb_printf_info,Listing unused indexes in '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT schemaname$(mb_comma) tablename$(mb_comma) indexname$(mb_comma) \
pg_size_pretty(pg_relation_size(schemaname||'.'||indexname)) as size \
FROM pg_stat_user_indexes \
WHERE idx_scan = 0 \
ORDER BY pg_relation_size(schemaname||'.'||indexname) DESC;")

pg/index/reindex: ## Reindex the database (rebuilds all indexes)
$(call mb_printf_info,Reindexing database '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "REINDEX DATABASE \"$(pg_db)\";")

pg/index/reindex/%: ## Reindex specific table (make pg/index/reindex/users)
$(call mb_printf_info,Reindexing table: $*)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "REINDEX TABLE $*;")

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_INDEX__
37 changes: 37 additions & 0 deletions modules/databases/postgresql/features/maintenance.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## PostgreSQL Maintenance Feature
## Targets: pg/vacuum/analyze, pg/vacuum/full, pg/vacuum/%, pg/analyze, pg/bloat
ifndef __MB_PG_FEATURE_MAINTENANCE__
__MB_PG_FEATURE_MAINTENANCE__ := 1

ifndef __MB_TEST_DISCOVERY__

pg/vacuum/analyze: ## VACUUM ANALYZE current database
$(call mb_printf_info,Running VACUUM ANALYZE on '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "VACUUM (ANALYZE);")

pg/vacuum/full: ## VACUUM FULL ANALYZE (requires exclusive lock, reclaims space)
$(call mb_printf_info,Running VACUUM FULL ANALYZE on '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "VACUUM (FULL$(mb_comma) ANALYZE);")

pg/vacuum/%: ## VACUUM ANALYZE specific table
$(call mb_printf_info,Running VACUUM ANALYZE on table: $*)
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "VACUUM (ANALYZE) $*;")

pg/analyze: ## ANALYZE only (update statistics, no vacuum)
$(call mb_printf_info,Running ANALYZE on '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "ANALYZE;")

pg/bloat: ## Show table and index bloat estimates
$(call mb_printf_info,Checking bloat in '$(pg_db)')
$(call pg_invoke,$(pg_psql) -d $(pg_db) -c \
"SELECT schemaname$(mb_comma) tablename$(mb_comma) \
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size$(mb_comma) \
n_dead_tup as dead_tuples$(mb_comma) \
CASE WHEN n_live_tup > 0 THEN round(100.0 * n_dead_tup / n_live_tup$(mb_comma) 2) ELSE 0 END as dead_pct \
FROM pg_stat_user_tables \
WHERE n_dead_tup > 1000 \
ORDER BY n_dead_tup DESC LIMIT 20;")

endif # __MB_TEST_DISCOVERY__

endif # __MB_PG_FEATURE_MAINTENANCE__
Loading