pg-idx-manager 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pg_idx_manager-0.1.0/PKG-INFO +67 -0
- pg_idx_manager-0.1.0/README.md +53 -0
- pg_idx_manager-0.1.0/pg_idx_manager/__init__.py +5 -0
- pg_idx_manager-0.1.0/pg_idx_manager/cli.py +103 -0
- pg_idx_manager-0.1.0/pg_idx_manager/core.py +107 -0
- pg_idx_manager-0.1.0/pg_idx_manager.egg-info/PKG-INFO +67 -0
- pg_idx_manager-0.1.0/pg_idx_manager.egg-info/SOURCES.txt +11 -0
- pg_idx_manager-0.1.0/pg_idx_manager.egg-info/dependency_links.txt +1 -0
- pg_idx_manager-0.1.0/pg_idx_manager.egg-info/requires.txt +1 -0
- pg_idx_manager-0.1.0/pg_idx_manager.egg-info/top_level.txt +1 -0
- pg_idx_manager-0.1.0/pyproject.toml +24 -0
- pg_idx_manager-0.1.0/setup.cfg +4 -0
- pg_idx_manager-0.1.0/tests/test_orm.py +44 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pg_idx_manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, framework-agnostic developer linter and cleanup tool for PostgreSQL indexes.
|
|
5
|
+
Author-email: Pierpaolo <tua_email@example.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
14
|
+
|
|
15
|
+
# PG Index Manager & Janitor ๐
|
|
16
|
+
|
|
17
|
+
A lightweight, framework-agnostic Python library designed for developers to audit database queries, surface runtime performance bottlenecks, and safely maintain PostgreSQL indexes.
|
|
18
|
+
|
|
19
|
+
Unlike heavy Application Performance Monitoring (APM) tools that require invasive database extensions (like HypoPG) or expensive SaaS subscriptions, this library operates entirely inside your application or via a clean CLI using native PostgreSQL capabilities.
|
|
20
|
+
|
|
21
|
+
## The Problem It Solves: "ORM Blindness"
|
|
22
|
+
Modern Object-Relational Mappers (ORMs) like Django or SQLAlchemy maximize developer productivity but hide the underlying SQL execution plan. A seemingly innocent line of Python code can silently trigger a **Sequential Scan (Full Table Scan)** across millions of rows, saturating server CPU and disk I/O.
|
|
23
|
+
|
|
24
|
+
Since Large Language Models (AI) cannot inspect your production database cardinality, table sizes, or live index catalogs, they cannot reliably predict query performance. This library bridges that gap by running live `EXPLAIN ANALYZE` inspections directly on the database engine.
|
|
25
|
+
|
|
26
|
+
## Key Features
|
|
27
|
+
|
|
28
|
+
1. **Agnostic Performance Auditing**: Intercepts raw SQL queries, recursively parses the native PostgreSQL execution tree, and catches performance anomalies (**Sequential Scans**) with exact millisecond runtimes.
|
|
29
|
+
2. **Safe Janitor Mode (Interactive CLI)**: Scans PostgreSQL system catalogs (`pg_stat_user_indexes`) to discover dead, unused indexes that are slowing down your `INSERT`/`UPDATE` mutations. It allows you to drop them interactively using `DROP INDEX CONCURRENTLY` without locking tables or risking production application downtime.
|
|
30
|
+
3. **Zero-Dependency Architecture**: Does not require root or superuser privileges on the database server. If you can connect to the database, you can run this library.
|
|
31
|
+
|
|
32
|
+
## Architectural Architecture: Where does it live?
|
|
33
|
+
Because the core engine requires only a raw query string and a standard database connection, it is completely independent of your web framework. You can integrate it at the lowest layer of your infrastructure:
|
|
34
|
+
|
|
35
|
+
* **At the Driver Level (`psycopg2`)**: By extending the native database cursor, you can automatically audit **100% of your application queries** (Django, FastAPI, Flask, or raw SQL) before they hit the wire.
|
|
36
|
+
* **At the ORM Engine Level**: Easily hooks into global ORM events (e.g., SQLAlchemy's `before_cursor_execute`) to catch hidden query costs implicitly across all Services and Repositories with zero business-code modification.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Getting Started & Testing Locally ๐งช
|
|
41
|
+
|
|
42
|
+
This repository includes a fully containerized testing environment to observe performance optimization in real-time.
|
|
43
|
+
|
|
44
|
+
### 1. Spin up the isolated test database
|
|
45
|
+
```bash
|
|
46
|
+
docker compose up -d
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Install the library locally in editable mode
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Run the Core Agnostic Test
|
|
55
|
+
This script populates the database with **10,000 mock records**, executes an unindexed query, catches the Sequential Scan risk, and launches the persistent interactive CLI:
|
|
56
|
+
```bash
|
|
57
|
+
python tests/run_test.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 4. Run the ORM Integration Test
|
|
61
|
+
Observe how the library seamlessly intercepts real-time query metrics generated implicitly by SQLAlchemy ORM models:
|
|
62
|
+
```bash
|
|
63
|
+
python tests/test_orm.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
MIT License. Free to use and extend.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# PG Index Manager & Janitor ๐
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic Python library designed for developers to audit database queries, surface runtime performance bottlenecks, and safely maintain PostgreSQL indexes.
|
|
4
|
+
|
|
5
|
+
Unlike heavy Application Performance Monitoring (APM) tools that require invasive database extensions (like HypoPG) or expensive SaaS subscriptions, this library operates entirely inside your application or via a clean CLI using native PostgreSQL capabilities.
|
|
6
|
+
|
|
7
|
+
## The Problem It Solves: "ORM Blindness"
|
|
8
|
+
Modern Object-Relational Mappers (ORMs) like Django or SQLAlchemy maximize developer productivity but hide the underlying SQL execution plan. A seemingly innocent line of Python code can silently trigger a **Sequential Scan (Full Table Scan)** across millions of rows, saturating server CPU and disk I/O.
|
|
9
|
+
|
|
10
|
+
Since Large Language Models (AI) cannot inspect your production database cardinality, table sizes, or live index catalogs, they cannot reliably predict query performance. This library bridges that gap by running live `EXPLAIN ANALYZE` inspections directly on the database engine.
|
|
11
|
+
|
|
12
|
+
## Key Features
|
|
13
|
+
|
|
14
|
+
1. **Agnostic Performance Auditing**: Intercepts raw SQL queries, recursively parses the native PostgreSQL execution tree, and catches performance anomalies (**Sequential Scans**) with exact millisecond runtimes.
|
|
15
|
+
2. **Safe Janitor Mode (Interactive CLI)**: Scans PostgreSQL system catalogs (`pg_stat_user_indexes`) to discover dead, unused indexes that are slowing down your `INSERT`/`UPDATE` mutations. It allows you to drop them interactively using `DROP INDEX CONCURRENTLY` without locking tables or risking production application downtime.
|
|
16
|
+
3. **Zero-Dependency Architecture**: Does not require root or superuser privileges on the database server. If you can connect to the database, you can run this library.
|
|
17
|
+
|
|
18
|
+
## Architectural Architecture: Where does it live?
|
|
19
|
+
Because the core engine requires only a raw query string and a standard database connection, it is completely independent of your web framework. You can integrate it at the lowest layer of your infrastructure:
|
|
20
|
+
|
|
21
|
+
* **At the Driver Level (`psycopg2`)**: By extending the native database cursor, you can automatically audit **100% of your application queries** (Django, FastAPI, Flask, or raw SQL) before they hit the wire.
|
|
22
|
+
* **At the ORM Engine Level**: Easily hooks into global ORM events (e.g., SQLAlchemy's `before_cursor_execute`) to catch hidden query costs implicitly across all Services and Repositories with zero business-code modification.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Getting Started & Testing Locally ๐งช
|
|
27
|
+
|
|
28
|
+
This repository includes a fully containerized testing environment to observe performance optimization in real-time.
|
|
29
|
+
|
|
30
|
+
### 1. Spin up the isolated test database
|
|
31
|
+
```bash
|
|
32
|
+
docker compose up -d
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Install the library locally in editable mode
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Run the Core Agnostic Test
|
|
41
|
+
This script populates the database with **10,000 mock records**, executes an unindexed query, catches the Sequential Scan risk, and launches the persistent interactive CLI:
|
|
42
|
+
```bash
|
|
43
|
+
python tests/run_test.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 4. Run the ORM Integration Test
|
|
47
|
+
Observe how the library seamlessly intercepts real-time query metrics generated implicitly by SQLAlchemy ORM models:
|
|
48
|
+
```bash
|
|
49
|
+
python tests/test_orm.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
MIT License. Free to use and extend.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .core import IndexManagerCore
|
|
3
|
+
|
|
4
|
+
def is_safe_query(query: str) -> bool:
|
|
5
|
+
"""Sanity check to ensure the tool only processes read-only SELECT queries."""
|
|
6
|
+
clean_query = query.strip().upper()
|
|
7
|
+
if len(clean_query) < 6:
|
|
8
|
+
return True
|
|
9
|
+
dangerous_keywords = ["DROP TABLE", "DROP DATABASE", "DELETE FROM", "INSERT INTO", "UPDATE ", "TRUNCATE "]
|
|
10
|
+
for keyword in dangerous_keywords:
|
|
11
|
+
if keyword in clean_query:
|
|
12
|
+
return False
|
|
13
|
+
return True
|
|
14
|
+
|
|
15
|
+
def run_cli(connection):
|
|
16
|
+
"""Launches a persistent, continuous loop for interactive DB optimization."""
|
|
17
|
+
manager = IndexManagerCore(connection, min_table_rows=0)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
while True:
|
|
21
|
+
print("\n" + "="*40)
|
|
22
|
+
print("๐ PERSISTENT PG INDEX MANAGER CLI")
|
|
23
|
+
print("="*40)
|
|
24
|
+
print("1. Audit single SQL query efficiency")
|
|
25
|
+
print("2. Scan and clean up dead indexes (Janitor Mode)")
|
|
26
|
+
print("Type 'quit' at any prompt to exit.")
|
|
27
|
+
print("="*40)
|
|
28
|
+
|
|
29
|
+
choice = input("\nSelect option (1/2 or 'quit'): ").strip().lower()
|
|
30
|
+
|
|
31
|
+
if choice == "quit":
|
|
32
|
+
print("\n๐ Exiting Index Manager. Keep your database clean!")
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
elif choice == "1":
|
|
36
|
+
while True:
|
|
37
|
+
query = input("\nPaste SQL query to analyze (type 'back' for main menu, 'quit' to exit):\n> ").strip()
|
|
38
|
+
|
|
39
|
+
if query.lower() == 'quit':
|
|
40
|
+
print("\n๐ Exiting Index Manager. Keep your database clean!")
|
|
41
|
+
return
|
|
42
|
+
if query.lower() == 'back':
|
|
43
|
+
break
|
|
44
|
+
if not query:
|
|
45
|
+
continue
|
|
46
|
+
if not is_safe_query(query):
|
|
47
|
+
print("\nโ SECURITY ERROR: Only read-only SELECT queries are allowed for auditing.")
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
anomalies, execution_time = manager.analyze_query(query)
|
|
52
|
+
|
|
53
|
+
if not anomalies:
|
|
54
|
+
print("\n" + "="*40)
|
|
55
|
+
print("โ
SUCCESS: The query is properly indexed and optimized.")
|
|
56
|
+
print(f" Execution Time: {execution_time} ms (Index Scan active)")
|
|
57
|
+
print("="*40)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
for am in anomalies:
|
|
61
|
+
print("\n" + "="*40)
|
|
62
|
+
print(f"โ ๏ธ ANOMALY DETECTED: Seq Scan on table '{am['table']}'")
|
|
63
|
+
print(f" Execution Time: {am.get('execution_time', 0.0)} ms")
|
|
64
|
+
|
|
65
|
+
q_filter = am.get('filter')
|
|
66
|
+
if q_filter:
|
|
67
|
+
print(f" Offending filter clause: {q_filter}")
|
|
68
|
+
else:
|
|
69
|
+
print(" Offending filter clause: [Full Table Scan - No filter constraint applied]")
|
|
70
|
+
print("="*40)
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
connection.rollback()
|
|
74
|
+
print(f"\nโ SQL SYNTAX OR DATABASE ERROR: {e}")
|
|
75
|
+
print("Please verify your table names, column names, or SQL syntax.")
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
elif choice == "2":
|
|
79
|
+
print("\n๐ Scanning relational schemas for unused metadata indexes...")
|
|
80
|
+
unused = manager.get_unused_indexes()
|
|
81
|
+
if not unused:
|
|
82
|
+
print("โ
Perfect! No dead indexes found in the database catalog.")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
print(f"โ ๏ธ Identified {len(unused)} unused indexes slowing down write operations:")
|
|
86
|
+
for idx in unused:
|
|
87
|
+
print(f"- '{idx['index']}' on table '{idx['table']}'")
|
|
88
|
+
confirm = input(f" Drop index asynchronously (CONCURRENTLY)? [s/N]: ").lower().strip()
|
|
89
|
+
if confirm == 's':
|
|
90
|
+
try:
|
|
91
|
+
print(f" Removing index metadata in background...")
|
|
92
|
+
manager.drop_index_safely(idx['index'], idx['schema'])
|
|
93
|
+
print(f" ๐ Index '{idx['index']}' dropped successfully!")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f" โ Runtime execution error: {e}")
|
|
96
|
+
else:
|
|
97
|
+
print(" Skipped.")
|
|
98
|
+
else:
|
|
99
|
+
print("\nโ Invalid choice. Please enter 1, 2, or 'quit'.")
|
|
100
|
+
|
|
101
|
+
except KeyboardInterrupt:
|
|
102
|
+
print("\n\n๐ [Control+C] Interrupted by user. Exiting cleanly. Goodbye!")
|
|
103
|
+
sys.exit(0)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
class IndexManagerCore:
|
|
4
|
+
"""
|
|
5
|
+
Core engine handling PostgreSQL index auditing and safe background cleanup.
|
|
6
|
+
Does not require superuser privileges or invasive external extensions.
|
|
7
|
+
"""
|
|
8
|
+
def __init__(self, connection, min_table_rows=1000):
|
|
9
|
+
self.conn = connection
|
|
10
|
+
self.min_table_rows = min_table_rows
|
|
11
|
+
|
|
12
|
+
def _get_table_rows(self, cursor, table_name):
|
|
13
|
+
"""Queries PostgreSQL system catalogs to fetch live tuple statistics."""
|
|
14
|
+
try:
|
|
15
|
+
cursor.execute(
|
|
16
|
+
"SELECT n_live_tup FROM pg_stat_user_tables WHERE relname = %s",
|
|
17
|
+
(table_name,)
|
|
18
|
+
)
|
|
19
|
+
res = cursor.fetchone()
|
|
20
|
+
# res รจ una tupla tipo (10000,). Estraiamo il primo elemento res[0]
|
|
21
|
+
return res[0] if res else 0
|
|
22
|
+
except Exception:
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_explain_plan(self, node, anomalies=None):
|
|
27
|
+
"""Recursively traverses the EXPLAIN JSON tree to detect Sequential Scans."""
|
|
28
|
+
if anomalies is None:
|
|
29
|
+
anomalies = []
|
|
30
|
+
|
|
31
|
+
if node.get("Node Type") == "Seq Scan":
|
|
32
|
+
anomalies.append({
|
|
33
|
+
"table": node.get("Relation Name"),
|
|
34
|
+
"filter": node.get("Filter")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if "Plans" in node:
|
|
38
|
+
for sub_node in node["Plans"]:
|
|
39
|
+
self.parse_explain_plan(sub_node, anomalies)
|
|
40
|
+
|
|
41
|
+
return anomalies
|
|
42
|
+
|
|
43
|
+
def analyze_query(self, query):
|
|
44
|
+
"""
|
|
45
|
+
Executes an EXPLAIN (ANALYZE, FORMAT JSON) on the target query.
|
|
46
|
+
Safely unpacks the nested driver structure without bracket syntax bugs.
|
|
47
|
+
"""
|
|
48
|
+
anomalies_detected = []
|
|
49
|
+
with self.conn.cursor() as cursor:
|
|
50
|
+
cursor.execute(f"EXPLAIN (ANALYZE, FORMAT JSON) {query}")
|
|
51
|
+
raw_result = cursor.fetchone()
|
|
52
|
+
|
|
53
|
+
# Step 1: Extract the list from the psycopg2 tuple
|
|
54
|
+
if isinstance(raw_result, tuple) and len(raw_result) > 0:
|
|
55
|
+
raw_result = raw_result[0]
|
|
56
|
+
|
|
57
|
+
# Step 2: Extract the internal dictionary from the list
|
|
58
|
+
if isinstance(raw_result, list) and len(raw_result) > 0:
|
|
59
|
+
raw_result = raw_result[0]
|
|
60
|
+
|
|
61
|
+
# Final validation: check if we successfully unpacked into a dictionary
|
|
62
|
+
if not isinstance(raw_result, dict):
|
|
63
|
+
raise ValueError("Failed to parse PostgreSQL JSON plan into a dictionary configuration.")
|
|
64
|
+
|
|
65
|
+
plan = raw_result.get("Plan")
|
|
66
|
+
execution_time_ms = raw_result.get("Execution Time", 0.0)
|
|
67
|
+
|
|
68
|
+
if not plan:
|
|
69
|
+
raise ValueError("The 'Plan' root node is missing from the database engine response.")
|
|
70
|
+
|
|
71
|
+
raw_anomalies = self.parse_explain_plan(plan)
|
|
72
|
+
|
|
73
|
+
for anomaly in raw_anomalies:
|
|
74
|
+
table = anomaly["table"]
|
|
75
|
+
rows = self._get_table_rows(cursor, table)
|
|
76
|
+
|
|
77
|
+
if rows >= self.min_table_rows:
|
|
78
|
+
anomaly["table_rows"] = rows
|
|
79
|
+
anomaly["execution_time"] = execution_time_ms
|
|
80
|
+
anomalies_detected.append(anomaly)
|
|
81
|
+
|
|
82
|
+
return anomalies_detected, execution_time_ms
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_unused_indexes(self):
|
|
87
|
+
"""Scans pg_stat_user_indexes for dead indexes (idx_scan = 0)."""
|
|
88
|
+
sql = """
|
|
89
|
+
SELECT schemaname, relname AS table_name, indexrelname AS index_name
|
|
90
|
+
FROM pg_stat_user_indexes
|
|
91
|
+
JOIN pg_index ON pg_stat_user_indexes.indexrelid = pg_index.indexrelid
|
|
92
|
+
WHERE idx_scan = 0 AND indisunique = FALSE
|
|
93
|
+
ORDER BY relname ASC;
|
|
94
|
+
"""
|
|
95
|
+
unused = []
|
|
96
|
+
with self.conn.cursor() as cursor:
|
|
97
|
+
cursor.execute(sql)
|
|
98
|
+
for row in cursor.fetchall():
|
|
99
|
+
unused.append({"schema": row[0], "table": row[1], "index": row[2]})
|
|
100
|
+
return unused
|
|
101
|
+
|
|
102
|
+
def drop_index_safely(self, index_name, schema="public"):
|
|
103
|
+
"""Drops an index asynchronously without acquiring exclusive locks."""
|
|
104
|
+
sql = f"DROP INDEX CONCURRENTLY {schema}.{index_name};"
|
|
105
|
+
with self.conn.cursor() as cursor:
|
|
106
|
+
cursor.execute(sql)
|
|
107
|
+
self.conn.commit()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pg_idx_manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, framework-agnostic developer linter and cleanup tool for PostgreSQL indexes.
|
|
5
|
+
Author-email: Pierpaolo <tua_email@example.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
14
|
+
|
|
15
|
+
# PG Index Manager & Janitor ๐
|
|
16
|
+
|
|
17
|
+
A lightweight, framework-agnostic Python library designed for developers to audit database queries, surface runtime performance bottlenecks, and safely maintain PostgreSQL indexes.
|
|
18
|
+
|
|
19
|
+
Unlike heavy Application Performance Monitoring (APM) tools that require invasive database extensions (like HypoPG) or expensive SaaS subscriptions, this library operates entirely inside your application or via a clean CLI using native PostgreSQL capabilities.
|
|
20
|
+
|
|
21
|
+
## The Problem It Solves: "ORM Blindness"
|
|
22
|
+
Modern Object-Relational Mappers (ORMs) like Django or SQLAlchemy maximize developer productivity but hide the underlying SQL execution plan. A seemingly innocent line of Python code can silently trigger a **Sequential Scan (Full Table Scan)** across millions of rows, saturating server CPU and disk I/O.
|
|
23
|
+
|
|
24
|
+
Since Large Language Models (AI) cannot inspect your production database cardinality, table sizes, or live index catalogs, they cannot reliably predict query performance. This library bridges that gap by running live `EXPLAIN ANALYZE` inspections directly on the database engine.
|
|
25
|
+
|
|
26
|
+
## Key Features
|
|
27
|
+
|
|
28
|
+
1. **Agnostic Performance Auditing**: Intercepts raw SQL queries, recursively parses the native PostgreSQL execution tree, and catches performance anomalies (**Sequential Scans**) with exact millisecond runtimes.
|
|
29
|
+
2. **Safe Janitor Mode (Interactive CLI)**: Scans PostgreSQL system catalogs (`pg_stat_user_indexes`) to discover dead, unused indexes that are slowing down your `INSERT`/`UPDATE` mutations. It allows you to drop them interactively using `DROP INDEX CONCURRENTLY` without locking tables or risking production application downtime.
|
|
30
|
+
3. **Zero-Dependency Architecture**: Does not require root or superuser privileges on the database server. If you can connect to the database, you can run this library.
|
|
31
|
+
|
|
32
|
+
## Architectural Architecture: Where does it live?
|
|
33
|
+
Because the core engine requires only a raw query string and a standard database connection, it is completely independent of your web framework. You can integrate it at the lowest layer of your infrastructure:
|
|
34
|
+
|
|
35
|
+
* **At the Driver Level (`psycopg2`)**: By extending the native database cursor, you can automatically audit **100% of your application queries** (Django, FastAPI, Flask, or raw SQL) before they hit the wire.
|
|
36
|
+
* **At the ORM Engine Level**: Easily hooks into global ORM events (e.g., SQLAlchemy's `before_cursor_execute`) to catch hidden query costs implicitly across all Services and Repositories with zero business-code modification.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Getting Started & Testing Locally ๐งช
|
|
41
|
+
|
|
42
|
+
This repository includes a fully containerized testing environment to observe performance optimization in real-time.
|
|
43
|
+
|
|
44
|
+
### 1. Spin up the isolated test database
|
|
45
|
+
```bash
|
|
46
|
+
docker compose up -d
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Install the library locally in editable mode
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Run the Core Agnostic Test
|
|
55
|
+
This script populates the database with **10,000 mock records**, executes an unindexed query, catches the Sequential Scan risk, and launches the persistent interactive CLI:
|
|
56
|
+
```bash
|
|
57
|
+
python tests/run_test.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 4. Run the ORM Integration Test
|
|
61
|
+
Observe how the library seamlessly intercepts real-time query metrics generated implicitly by SQLAlchemy ORM models:
|
|
62
|
+
```bash
|
|
63
|
+
python tests/test_orm.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
MIT License. Free to use and extend.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pg_idx_manager/__init__.py
|
|
4
|
+
pg_idx_manager/cli.py
|
|
5
|
+
pg_idx_manager/core.py
|
|
6
|
+
pg_idx_manager.egg-info/PKG-INFO
|
|
7
|
+
pg_idx_manager.egg-info/SOURCES.txt
|
|
8
|
+
pg_idx_manager.egg-info/dependency_links.txt
|
|
9
|
+
pg_idx_manager.egg-info/requires.txt
|
|
10
|
+
pg_idx_manager.egg-info/top_level.txt
|
|
11
|
+
tests/test_orm.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psycopg2-binary>=2.9.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pg_idx_manager
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pg_idx_manager"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight, framework-agnostic developer linter and cleanup tool for PostgreSQL indexes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [{ name = "Pierpaolo", email = "tua_email@example.com" }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Database :: Database Engines/Servers",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"psycopg2-binary>=2.9.0"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["pg_idx_manager"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from sqlalchemy import create_engine, Column, Integer, String, Numeric
|
|
2
|
+
from sqlalchemy.orm import declarative_base, sessionmaker
|
|
3
|
+
from pg_index_manager.decorators import audit_index
|
|
4
|
+
|
|
5
|
+
# 1. Database Connection Configuration
|
|
6
|
+
DATABASE_URL = "postgresql+psycopg2://tester:supersecretpassword@localhost:5432/testing_perf"
|
|
7
|
+
engine = create_engine(DATABASE_URL)
|
|
8
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
9
|
+
Base = declarative_base()
|
|
10
|
+
|
|
11
|
+
# 2. ORM Model Mapping
|
|
12
|
+
class Order(Base):
|
|
13
|
+
__tablename__ = "orders"
|
|
14
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
15
|
+
customer_name = Column(String)
|
|
16
|
+
status = Column(String)
|
|
17
|
+
amount = Column(Numeric)
|
|
18
|
+
|
|
19
|
+
# 3. Intercepted Function
|
|
20
|
+
@audit_index(conn_param="raw_conn", min_rows=0)
|
|
21
|
+
def execute_backend_task(raw_conn, query):
|
|
22
|
+
with raw_conn.cursor() as cursor:
|
|
23
|
+
cursor.execute(query)
|
|
24
|
+
return cursor.fetchall()
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
db_session = SessionLocal()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Recuperiamo la connessione psycopg2 grezza in modo moderno
|
|
31
|
+
raw_connection = db_session.connection()._dbapi_connection
|
|
32
|
+
|
|
33
|
+
# Metti qui la tua query da testare
|
|
34
|
+
orm_query = db_session.query(Order).filter(Order.customer_name == "pierpaolo")
|
|
35
|
+
|
|
36
|
+
# Compile and bind parameters to get the raw SQL string
|
|
37
|
+
sql_string = str(orm_query.statement.compile(engine, compile_kwargs={"literal_binds": True}))
|
|
38
|
+
|
|
39
|
+
# Execute: the decorator handles the audit output automatically
|
|
40
|
+
execute_backend_task(raw_conn=raw_connection, query=sql_string)
|
|
41
|
+
|
|
42
|
+
finally:
|
|
43
|
+
db_session.close()
|
|
44
|
+
|