Skip to content

Commit cff1e73

Browse files
geekypunkclaude
andcommitted
fix(postgres): introspect the session search_path, not a hardcoded 'public'
Every catalog query in PostgresIntrospectionProvider filtered on the literal 'public', and the Java side tagged every discovered object with a DEFAULT_SCHEMA constant of the same value. Any database that keeps its tables elsewhere was therefore completely invisible to DeepSQL. The failure was silent and looked like success. On a dbt warehouse whose 37 tables live in `marts` and whose `public` holds nothing but extension views, connection init ran all nine stages in 1.1 seconds and wrote "All set! Brain is ready." at 100% — having produced an empty schema snapshot ("tables":[], totalTables 0), zero rag_documents, zero column profiles and zero semantic models. Nothing errored, because nothing was found to process. All 16 predicates now filter on current_schema(), and the schema tags come from a resolveSchema(Connection) helper reading the same value, so the target schema becomes a property of the connection — set it on the role, or with the JDBC currentSchema parameter — instead of a compile-time constant. It falls back to 'public' when current_schema() cannot be read, so existing connections behave exactly as before. getDefaultSchema() deliberately keeps returning 'public'. Its four callers (SchemaIntrospectionService, PgVectorSearchService, AzureSearchService, TrainingService) use it to decide whether a name needs qualifying; returning the session schema there would strip the qualifier off precisely the names that need it, turning marts.dim_person into a bare dim_person. Verified against a live 25GB warehouse: the same connection that previously snapshotted 0 tables now snapshots 37 tables and 837 columns, and init proceeds through real data sampling instead of completing instantly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 57eb925 commit cff1e73

1 file changed

Lines changed: 73 additions & 28 deletions

File tree

backend/src/main/java/com/dbaagent/provider/postgres/PostgresIntrospectionProvider.java

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@
1212
/**
1313
* PostgreSQL implementation of IntrospectionProvider.
1414
* Uses pg_catalog and information_schema for schema introspection.
15+
*
16+
* <p>Every catalog query filters on {@code current_schema()} rather than a
17+
* hardcoded {@code 'public'}. Databases that keep their tables anywhere else —
18+
* a dbt warehouse in {@code marts}, a tenant schema, anything — were previously
19+
* invisible: introspection returned zero tables, so brain initialization
20+
* "COMPLETED" in about a second having learned nothing, and the schema snapshot
21+
* was persisted as an empty table list. Honouring the session search_path makes
22+
* the target schema a property of the connection (set it on the role, or via the
23+
* JDBC {@code currentSchema} parameter) instead of a compile-time constant.
24+
*
25+
* <p>{@link #getDefaultSchema()} deliberately still reports {@code public}: its
26+
* callers use it to decide whether a name needs qualifying, and reporting the
27+
* session schema there would strip the schema off exactly the names that need
28+
* it most ({@code marts.dim_person} → {@code dim_person}).
1529
*/
1630
@Slf4j
1731
@Component
@@ -55,25 +69,27 @@ private List<DatabaseObject> getTablesAndViews(Connection connection) throws SQL
5569
JOIN pg_namespace n ON n.nspname = t.schemaname
5670
JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.tablename
5771
LEFT JOIN pg_stat_all_tables s ON s.relid = c.oid
58-
WHERE t.schemaname = 'public' AND c.relkind IN ('r', 'p')
72+
WHERE t.schemaname = current_schema() AND c.relkind IN ('r', 'p')
5973
UNION ALL
6074
SELECT v.viewname as name, 'view' as type, 0 as row_count
61-
FROM pg_views v WHERE v.schemaname = 'public'
75+
FROM pg_views v WHERE v.schemaname = current_schema()
6276
ORDER BY type, name
6377
""";
6478

79+
String schema = resolveSchema(connection);
80+
6581
try (Statement stmt = connection.createStatement();
6682
ResultSet rs = stmt.executeQuery(query)) {
6783
while (rs.next()) {
6884
DatabaseObject obj = new DatabaseObject();
6985
obj.setName(rs.getString("name"));
70-
obj.setSchema(DEFAULT_SCHEMA);
86+
obj.setSchema(schema);
7187
obj.setType(rs.getString("type"));
7288
Long estimatedRowCount = getNullableLong(rs, "row_count");
7389
obj.setRowCount("table".equals(obj.getType())
74-
? resolveTableRowCount(connection, DEFAULT_SCHEMA, obj.getName(), estimatedRowCount)
90+
? resolveTableRowCount(connection, schema, obj.getName(), estimatedRowCount)
7591
: estimatedRowCount);
76-
obj.setColumns(getTableColumns(connection, DEFAULT_SCHEMA, obj.getName()));
92+
obj.setColumns(getTableColumns(connection, schema, obj.getName()));
7793
objects.add(obj);
7894
}
7995
}
@@ -87,16 +103,18 @@ private List<DatabaseObject> getFunctions(Connection connection) throws SQLExcep
87103
SELECT p.proname as name, pg_get_functiondef(p.oid) as definition
88104
FROM pg_proc p
89105
JOIN pg_namespace n ON p.pronamespace = n.oid
90-
WHERE n.nspname = 'public' AND p.prokind = 'f'
106+
WHERE n.nspname = current_schema() AND p.prokind = 'f'
91107
ORDER BY p.proname
92108
""";
93109

110+
String schema = resolveSchema(connection);
111+
94112
try (Statement stmt = connection.createStatement();
95113
ResultSet rs = stmt.executeQuery(query)) {
96114
while (rs.next()) {
97115
DatabaseObject obj = new DatabaseObject();
98116
obj.setName(rs.getString("name"));
99-
obj.setSchema(DEFAULT_SCHEMA);
117+
obj.setSchema(schema);
100118
obj.setType("function");
101119
obj.setDefinition(rs.getString("definition"));
102120
objects.add(obj);
@@ -112,16 +130,18 @@ private List<DatabaseObject> getProcedures(Connection connection) throws SQLExce
112130
SELECT p.proname as name, pg_get_functiondef(p.oid) as definition
113131
FROM pg_proc p
114132
JOIN pg_namespace n ON p.pronamespace = n.oid
115-
WHERE n.nspname = 'public' AND p.prokind = 'p'
133+
WHERE n.nspname = current_schema() AND p.prokind = 'p'
116134
ORDER BY p.proname
117135
""";
118136

137+
String schema = resolveSchema(connection);
138+
119139
try (Statement stmt = connection.createStatement();
120140
ResultSet rs = stmt.executeQuery(query)) {
121141
while (rs.next()) {
122142
DatabaseObject obj = new DatabaseObject();
123143
obj.setName(rs.getString("name"));
124-
obj.setSchema(DEFAULT_SCHEMA);
144+
obj.setSchema(schema);
125145
obj.setType("procedure");
126146
obj.setDefinition(rs.getString("definition"));
127147
objects.add(obj);
@@ -151,7 +171,7 @@ LEFT JOIN (
151171
try (PreparedStatement stmt = connection.prepareStatement(query)) {
152172
stmt.setString(1, tableName);
153173
stmt.setString(2, tableName);
154-
stmt.setString(3, DEFAULT_SCHEMA);
174+
stmt.setString(3, resolveSchema(connection));
155175
try (ResultSet rs = stmt.executeQuery()) {
156176
while (rs.next()) {
157177
ColumnInfo col = new ColumnInfo();
@@ -257,11 +277,12 @@ WITH t AS (SELECT ?::regclass AS rel)
257277
stats.setIndexSize(rs.getLong("index_bytes"));
258278
stats.setSizeBytes(rs.getLong("total_bytes"));
259279
stats.setIndexSizeBytes(rs.getLong("index_bytes"));
280+
String schema = resolveSchema(connection);
260281
stats.setRowCount(resolveTableRowCount(
261282
connection,
262-
DEFAULT_SCHEMA,
283+
schema,
263284
tableName,
264-
getEstimatedTableRowCount(connection, DEFAULT_SCHEMA, tableName)
285+
getEstimatedTableRowCount(connection, schema, tableName)
265286
));
266287
}
267288
}
@@ -288,27 +309,28 @@ public SchemaMetadata scanSchema(Connection connection, String database) throws
288309
"JOIN pg_namespace n ON n.nspname = t.schemaname " +
289310
"JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.tablename " +
290311
"LEFT JOIN pg_stat_all_tables s ON s.relid = c.oid " +
291-
"WHERE t.schemaname = 'public' AND c.relkind IN ('r', 'p') " +
312+
"WHERE t.schemaname = current_schema() AND c.relkind IN ('r', 'p') " +
292313
"UNION ALL " +
293314
"SELECT v.viewname as tablename, 'view' as type, 0 as size_bytes, 0 as row_count " +
294315
"FROM pg_views v " +
295-
"WHERE v.schemaname = 'public' " +
316+
"WHERE v.schemaname = current_schema() " +
296317
"ORDER BY tablename";
297318

298319
Map<String, TableMetadata> tableMap = new HashMap<>();
320+
String schemaName = resolveSchema(connection);
299321

300322
try (Statement stmt = connection.createStatement()) {
301323
applyStatementSettings(stmt);
302324
try (ResultSet rs = stmt.executeQuery(tablesQuery)) {
303325
while (rs.next()) {
304326
TableMetadata table = new TableMetadata();
305327
table.setName(rs.getString("tablename"));
306-
table.setSchema(DEFAULT_SCHEMA);
328+
table.setSchema(schemaName);
307329
table.setType(rs.getString("type"));
308330
table.setSizeBytes(rs.getLong("size_bytes"));
309331
Long estimatedRowCount = getNullableLong(rs, "row_count");
310332
table.setRowCount("table".equals(table.getType())
311-
? resolveTableRowCount(connection, DEFAULT_SCHEMA, table.getName(), estimatedRowCount)
333+
? resolveTableRowCount(connection, schemaName, table.getName(), estimatedRowCount)
312334
: estimatedRowCount);
313335
schema.getTables().add(table);
314336
tableMap.put(table.getName(), table);
@@ -349,9 +371,9 @@ private void scanPostgreSQLColumnsBatch(Connection connection, Map<String, Table
349371
" FROM information_schema.table_constraints tc " +
350372
" JOIN information_schema.key_column_usage ku " +
351373
" ON tc.constraint_name = ku.constraint_name AND tc.table_name = ku.table_name " +
352-
" WHERE tc.table_schema = 'public' AND tc.constraint_type = 'PRIMARY KEY' " +
374+
" WHERE tc.table_schema = current_schema() AND tc.constraint_type = 'PRIMARY KEY' " +
353375
") pk ON c.table_name = pk.table_name AND c.column_name = pk.column_name " +
354-
"WHERE c.table_schema = 'public' " +
376+
"WHERE c.table_schema = current_schema() " +
355377
"ORDER BY c.table_name, c.ordinal_position";
356378

357379
try (Statement stmt = connection.createStatement()) {
@@ -386,10 +408,10 @@ private void scanPostgreSQLIndexesBatch(Connection connection, Map<String, Table
386408
"array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns, " +
387409
"ix.indisunique " +
388410
"FROM pg_indexes i " +
389-
"JOIN pg_class c ON c.relname = i.tablename AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') " +
390-
"JOIN pg_index ix ON ix.indexrelid = (SELECT oid FROM pg_class WHERE relname = i.indexname AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')) " +
411+
"JOIN pg_class c ON c.relname = i.tablename AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema()) " +
412+
"JOIN pg_index ix ON ix.indexrelid = (SELECT oid FROM pg_class WHERE relname = i.indexname AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) " +
391413
"JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(ix.indkey) " +
392-
"WHERE i.schemaname = 'public' " +
414+
"WHERE i.schemaname = current_schema() " +
393415
"GROUP BY i.tablename, i.indexname, i.indexdef, ix.indisunique " +
394416
"ORDER BY i.tablename, i.indexname";
395417

@@ -435,7 +457,7 @@ private void scanPostgreSQLForeignKeys(Connection connection, SchemaMetadata sch
435457
"ON tc.constraint_name = kcu.constraint_name " +
436458
"JOIN information_schema.constraint_column_usage AS ccu " +
437459
"ON ccu.constraint_name = tc.constraint_name " +
438-
"WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' " +
460+
"WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = current_schema() " +
439461
"ORDER BY tc.table_name, tc.constraint_name";
440462

441463
try (Statement stmt = connection.createStatement()) {
@@ -472,7 +494,7 @@ public List<RelationshipMetadata> getForeignKeys(Connection connection, String d
472494
JOIN information_schema.constraint_column_usage ccu
473495
ON tc.constraint_name = ccu.constraint_name
474496
WHERE tc.constraint_type = 'FOREIGN KEY'
475-
AND tc.table_schema = 'public'
497+
AND tc.table_schema = current_schema()
476498
ORDER BY tc.table_name, tc.constraint_name
477499
""";
478500

@@ -502,7 +524,7 @@ public List<ColumnDetail> getColumnDetails(Connection connection, String databas
502524
data_type, character_maximum_length, numeric_precision, numeric_scale,
503525
udt_name
504526
FROM information_schema.columns
505-
WHERE table_schema = 'public' AND table_name = ?
527+
WHERE table_schema = current_schema() AND table_name = ?
506528
ORDER BY ordinal_position
507529
""";
508530

@@ -547,7 +569,7 @@ public List<ConstraintDetail> getConstraintDetails(Connection connection, String
547569
LEFT JOIN information_schema.constraint_column_usage ccu
548570
ON tc.constraint_name = ccu.constraint_name
549571
AND tc.constraint_type = 'FOREIGN KEY'
550-
WHERE tc.table_schema = 'public' AND tc.table_name = ?
572+
WHERE tc.table_schema = current_schema() AND tc.table_name = ?
551573
ORDER BY tc.constraint_name
552574
""";
553575

@@ -582,11 +604,12 @@ public List<ConstraintDetail> getConstraintDetails(Connection connection, String
582604

583605
@Override
584606
public Long getTableRowCount(Connection connection, String database, String tableName) throws SQLException {
607+
String schema = resolveSchema(connection);
585608
return resolveTableRowCount(
586609
connection,
587-
DEFAULT_SCHEMA,
610+
schema,
588611
tableName,
589-
getEstimatedTableRowCount(connection, DEFAULT_SCHEMA, tableName)
612+
getEstimatedTableRowCount(connection, schema, tableName)
590613
);
591614
}
592615

@@ -763,6 +786,28 @@ private Long getExactTableRowCount(Connection connection, String schemaName, Str
763786
return null;
764787
}
765788

789+
/**
790+
* The schema this session's catalog queries resolve against — the first
791+
* existing entry in the search_path. Falls back to {@code public} so a
792+
* connection whose search_path names only missing schemas behaves exactly
793+
* as it did before, rather than tagging every object with a null schema.
794+
*/
795+
private String resolveSchema(Connection connection) {
796+
try (Statement stmt = connection.createStatement();
797+
ResultSet rs = stmt.executeQuery("SELECT current_schema()")) {
798+
if (rs.next()) {
799+
String schema = rs.getString(1);
800+
if (schema != null && !schema.isBlank()) {
801+
return schema;
802+
}
803+
}
804+
} catch (SQLException e) {
805+
log.debug("Could not resolve current_schema(), falling back to {}: {}",
806+
DEFAULT_SCHEMA, e.getMessage());
807+
}
808+
return DEFAULT_SCHEMA;
809+
}
810+
766811
private String quoteIdentifier(String identifier) {
767812
return "\"" + identifier.replace("\"", "\"\"") + "\"";
768813
}
@@ -802,7 +847,7 @@ public List<Map<String, Object>> getAllTablesWithMetadata(Connection connection,
802847
JOIN pg_namespace n ON n.nspname = t.schemaname
803848
JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.tablename
804849
LEFT JOIN pg_stat_all_tables s ON s.relid = c.oid
805-
WHERE t.schemaname = 'public' AND c.relkind IN ('r', 'p')
850+
WHERE t.schemaname = current_schema() AND c.relkind IN ('r', 'p')
806851
ORDER BY t.tablename
807852
""";
808853

0 commit comments

Comments
 (0)