diff --git a/CHANGELOG.md b/CHANGELOG.md index b57d50b..93b25c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `_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 `_env` to commands + ## [2.2.7] - 2026-01-04 ### Added diff --git a/core/functions.mk b/core/functions.mk index b246f76..86bd97d 100644 --- a/core/functions.mk +++ b/core/functions.mk @@ -563,19 +563,33 @@ $(strip endef ## @function mb_exec_with_mode -## @desc Execute command with mode selection (local/docker/docker-compose) -## @desc Reads _exec_mode to determine execution mode and delegates to appropriate handler. -## @desc Supports three modes: local (uses _bin), docker (uses dk_shellc with _dk_container), -## @desc and docker-compose (uses dc_shellc with _dc_service). +## @desc Execute command with mode selection via extensible handler system. +## @desc Reads _exec_mode to determine which handler to call: mb_exec_with_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_ 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 _* variables it needs. +## @desc Example: mb_exec_with_mode_ssh could look up _ssh_host, _ssh_user, etc. +## @desc +## @desc Environment variables: +## @desc Handlers should check for _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))) @@ -597,6 +611,7 @@ endef ## @arg 1: command (required) - Command to execute ## @arg 2: prefix (required) - Variable prefix for config lookup ## @requires _bin - Path to the binary +## @optional _env - Environment variables to prepend (e.g., "PGPASSWORD=xxx") ## @group exec_mode ## @see mb_exec_with_mode define mb_exec_with_mode_local @@ -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 diff --git a/modules/containers/docker/functions.mk b/modules/containers/docker/functions.mk index 9d1090b..866221b 100644 --- a/modules/containers/docker/functions.mk +++ b/modules/containers/docker/functions.mk @@ -290,6 +290,7 @@ endef ## @requires _dk_container - Container name/id ## @optional _dk_shell - Shell to use (default: dk_shell_default) ## @optional _dk_tty - TTY flags (default: dk_exec_default_tty) +## @optional _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 @@ -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 diff --git a/modules/containers/docker_compose/docker_compose.mk b/modules/containers/docker_compose/docker_compose.mk index c480716..4b3ec2c 100644 --- a/modules/containers/docker_compose/docker_compose.mk +++ b/modules/containers/docker_compose/docker_compose.mk @@ -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 @@ -169,6 +169,7 @@ endef ## @optional _dc_shell - Shell to use (default: dc_shellc_default_shell_bin) ## @optional _dc_cmd - Docker compose command: exec or run (default: dc_shellc_default_cmd) ## @optional _dc_options - Extra options (default: dc_shellc_default_extra_options) +## @optional _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 @@ -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 diff --git a/modules/databases/postgresql/features/conn.mk b/modules/databases/postgresql/features/conn.mk new file mode 100644 index 0000000..1e042e0 --- /dev/null +++ b/modules/databases/postgresql/features/conn.mk @@ -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__ diff --git a/modules/databases/postgresql/features/core.mk b/modules/databases/postgresql/features/core.mk new file mode 100644 index 0000000..f11b8c4 --- /dev/null +++ b/modules/databases/postgresql/features/core.mk @@ -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__ diff --git a/modules/databases/postgresql/features/db.mk b/modules/databases/postgresql/features/db.mk new file mode 100644 index 0000000..da671cc --- /dev/null +++ b/modules/databases/postgresql/features/db.mk @@ -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 (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 (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 + $(call pg_invoke,$(pg_psql) -d postgres -Atc "CREATE DATABASE \"$*\";") + +pg/db/exists/%: ## Check if database 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__ diff --git a/modules/databases/postgresql/features/dump.mk b/modules/databases/postgresql/features/dump.mk new file mode 100644 index 0000000..c759274 --- /dev/null +++ b/modules/databases/postgresql/features/dump.mk @@ -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= required) + $(call mb_printf_info,Restoring into '$(pg_db)' from $(pg_dump_file)) + $(if $(value pg_dump_file),,$(error Please provide pg_dump_file=)) + $(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= required) + $(call mb_printf_info,Listing contents of $(pg_dump_file)) + $(if $(value pg_dump_file),,$(error Please provide pg_dump_file=)) + $(call mb_invoke,pg_restore -l "$(pg_dump_file)") + +endif # __MB_TEST_DISCOVERY__ + +endif # __MB_PG_FEATURE_DUMP__ diff --git a/modules/databases/postgresql/features/extension.mk b/modules/databases/postgresql/features/extension.mk new file mode 100644 index 0000000..33a39bf --- /dev/null +++ b/modules/databases/postgresql/features/extension.mk @@ -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__ diff --git a/modules/databases/postgresql/features/index.mk b/modules/databases/postgresql/features/index.mk new file mode 100644 index 0000000..11d29de --- /dev/null +++ b/modules/databases/postgresql/features/index.mk @@ -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__ diff --git a/modules/databases/postgresql/features/maintenance.mk b/modules/databases/postgresql/features/maintenance.mk new file mode 100644 index 0000000..1d6c7b9 --- /dev/null +++ b/modules/databases/postgresql/features/maintenance.mk @@ -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__ diff --git a/modules/databases/postgresql/features/role.mk b/modules/databases/postgresql/features/role.mk new file mode 100644 index 0000000..05f8cb6 --- /dev/null +++ b/modules/databases/postgresql/features/role.mk @@ -0,0 +1,33 @@ +## PostgreSQL Role Management Feature +## Targets: pg/role/create/%, pg/role/drop/%, pg/role/exists/%, pg/role/list +ifndef __MB_PG_FEATURE_ROLE__ +__MB_PG_FEATURE_ROLE__ := 1 + +ifndef __MB_TEST_DISCOVERY__ + +pg/role/create/%: ## Create role with LOGIN (pg_role_pass=... for password) + $(call mb_printf_info,Creating role: $*) + $(if $(value pg_role_pass),\ + $(call pg_invoke,$(pg_psql) -d postgres -Atc \ + "DO \$$\$$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='$*') THEN CREATE ROLE \"$*\" LOGIN PASSWORD '$(pg_role_pass)'; END IF; END \$$\$$;"),\ + $(call pg_invoke,$(pg_psql) -d postgres -Atc \ + "DO \$$\$$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='$*') THEN CREATE ROLE \"$*\" LOGIN; END IF; END \$$\$$;")\ + ) + +pg/role/drop/%: ## Drop role if exists + $(call mb_printf_info,Dropping role: $*) + $(call pg_invoke,$(pg_psql) -d postgres -Atc "DROP ROLE IF EXISTS \"$*\";") + +pg/role/exists/%: ## Check if role exists + $(call mb_printf_info,Checking if role exists: $*) + @$(pg_env_pass) $(pg_psql) -d postgres -Atc "SELECT 1 FROM pg_roles WHERE rolname='$*';" | grep -q 1 && \ + echo "Role '$*' exists" || \ + echo "Role '$*' does NOT exist" + +pg/role/list: ## List all roles with attributes + $(call mb_printf_info,Listing all roles) + $(call pg_invoke,$(pg_psql) -d postgres -c "\du") + +endif # __MB_TEST_DISCOVERY__ + +endif # __MB_PG_FEATURE_ROLE__ diff --git a/modules/databases/postgresql/features/session.mk b/modules/databases/postgresql/features/session.mk new file mode 100644 index 0000000..8f47155 --- /dev/null +++ b/modules/databases/postgresql/features/session.mk @@ -0,0 +1,27 @@ +## PostgreSQL Session Management Feature +## Targets: pg/session/kill/%, pg/session/kill-idle, pg/session/cancel/% +ifndef __MB_PG_FEATURE_SESSION__ +__MB_PG_FEATURE_SESSION__ := 1 + +ifndef __MB_TEST_DISCOVERY__ + +pg/session/kill/%: ## Kill session by PID (make pg/session/kill/12345) + $(call mb_printf_info,Terminating session with PID: $*) + $(call pg_invoke,$(pg_psql) -d postgres -Atc "SELECT pg_terminate_backend($*);") + +pg/session/kill-idle: ## Kill idle sessions older than pg_idle_timeout minutes (default: 30) + $(call mb_printf_info,Killing idle sessions older than $(pg_idle_timeout) minutes) + $(call pg_invoke,$(pg_psql) -d postgres -c \ + "SELECT pg_terminate_backend(pid)$(mb_comma) usename$(mb_comma) datname$(mb_comma) state$(mb_comma) state_change \ + FROM pg_stat_activity \ + WHERE pid <> pg_backend_pid() \ + AND state IN ('idle'$(mb_comma) 'idle in transaction'$(mb_comma) 'idle in transaction (aborted)') \ + AND state_change < now() - interval '$(pg_idle_timeout) minutes';") + +pg/session/cancel/%: ## Cancel query (not session) by PID - connection stays open + $(call mb_printf_info,Cancelling query for PID: $*) + $(call pg_invoke,$(pg_psql) -d postgres -Atc "SELECT pg_cancel_backend($*);") + +endif # __MB_TEST_DISCOVERY__ + +endif # __MB_PG_FEATURE_SESSION__ diff --git a/modules/databases/postgresql/features/sql.mk b/modules/databases/postgresql/features/sql.mk new file mode 100644 index 0000000..8c40979 --- /dev/null +++ b/modules/databases/postgresql/features/sql.mk @@ -0,0 +1,22 @@ +## PostgreSQL SQL Execution Feature +## Targets: pg/sql/file/%, pg/sql/exec, pg/query +ifndef __MB_PG_FEATURE_SQL__ +__MB_PG_FEATURE_SQL__ := 1 + +ifndef __MB_TEST_DISCOVERY__ + +pg/sql/file/%: ## Execute a .sql file (make pg/sql/file/path/to/script.sql) + $(call mb_printf_info,Executing SQL file: $*) + $(if $(wildcard $*),,$(call mb_printf_error,SQL file not found: $*); exit 2) + $(call pg_invoke,$(pg_psql) -d $(pg_db) -f "$*") + +pg/sql/exec: ## Execute SQL query (pg_query="SELECT ...") + $(call mb_printf_info,Executing SQL query) + $(if $(value pg_query),,$(error Please provide pg_query="")) + $(call pg_invoke,$(pg_psql) -d $(pg_db) -c "$(pg_query)") + +pg/query: pg/sql/exec ## Alias for pg/sql/exec + +endif # __MB_TEST_DISCOVERY__ + +endif # __MB_PG_FEATURE_SQL__ diff --git a/modules/databases/postgresql/features/table.mk b/modules/databases/postgresql/features/table.mk new file mode 100644 index 0000000..eb1174a --- /dev/null +++ b/modules/databases/postgresql/features/table.mk @@ -0,0 +1,37 @@ +## PostgreSQL Table & Schema Introspection Feature +## Targets: pg/schema/list, pg/table/list, pg/table/count/%, pg/table/describe/%, pg/table/stats +ifndef __MB_PG_FEATURE_TABLE__ +__MB_PG_FEATURE_TABLE__ := 1 + +ifndef __MB_TEST_DISCOVERY__ + +pg/schema/list: ## List schemas and owners + $(call mb_printf_info,Listing schemas and owners) + $(call pg_invoke,$(pg_psql) -d $(pg_db) -c \ + "SELECT nspname AS schema$(mb_comma) pg_catalog.pg_get_userbyid(nspowner) AS owner \ + FROM pg_namespace WHERE nspname NOT LIKE 'pg_%' ORDER BY 1;") + +pg/table/list: ## List all tables with size + $(call mb_printf_info,Listing all tables 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 size \ + FROM pg_tables WHERE schemaname NOT IN ('pg_catalog'$(mb_comma) 'information_schema') \ + ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;") + +pg/table/count/%: ## Count rows in table (make pg/table/count/users or pg/table/count/public.users) + $(call mb_printf_info,Counting rows in: $*) + $(call pg_invoke,$(pg_psql) -d $(pg_db) -Atc "SELECT COUNT(*) FROM $*;") + +pg/table/describe/%: ## Describe table structure + $(call mb_printf_info,Describing table: $*) + $(call pg_invoke,$(pg_psql) -d $(pg_db) -c "\d+ $*") + +pg/table/stats: ## Show table statistics (row counts, dead tuples) + $(call mb_printf_info,Table statistics for '$(pg_db)') + $(call pg_invoke,$(pg_psql) -d $(pg_db) -c \ + "SELECT schemaname$(mb_comma) relname$(mb_comma) n_live_tup$(mb_comma) n_dead_tup$(mb_comma) last_vacuum$(mb_comma) last_autovacuum \ + FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20;") + +endif # __MB_TEST_DISCOVERY__ + +endif # __MB_PG_FEATURE_TABLE__ diff --git a/modules/databases/postgresql/mod_config.mk b/modules/databases/postgresql/mod_config.mk new file mode 100644 index 0000000..89909a5 --- /dev/null +++ b/modules/databases/postgresql/mod_config.mk @@ -0,0 +1,49 @@ +## PostgreSQL Module Configuration +## ================================ + +## Enabled features (space-separated list) +## Available: core sql db role conn session dump extension table index maintenance +pg_features ?= core sql db dump maintenance# + +##################################################################################### +## Execution Mode Configuration (mb_exec_with_mode compatible) +##################################################################################### + +## Execution mode: local, docker, docker-compose +pg_exec_mode ?= local# + +## Docker mode settings +pg_dk_container ?= postgres# +pg_dk_shell ?= /bin/bash# + +## Docker Compose mode settings +pg_dc_service ?= postgres# +pg_dc_shell ?= /bin/bash# + +##################################################################################### +## Connection Settings +##################################################################################### + +pg_user ?= postgres# +pg_pass ?=# +pg_db ?= app# + +## Host/port for local mode connections +pg_host ?= 127.0.0.1# +pg_port ?= 5432# + +##################################################################################### +## Dump & Restore Settings +##################################################################################### + +pg_dump_dir ?= backups# +pg_dump_format ?= c# +pg_dump_flags ?= --no-owner --no-privileges# +pg_restore_flags ?= --clean --if-exists --no-owner --no-privileges# + +##################################################################################### +## Session Management Settings +##################################################################################### + +pg_idle_timeout ?= 30# +pg_long_query_threshold ?= 60# diff --git a/modules/databases/postgresql/mod_info.mk b/modules/databases/postgresql/mod_info.mk new file mode 100644 index 0000000..a707d88 --- /dev/null +++ b/modules/databases/postgresql/mod_info.mk @@ -0,0 +1,6 @@ +mb_module_name := postgresql +mb_module_version := 1.1.0 +mb_module_description := PostgreSQL database management (psql, dumps, migrations, roles) +mb_module_author := +mb_module_license := MIT +mb_module_depends := diff --git a/modules/databases/postgresql/postgresql.mk b/modules/databases/postgresql/postgresql.mk new file mode 100644 index 0000000..a0b15af --- /dev/null +++ b/modules/databases/postgresql/postgresql.mk @@ -0,0 +1,67 @@ +##################################################################################### +# Project: MakeBind +# File: modules/databases/postgresql/postgresql.mk +# Description: PostgreSQL database module with feature-based target loading +# Author: AntonioCS +# License: MIT License +##################################################################################### +ifndef __MB_MODULES_POSTGRESQL__ +__MB_MODULES_POSTGRESQL__ := 1 + +## Available features: core sql db role conn session dump extension table index maintenance +pg_available_features := core sql db role conn session dump extension table index maintenance + +## Validate pg_features - warn if unknown feature requested +$(foreach f,$(pg_features),\ + $(if $(filter $(f),$(pg_available_features)),,\ + $(warning PostgreSQL: Unknown feature '$(f)'. Available: $(pg_available_features)))) + +## ============================================================================= +## Shared variables (used by all features) +## ============================================================================= + +pg_env := $(if $(pg_pass),PGPASSWORD=$(pg_pass)) +pg_user_flags := -U $(pg_user) +pg_host_flags := -h $(pg_host) -p $(pg_port) +pg_psql_common := -v ON_ERROR_STOP=1 +pg_now := $(shell date +%Y%m%d_%H%M%S) + +## ============================================================================= +## pg_invoke - Execute PostgreSQL commands with mode support +## ============================================================================= +## Supports 3 execution modes: +## - local: Run command directly on host +## - docker: Run via docker exec in pg_dk_container +## - docker-compose: Run via docker compose exec in pg_dc_service +## +## Auto-extracts the binary (first word) from the command and sets pg_bin, +## then passes the remaining args to mb_exec_with_mode. +## +## @arg 1: command - Full command to execute (e.g., "psql -d mydb -c 'SELECT 1'") +## @example $(call pg_invoke,psql -d mydb -c "SELECT 1") +## @example $(call pg_invoke,pg_dump -F c mydb > backup.dump) + +define pg_invoke +$(strip + $(eval pg_bin := $(firstword $1)) + $(call mb_exec_with_mode,$(wordlist 2,999,$1),pg) +) +endef + +## Pre-built command fragments for convenience +pg_psql = psql $(pg_host_flags) $(pg_user_flags) $(pg_psql_common) +pg_dump = pg_dump $(pg_host_flags) $(pg_user_flags) +pg_restore = pg_restore $(pg_host_flags) $(pg_user_flags) + +## ============================================================================= +## Feature loader +## ============================================================================= + +pg_features_path := $(dir $(lastword $(MAKEFILE_LIST)))features + +$(foreach feature,$(pg_features),\ + $(if $(wildcard $(pg_features_path)/$(feature).mk),\ + $(eval include $(pg_features_path)/$(feature).mk),\ + $(warning PostgreSQL: Feature file not found: $(pg_features_path)/$(feature).mk))) + +endif # __MB_MODULES_POSTGRESQL__ diff --git a/modules/databases/postgresql/postgresql_test.mk b/modules/databases/postgresql/postgresql_test.mk new file mode 100644 index 0000000..c2e8dce --- /dev/null +++ b/modules/databases/postgresql/postgresql_test.mk @@ -0,0 +1,102 @@ +##################################################################################### +# Project: MakeBind +# File: modules/databases/postgresql/postgresql_test.mk +# Description: Tests for the PostgreSQL module +# Author: AntonioCS +# License: MIT License +##################################################################################### + +include $(mb_core_path)/util.mk +include $(mb_core_path)/functions.mk + +## Load config to define variables +include $(mb_modules_path)/databases/postgresql/mod_config.mk + +###################################################################################### +# Configuration tests +###################################################################################### + +define test_modules_postgresql_config_defaults + $(call mb_assert_eq,core sql db dump maintenance,$(pg_features),pg_features should have default features) + $(call mb_assert_eq,local,$(pg_exec_mode),pg_exec_mode should default to local) + $(call mb_assert_eq,postgres,$(pg_user),pg_user should default to postgres) + $(call mb_assert_eq,app,$(pg_db),pg_db should default to app) + $(call mb_assert_eq,127.0.0.1,$(pg_host),pg_host should default to 127.0.0.1) + $(call mb_assert_eq,5432,$(pg_port),pg_port should default to 5432) + $(call mb_assert_eq,postgres,$(pg_dc_service),pg_dc_service should default to postgres) + $(call mb_assert_eq,backups,$(pg_dump_dir),pg_dump_dir should default to backups) + $(call mb_assert_eq,c,$(pg_dump_format),pg_dump_format should default to c) + $(call mb_assert_eq,30,$(pg_idle_timeout),pg_idle_timeout should default to 30) + $(call mb_assert_eq,60,$(pg_long_query_threshold),pg_long_query_threshold should default to 60) +endef + +define test_modules_postgresql_available_features + $(call mb_assert_not_empty,$(pg_available_features),pg_available_features should be defined) + $(call mb_assert_filter,core,$(pg_available_features),core should be an available feature) + $(call mb_assert_filter,sql,$(pg_available_features),sql should be an available feature) + $(call mb_assert_filter,db,$(pg_available_features),db should be an available feature) + $(call mb_assert_filter,role,$(pg_available_features),role should be an available feature) + $(call mb_assert_filter,conn,$(pg_available_features),conn should be an available feature) + $(call mb_assert_filter,session,$(pg_available_features),session should be an available feature) + $(call mb_assert_filter,dump,$(pg_available_features),dump should be an available feature) + $(call mb_assert_filter,extension,$(pg_available_features),extension should be an available feature) + $(call mb_assert_filter,table,$(pg_available_features),table should be an available feature) + $(call mb_assert_filter,index,$(pg_available_features),index should be an available feature) + $(call mb_assert_filter,maintenance,$(pg_available_features),maintenance should be an available feature) +endef + +###################################################################################### +# Command building tests +###################################################################################### + +## Load the main module to test command building +include $(mb_modules_path)/databases/postgresql/postgresql.mk + +define test_modules_postgresql_psql_cmd_fragments + $(call mb_assert_contains,psql,$(pg_psql),pg_psql should contain psql) + $(call mb_assert_contains,-U $(pg_user),$(pg_psql),pg_psql should contain user flag) + $(call mb_assert_contains,-h $(pg_host),$(pg_psql),pg_psql should contain host flag) + $(call mb_assert_contains,-p $(pg_port),$(pg_psql),pg_psql should contain port flag) +endef + +define test_modules_postgresql_dump_cmd_fragments + $(call mb_assert_contains,pg_dump,$(pg_dump),pg_dump should contain pg_dump) + $(call mb_assert_contains,-U $(pg_user),$(pg_dump),pg_dump should contain user flag) + $(call mb_assert_contains,-h $(pg_host),$(pg_dump),pg_dump should contain host flag) +endef + +define test_modules_postgresql_restore_cmd_fragments + $(call mb_assert_contains,pg_restore,$(pg_restore),pg_restore should contain pg_restore) + $(call mb_assert_contains,-U $(pg_user),$(pg_restore),pg_restore should contain user flag) + $(call mb_assert_contains,-h $(pg_host),$(pg_restore),pg_restore should contain host flag) +endef + +define test_modules_postgresql_pg_invoke_defined + $(call mb_assert,$(value pg_invoke),pg_invoke function should be defined) +endef + +define test_modules_postgresql_exec_mode_config + $(call mb_assert_eq,postgres,$(pg_dk_container),pg_dk_container should default to postgres) + $(call mb_assert_eq,postgres,$(pg_dc_service),pg_dc_service should default to postgres) + $(call mb_assert_eq,/bin/bash,$(pg_dk_shell),pg_dk_shell should default to /bin/bash) + $(call mb_assert_eq,/bin/bash,$(pg_dc_shell),pg_dc_shell should default to /bin/bash) +endef + +###################################################################################### +# Feature loading tests +###################################################################################### + +define test_modules_postgresql_feature_core_loaded + $(call mb_assert,$(filter core,$(pg_features)),core feature should be in pg_features) + $(call mb_assert,$(value __MB_PG_FEATURE_CORE__),core feature guard should be defined) +endef + +define test_modules_postgresql_feature_dump_loaded + $(call mb_assert,$(filter dump,$(pg_features)),dump feature should be in pg_features) + $(call mb_assert,$(value __MB_PG_FEATURE_DUMP__),dump feature guard should be defined) +endef + +define test_modules_postgresql_feature_maintenance_loaded + $(call mb_assert,$(filter maintenance,$(pg_features)),maintenance feature should be in pg_features) + $(call mb_assert,$(value __MB_PG_FEATURE_MAINTENANCE__),maintenance feature guard should be defined) +endef