coding-client 1.21.2__py3-none-any.whl
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.
- coding_client/__init__.py +1 -0
- coding_client/api/__init__.py +19 -0
- coding_client/api/exception_handlers.py +158 -0
- coding_client/api/logging_setup.py +44 -0
- coding_client/api/metrics.py +40 -0
- coding_client/api/openapi_config.py +124 -0
- coding_client/api/request_logging.py +60 -0
- coding_client/api/validation.py +150 -0
- coding_client/auth/__init__.py +78 -0
- coding_client/auth/_default_tenant.py +132 -0
- coding_client/auth/_header_trust.py +98 -0
- coding_client/auth/_onbehalf.py +315 -0
- coding_client/auth/_trusted_networks.py +94 -0
- coding_client/auth/dependencies.py +61 -0
- coding_client/auth/middleware.py +445 -0
- coding_client/auth/models.py +83 -0
- coding_client/auth/outbound.py +137 -0
- coding_client/auth/rbac.py +21 -0
- coding_client/auth/rbac_coding.py +5 -0
- coding_client/auth/tenant.py +650 -0
- coding_client/auth/websocket.py +148 -0
- coding_client/clients/__init__.py +24 -0
- coding_client/clients/base.py +131 -0
- coding_client/clients/coding_ai.py +33 -0
- coding_client/clients/coding_analysis.py +36 -0
- coding_client/clients/coding_containers.py +45 -0
- coding_client/clients/coding_github.py +127 -0
- coding_client/clients/coding_realtime.py +127 -0
- coding_client/clients/coding_security.py +51 -0
- coding_client/clients/coding_testing.py +31 -0
- coding_client/clients/coding_workspaces.py +265 -0
- coding_client/config/__init__.py +12 -0
- coding_client/config/base_settings.py +61 -0
- coding_client/config/yaml_config_loader.py +172 -0
- coding_client/db/__init__.py +42 -0
- coding_client/db/db_types.py +122 -0
- coding_client/db/exceptions.py +59 -0
- coding_client/db/postgres_connection.py +572 -0
- coding_client/db/tenant_query_helper.py +417 -0
- coding_client/db_init.py +49 -0
- coding_client/exceptions/__init__.py +67 -0
- coding_client/exceptions/api_exceptions.py +395 -0
- coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
- coding_client/interfaces/__init__.py +22 -0
- coding_client/interfaces/cache_interface.py +179 -0
- coding_client/interfaces/container_interface.py +49 -0
- coding_client/interfaces/docker_interface.py +283 -0
- coding_client/interfaces/graph_interface.py +28 -0
- coding_client/interfaces/security_interface.py +72 -0
- coding_client/interfaces/webhook_interface.py +67 -0
- coding_client/llm/__init__.py +54 -0
- coding_client/llm/base_agent.py +195 -0
- coding_client/llm/context.py +121 -0
- coding_client/llm/responses.py +102 -0
- coding_client/llm/routing.py +82 -0
- coding_client/llm/settings.py +102 -0
- coding_client/models/__init__.py +62 -0
- coding_client/models/agent.py +38 -0
- coding_client/models/ai_test_failure.py +745 -0
- coding_client/models/analysis.py +330 -0
- coding_client/models/architecture.py +685 -0
- coding_client/models/bug.py +128 -0
- coding_client/models/code_graph.py +317 -0
- coding_client/models/container.py +241 -0
- coding_client/models/core.py +878 -0
- coding_client/models/docker_container.py +104 -0
- coding_client/models/github.py +293 -0
- coding_client/models/logging.py +29 -0
- coding_client/models/package_manager.py +257 -0
- coding_client/models/pr_validation.py +440 -0
- coding_client/models/pydantic_ai.py +395 -0
- coding_client/models/repo_scanner.py +249 -0
- coding_client/models/responses.py +431 -0
- coding_client/models/security.py +185 -0
- coding_client/models/static_analysis.py +508 -0
- coding_client/models/system.py +48 -0
- coding_client/models/test_executor.py +204 -0
- coding_client/models/testing.py +673 -0
- coding_client/models/testing_config.py +200 -0
- coding_client/models/validation.py +127 -0
- coding_client/models_init.py +272 -0
- coding_client/telemetry.py +195 -0
- coding_client/testing.py +180 -0
- coding_client/types/__init__.py +130 -0
- coding_client/types/constraint_types.py +276 -0
- coding_client/types/core_types.py +112 -0
- coding_client/types/state_machine.py +443 -0
- coding_client/utils/__init__.py +86 -0
- coding_client/utils/archive_utils.py +387 -0
- coding_client/utils/ast_debug.py +45 -0
- coding_client/utils/async_timeout.py +364 -0
- coding_client/utils/constants.py +518 -0
- coding_client/utils/content_processing.py +326 -0
- coding_client/utils/database.py +916 -0
- coding_client/utils/encoding_helper.py +222 -0
- coding_client/utils/enum_helpers.py +166 -0
- coding_client/utils/file_operations.py +177 -0
- coding_client/utils/filesystem_ops.py +528 -0
- coding_client/utils/http_client.py +562 -0
- coding_client/utils/path_utils.py +471 -0
- coding_client/utils/profiling.py +421 -0
- coding_client/utils/response_builder.py +240 -0
- coding_client/utils/retry_helper.py +712 -0
- coding_client/utils/security_validation.py +423 -0
- coding_client-1.21.2.dist-info/METADATA +234 -0
- coding_client-1.21.2.dist-info/RECORD +109 -0
- coding_client-1.21.2.dist-info/WHEEL +5 -0
- coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
- coding_client-1.21.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL connection manager for code_graph_helper.
|
|
3
|
+
|
|
4
|
+
Designed to work with PgBouncer connection pooler.
|
|
5
|
+
Applications connect through PgBouncer, which handles connection pooling to PostgreSQL.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
Application -> PgBouncer (connection pooler) -> PostgreSQL
|
|
9
|
+
|
|
10
|
+
PgBouncer handles:
|
|
11
|
+
- Connection pooling
|
|
12
|
+
- Connection reuse
|
|
13
|
+
- Load balancing
|
|
14
|
+
|
|
15
|
+
This module provides:
|
|
16
|
+
- Simple connection creation (no pooling needed)
|
|
17
|
+
- Transaction management
|
|
18
|
+
- Query builders for graph operations
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from collections.abc import AsyncIterator
|
|
25
|
+
from contextlib import asynccontextmanager
|
|
26
|
+
|
|
27
|
+
import psycopg
|
|
28
|
+
from psycopg import sql
|
|
29
|
+
|
|
30
|
+
# get_settings is provided by the service layer (e.g., from app.core.config)
|
|
31
|
+
# Services must call set_settings_provider() before using postgres_connection
|
|
32
|
+
_settings_provider = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_settings_provider(provider):
|
|
36
|
+
"""Set the settings provider function. Call this from your service startup."""
|
|
37
|
+
global _settings_provider
|
|
38
|
+
_settings_provider = provider
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_settings():
|
|
42
|
+
"""Get settings from the configured provider."""
|
|
43
|
+
if _settings_provider is None:
|
|
44
|
+
raise RuntimeError("Settings provider not configured. Call set_settings_provider() first.")
|
|
45
|
+
return _settings_provider()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
# Connection parameters (cached)
|
|
51
|
+
_connection_params: dict | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_connection_params() -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Get PostgreSQL connection parameters.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary of connection parameters for psycopg
|
|
60
|
+
"""
|
|
61
|
+
global _connection_params
|
|
62
|
+
|
|
63
|
+
if _connection_params is None:
|
|
64
|
+
settings = get_settings()
|
|
65
|
+
db_settings = settings.database
|
|
66
|
+
|
|
67
|
+
_connection_params = db_settings.as_connection_params()
|
|
68
|
+
|
|
69
|
+
logger.info(
|
|
70
|
+
"PostgreSQL connection configured via PgBouncer: %s:%s/%s",
|
|
71
|
+
_connection_params["host"],
|
|
72
|
+
_connection_params["port"],
|
|
73
|
+
_connection_params["dbname"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return _connection_params
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def init_postgres_pool():
|
|
80
|
+
"""
|
|
81
|
+
Initialize PostgreSQL connection (for compatibility).
|
|
82
|
+
|
|
83
|
+
Note: When using PgBouncer, we don't create a connection pool.
|
|
84
|
+
PgBouncer handles pooling. This function just validates connectivity.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
None
|
|
88
|
+
"""
|
|
89
|
+
logger.info("Validating PostgreSQL connection through PgBouncer...")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
async with get_postgres_connection() as conn:
|
|
93
|
+
cur = await conn.execute("SELECT version()")
|
|
94
|
+
row = await cur.fetchone()
|
|
95
|
+
version = row[0] if row else "unknown"
|
|
96
|
+
logger.info(f"PostgreSQL connection established (via PgBouncer): {version[:80]}...")
|
|
97
|
+
|
|
98
|
+
# Verify we can access the schema
|
|
99
|
+
cur = await conn.execute(
|
|
100
|
+
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'"
|
|
101
|
+
)
|
|
102
|
+
row = await cur.fetchone()
|
|
103
|
+
table_count = row[0] if row else 0
|
|
104
|
+
logger.info(f"Found {table_count} tables in database")
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
# SECURITY: Don't log exception details that might contain passwords
|
|
108
|
+
logger.error("Failed to connect to PostgreSQL through PgBouncer: %s", type(e).__name__)
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def close_postgres_pool():
|
|
113
|
+
"""
|
|
114
|
+
Close PostgreSQL connections (for compatibility).
|
|
115
|
+
|
|
116
|
+
Note: When using PgBouncer, individual connections are closed automatically.
|
|
117
|
+
This is a no-op for compatibility with pooled connection APIs.
|
|
118
|
+
"""
|
|
119
|
+
logger.info("PostgreSQL connections will be closed by PgBouncer")
|
|
120
|
+
# No-op when using PgBouncer
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@asynccontextmanager
|
|
124
|
+
async def get_postgres_connection() -> AsyncIterator[psycopg.AsyncConnection]:
|
|
125
|
+
"""
|
|
126
|
+
Context manager for creating a PostgreSQL connection through PgBouncer.
|
|
127
|
+
|
|
128
|
+
Usage:
|
|
129
|
+
async with get_postgres_connection() as conn:
|
|
130
|
+
cur = await conn.execute("SELECT * FROM nodes LIMIT 10")
|
|
131
|
+
rows = cur.fetchall()
|
|
132
|
+
|
|
133
|
+
Note:
|
|
134
|
+
- Connects through PgBouncer, which handles connection pooling
|
|
135
|
+
- Each connection is lightweight and managed by PgBouncer
|
|
136
|
+
- Connection is automatically closed when context exits
|
|
137
|
+
|
|
138
|
+
Yields:
|
|
139
|
+
psycopg AsyncConnection instance
|
|
140
|
+
"""
|
|
141
|
+
import asyncio
|
|
142
|
+
|
|
143
|
+
params = _get_connection_params()
|
|
144
|
+
|
|
145
|
+
# Build conninfo string from params dict
|
|
146
|
+
conninfo_parts = []
|
|
147
|
+
param_map = {
|
|
148
|
+
"host": "host",
|
|
149
|
+
"port": "port",
|
|
150
|
+
"user": "user",
|
|
151
|
+
"password": "password",
|
|
152
|
+
"dbname": "dbname",
|
|
153
|
+
}
|
|
154
|
+
for key, conninfo_key in param_map.items():
|
|
155
|
+
if key in params and params[key]:
|
|
156
|
+
conninfo_parts.append(f"{conninfo_key}={params[key]}")
|
|
157
|
+
|
|
158
|
+
# Add server_settings as options
|
|
159
|
+
server_settings = params.get("server_settings", {})
|
|
160
|
+
if server_settings.get("application_name"):
|
|
161
|
+
conninfo_parts.append(f"application_name={server_settings['application_name']}")
|
|
162
|
+
|
|
163
|
+
conninfo = " ".join(conninfo_parts)
|
|
164
|
+
|
|
165
|
+
conn = await asyncio.wait_for(
|
|
166
|
+
psycopg.AsyncConnection.connect(conninfo=conninfo, autocommit=True),
|
|
167
|
+
timeout=params.get("timeout", 30.0),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Set session parameters
|
|
172
|
+
settings = get_settings()
|
|
173
|
+
db_settings = settings.database
|
|
174
|
+
|
|
175
|
+
if db_settings.statement_timeout > 0:
|
|
176
|
+
await conn.execute(
|
|
177
|
+
sql.SQL("SET statement_timeout = {}").format(
|
|
178
|
+
sql.Literal(db_settings.statement_timeout)
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
yield conn
|
|
183
|
+
|
|
184
|
+
finally:
|
|
185
|
+
await conn.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@asynccontextmanager
|
|
189
|
+
async def postgres_transaction() -> AsyncIterator[psycopg.AsyncConnection]:
|
|
190
|
+
"""
|
|
191
|
+
Context manager for PostgreSQL transactions.
|
|
192
|
+
|
|
193
|
+
Usage:
|
|
194
|
+
async with postgres_transaction() as conn:
|
|
195
|
+
await conn.execute("INSERT INTO nodes ...")
|
|
196
|
+
await conn.execute("INSERT INTO edges ...")
|
|
197
|
+
# Automatically commits on success, rolls back on exception
|
|
198
|
+
|
|
199
|
+
Yields:
|
|
200
|
+
psycopg AsyncConnection instance with active transaction
|
|
201
|
+
"""
|
|
202
|
+
import asyncio
|
|
203
|
+
|
|
204
|
+
params = _get_connection_params()
|
|
205
|
+
|
|
206
|
+
# Build conninfo string from params dict
|
|
207
|
+
conninfo_parts = []
|
|
208
|
+
param_map = {
|
|
209
|
+
"host": "host",
|
|
210
|
+
"port": "port",
|
|
211
|
+
"user": "user",
|
|
212
|
+
"password": "password",
|
|
213
|
+
"dbname": "dbname",
|
|
214
|
+
}
|
|
215
|
+
for key, conninfo_key in param_map.items():
|
|
216
|
+
if key in params and params[key]:
|
|
217
|
+
conninfo_parts.append(f"{conninfo_key}={params[key]}")
|
|
218
|
+
|
|
219
|
+
server_settings = params.get("server_settings", {})
|
|
220
|
+
if server_settings.get("application_name"):
|
|
221
|
+
conninfo_parts.append(f"application_name={server_settings['application_name']}")
|
|
222
|
+
|
|
223
|
+
conninfo = " ".join(conninfo_parts)
|
|
224
|
+
|
|
225
|
+
# autocommit=False enables transaction mode
|
|
226
|
+
conn = await asyncio.wait_for(
|
|
227
|
+
psycopg.AsyncConnection.connect(conninfo=conninfo, autocommit=False),
|
|
228
|
+
timeout=params.get("timeout", 30.0),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
yield conn
|
|
233
|
+
await conn.commit()
|
|
234
|
+
except Exception:
|
|
235
|
+
await conn.rollback()
|
|
236
|
+
raise
|
|
237
|
+
finally:
|
|
238
|
+
await conn.close()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class PostgresQueryBuilder:
|
|
242
|
+
"""
|
|
243
|
+
Helper class for building secure PostgreSQL graph queries with parameterized statements.
|
|
244
|
+
|
|
245
|
+
SECURITY: All methods use parameterized queries to prevent SQL injection.
|
|
246
|
+
|
|
247
|
+
TENANT ISOLATION: All queries that accept tenant_id will filter results to only
|
|
248
|
+
include data belonging to that tenant. When tenant_id is None, no filtering is applied
|
|
249
|
+
(for admin access or legacy compatibility).
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def find_descendants(
|
|
254
|
+
start_node_id: str,
|
|
255
|
+
max_depth: int = 10,
|
|
256
|
+
edge_types: list[str] | None = None,
|
|
257
|
+
tenant_id: str | None = None,
|
|
258
|
+
) -> tuple[str, list]:
|
|
259
|
+
"""
|
|
260
|
+
Build recursive CTE query to find all descendants of a node.
|
|
261
|
+
|
|
262
|
+
SECURITY: Uses parameterized queries to prevent SQL injection.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
start_node_id: Starting node ID
|
|
266
|
+
max_depth: Maximum traversal depth
|
|
267
|
+
edge_types: Optional list of edge types to filter by
|
|
268
|
+
tenant_id: Optional tenant ID for data isolation
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Tuple of (SQL query string with placeholders, list of parameter values)
|
|
272
|
+
|
|
273
|
+
Usage:
|
|
274
|
+
query, params = PostgresQueryBuilder.find_descendants("node_123", tenant_id="tenant_abc")
|
|
275
|
+
cur = await conn.execute(query, params)
|
|
276
|
+
results = cur.fetchall()
|
|
277
|
+
"""
|
|
278
|
+
params: list[str | int | list[str]] = [start_node_id, start_node_id, max_depth]
|
|
279
|
+
edge_filter = ""
|
|
280
|
+
tenant_filter = ""
|
|
281
|
+
|
|
282
|
+
if edge_types:
|
|
283
|
+
params.append(edge_types)
|
|
284
|
+
edge_filter = "AND e.edge_type = ANY(%s)"
|
|
285
|
+
|
|
286
|
+
if tenant_id:
|
|
287
|
+
params.extend([tenant_id, tenant_id])
|
|
288
|
+
tenant_filter = "AND n.tenant_id = %s AND e.tenant_id = %s"
|
|
289
|
+
|
|
290
|
+
query = f"""
|
|
291
|
+
WITH RECURSIVE descendants AS (
|
|
292
|
+
-- Base case
|
|
293
|
+
SELECT
|
|
294
|
+
e.target_id as node_id,
|
|
295
|
+
n.name,
|
|
296
|
+
n.node_type,
|
|
297
|
+
1 as depth,
|
|
298
|
+
ARRAY[%s, e.target_id] as path,
|
|
299
|
+
e.edge_type
|
|
300
|
+
FROM edges e
|
|
301
|
+
JOIN nodes n ON e.target_id = n.node_id
|
|
302
|
+
WHERE e.source_id = %s
|
|
303
|
+
{edge_filter}
|
|
304
|
+
{tenant_filter}
|
|
305
|
+
|
|
306
|
+
UNION ALL
|
|
307
|
+
|
|
308
|
+
-- Recursive case
|
|
309
|
+
SELECT
|
|
310
|
+
e.target_id,
|
|
311
|
+
n.name,
|
|
312
|
+
n.node_type,
|
|
313
|
+
d.depth + 1,
|
|
314
|
+
d.path || e.target_id,
|
|
315
|
+
e.edge_type
|
|
316
|
+
FROM descendants d
|
|
317
|
+
JOIN edges e ON d.node_id = e.source_id
|
|
318
|
+
JOIN nodes n ON e.target_id = n.node_id
|
|
319
|
+
WHERE e.target_id != ALL(d.path)
|
|
320
|
+
AND d.depth < %s
|
|
321
|
+
{edge_filter}
|
|
322
|
+
{tenant_filter}
|
|
323
|
+
)
|
|
324
|
+
SELECT DISTINCT
|
|
325
|
+
node_id,
|
|
326
|
+
name,
|
|
327
|
+
node_type,
|
|
328
|
+
depth,
|
|
329
|
+
path,
|
|
330
|
+
edge_type
|
|
331
|
+
FROM descendants
|
|
332
|
+
ORDER BY depth, name
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
return query, params
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def find_ancestors(
|
|
339
|
+
start_node_id: str,
|
|
340
|
+
max_depth: int = 10,
|
|
341
|
+
edge_types: list[str] | None = None,
|
|
342
|
+
tenant_id: str | None = None,
|
|
343
|
+
) -> tuple[str, list]:
|
|
344
|
+
"""
|
|
345
|
+
Build recursive CTE query to find all ancestors of a node.
|
|
346
|
+
|
|
347
|
+
SECURITY: Uses parameterized queries to prevent SQL injection.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
start_node_id: Starting node ID
|
|
351
|
+
max_depth: Maximum traversal depth
|
|
352
|
+
edge_types: Optional list of edge types to filter by
|
|
353
|
+
tenant_id: Optional tenant ID for data isolation
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Tuple of (SQL query string with placeholders, list of parameter values)
|
|
357
|
+
|
|
358
|
+
Usage:
|
|
359
|
+
query, params = PostgresQueryBuilder.find_ancestors("node_123", tenant_id="tenant_abc")
|
|
360
|
+
cur = await conn.execute(query, params)
|
|
361
|
+
results = cur.fetchall()
|
|
362
|
+
"""
|
|
363
|
+
params: list[str | int | list[str]] = [start_node_id, start_node_id, max_depth]
|
|
364
|
+
edge_filter = ""
|
|
365
|
+
tenant_filter = ""
|
|
366
|
+
|
|
367
|
+
if edge_types:
|
|
368
|
+
params.append(edge_types)
|
|
369
|
+
edge_filter = "AND e.edge_type = ANY(%s)"
|
|
370
|
+
|
|
371
|
+
if tenant_id:
|
|
372
|
+
params.extend([tenant_id, tenant_id])
|
|
373
|
+
tenant_filter = "AND n.tenant_id = %s AND e.tenant_id = %s"
|
|
374
|
+
|
|
375
|
+
query = f"""
|
|
376
|
+
WITH RECURSIVE ancestors AS (
|
|
377
|
+
-- Base case
|
|
378
|
+
SELECT
|
|
379
|
+
e.source_id as node_id,
|
|
380
|
+
n.name,
|
|
381
|
+
n.node_type,
|
|
382
|
+
1 as depth,
|
|
383
|
+
ARRAY[e.source_id, %s] as path,
|
|
384
|
+
e.edge_type
|
|
385
|
+
FROM edges e
|
|
386
|
+
JOIN nodes n ON e.source_id = n.node_id
|
|
387
|
+
WHERE e.target_id = %s
|
|
388
|
+
{edge_filter}
|
|
389
|
+
{tenant_filter}
|
|
390
|
+
|
|
391
|
+
UNION ALL
|
|
392
|
+
|
|
393
|
+
-- Recursive case
|
|
394
|
+
SELECT
|
|
395
|
+
e.source_id,
|
|
396
|
+
n.name,
|
|
397
|
+
n.node_type,
|
|
398
|
+
a.depth + 1,
|
|
399
|
+
e.source_id || a.path,
|
|
400
|
+
e.edge_type
|
|
401
|
+
FROM ancestors a
|
|
402
|
+
JOIN edges e ON a.node_id = e.target_id
|
|
403
|
+
JOIN nodes n ON e.source_id = n.node_id
|
|
404
|
+
WHERE e.source_id != ALL(a.path)
|
|
405
|
+
AND a.depth < %s
|
|
406
|
+
{edge_filter}
|
|
407
|
+
{tenant_filter}
|
|
408
|
+
)
|
|
409
|
+
SELECT DISTINCT
|
|
410
|
+
node_id,
|
|
411
|
+
name,
|
|
412
|
+
node_type,
|
|
413
|
+
depth,
|
|
414
|
+
path,
|
|
415
|
+
edge_type
|
|
416
|
+
FROM ancestors
|
|
417
|
+
ORDER BY depth, name
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
return query, params
|
|
421
|
+
|
|
422
|
+
@staticmethod
|
|
423
|
+
def find_call_graph(
|
|
424
|
+
function_name: str,
|
|
425
|
+
direction: str = "outgoing",
|
|
426
|
+
max_depth: int = 3,
|
|
427
|
+
tenant_id: str | None = None,
|
|
428
|
+
) -> tuple[str, list]:
|
|
429
|
+
"""
|
|
430
|
+
Build query to find function call graph.
|
|
431
|
+
|
|
432
|
+
SECURITY: Uses parameterized queries to prevent SQL injection.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
function_name: Function name to start from
|
|
436
|
+
direction: 'outgoing' for callees, 'incoming' for callers
|
|
437
|
+
max_depth: Maximum call depth
|
|
438
|
+
tenant_id: Optional tenant ID for data isolation
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Tuple of (SQL query string with placeholders, list of parameter values)
|
|
442
|
+
|
|
443
|
+
Usage:
|
|
444
|
+
query, params = PostgresQueryBuilder.find_call_graph("my_function", tenant_id="tenant_abc")
|
|
445
|
+
cur = await conn.execute(query, params)
|
|
446
|
+
results = cur.fetchall()
|
|
447
|
+
"""
|
|
448
|
+
params = [function_name, function_name, max_depth]
|
|
449
|
+
tenant_filter = ""
|
|
450
|
+
|
|
451
|
+
if tenant_id:
|
|
452
|
+
params.append(tenant_id)
|
|
453
|
+
tenant_filter = "AND n.tenant_id = %s"
|
|
454
|
+
|
|
455
|
+
if direction == "outgoing":
|
|
456
|
+
query = f"""
|
|
457
|
+
WITH RECURSIVE call_graph AS (
|
|
458
|
+
-- Base case: direct calls
|
|
459
|
+
SELECT
|
|
460
|
+
ce.callee_id as function_id,
|
|
461
|
+
n.name as function_name,
|
|
462
|
+
n.file_path,
|
|
463
|
+
1 as depth,
|
|
464
|
+
ARRAY[(SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1), ce.callee_id] as path
|
|
465
|
+
FROM call_edges ce
|
|
466
|
+
JOIN nodes n ON ce.callee_id = n.node_id
|
|
467
|
+
WHERE ce.caller_id = (SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1)
|
|
468
|
+
{tenant_filter}
|
|
469
|
+
|
|
470
|
+
UNION ALL
|
|
471
|
+
|
|
472
|
+
-- Recursive case
|
|
473
|
+
SELECT
|
|
474
|
+
ce.callee_id,
|
|
475
|
+
n.name,
|
|
476
|
+
n.file_path,
|
|
477
|
+
cg.depth + 1,
|
|
478
|
+
cg.path || ce.callee_id
|
|
479
|
+
FROM call_graph cg
|
|
480
|
+
JOIN call_edges ce ON cg.function_id = ce.caller_id
|
|
481
|
+
JOIN nodes n ON ce.callee_id = n.node_id
|
|
482
|
+
WHERE ce.callee_id != ALL(cg.path)
|
|
483
|
+
AND cg.depth < %s
|
|
484
|
+
{tenant_filter}
|
|
485
|
+
)
|
|
486
|
+
SELECT DISTINCT * FROM call_graph ORDER BY depth, function_name
|
|
487
|
+
"""
|
|
488
|
+
else: # incoming
|
|
489
|
+
query = f"""
|
|
490
|
+
WITH RECURSIVE call_graph AS (
|
|
491
|
+
-- Base case: direct callers
|
|
492
|
+
SELECT
|
|
493
|
+
ce.caller_id as function_id,
|
|
494
|
+
n.name as function_name,
|
|
495
|
+
n.file_path,
|
|
496
|
+
1 as depth,
|
|
497
|
+
ARRAY[ce.caller_id, (SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1)] as path
|
|
498
|
+
FROM call_edges ce
|
|
499
|
+
JOIN nodes n ON ce.caller_id = n.node_id
|
|
500
|
+
WHERE ce.callee_id = (SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1)
|
|
501
|
+
{tenant_filter}
|
|
502
|
+
|
|
503
|
+
UNION ALL
|
|
504
|
+
|
|
505
|
+
-- Recursive case
|
|
506
|
+
SELECT
|
|
507
|
+
ce.caller_id,
|
|
508
|
+
n.name,
|
|
509
|
+
n.file_path,
|
|
510
|
+
cg.depth + 1,
|
|
511
|
+
ce.caller_id || cg.path
|
|
512
|
+
FROM call_graph cg
|
|
513
|
+
JOIN call_edges ce ON cg.function_id = ce.callee_id
|
|
514
|
+
JOIN nodes n ON ce.caller_id = n.node_id
|
|
515
|
+
WHERE ce.caller_id != ALL(cg.path)
|
|
516
|
+
AND cg.depth < %s
|
|
517
|
+
{tenant_filter}
|
|
518
|
+
)
|
|
519
|
+
SELECT DISTINCT * FROM call_graph ORDER BY depth, function_name
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
return query, params
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def find_circular_dependencies() -> str:
|
|
526
|
+
"""
|
|
527
|
+
Build query to find circular import dependencies.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
SQL query string
|
|
531
|
+
"""
|
|
532
|
+
return """
|
|
533
|
+
WITH RECURSIVE dependency_chain AS (
|
|
534
|
+
SELECT
|
|
535
|
+
ie.importer_id as start_node,
|
|
536
|
+
ie.importer_id,
|
|
537
|
+
ie.imported_module,
|
|
538
|
+
ARRAY[ie.importer_id] as path,
|
|
539
|
+
1 as depth
|
|
540
|
+
FROM import_edges ie
|
|
541
|
+
|
|
542
|
+
UNION ALL
|
|
543
|
+
|
|
544
|
+
SELECT
|
|
545
|
+
dc.start_node,
|
|
546
|
+
ie.importer_id,
|
|
547
|
+
ie.imported_module,
|
|
548
|
+
dc.path || ie.importer_id,
|
|
549
|
+
dc.depth + 1
|
|
550
|
+
FROM dependency_chain dc
|
|
551
|
+
JOIN import_edges ie ON dc.imported_module = ie.importer_id
|
|
552
|
+
WHERE ie.importer_id != ALL(dc.path)
|
|
553
|
+
AND dc.depth < 50
|
|
554
|
+
)
|
|
555
|
+
SELECT
|
|
556
|
+
start_node,
|
|
557
|
+
path || ARRAY[imported_module] as cycle_path,
|
|
558
|
+
depth as cycle_length
|
|
559
|
+
FROM dependency_chain
|
|
560
|
+
WHERE imported_module = start_node
|
|
561
|
+
ORDER BY cycle_length, start_node
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# Export public API
|
|
566
|
+
__all__ = [
|
|
567
|
+
"init_postgres_pool",
|
|
568
|
+
"close_postgres_pool",
|
|
569
|
+
"get_postgres_connection",
|
|
570
|
+
"postgres_transaction",
|
|
571
|
+
"PostgresQueryBuilder",
|
|
572
|
+
]
|