fix(schema-diff): require explicit expression index provenance
This commit is contained in:
1 parent
01ea889569
commit
4660a11f25
28 files changed
+673
-39
No files matched your search
@@ -13,13 +13,18 @@ public final class IndexInfo {
|
||||
private String index_type;
|
||||
private List<String> included_columns;
|
||||
private String comment;
|
||||
// Parallel to `columns`: true at index i means columns[i] is a raw expression (e.g. sourced
|
||||
// from pg_get_indexdef), not a plain column name. Empty when the introspection source
|
||||
// doesn't track this (provenance unknown for that dialect/path). Mirrors
|
||||
// crate::types::IndexInfo::key_is_expression on the Rust side (#6312 review).
|
||||
private List<Boolean> key_is_expression;
|
||||
|
||||
public IndexInfo() {
|
||||
this("", Collections.emptyList(), false, false);
|
||||
}
|
||||
|
||||
public IndexInfo(String name, List<String> columns, boolean is_unique, boolean is_primary) {
|
||||
this(name, columns, is_unique, is_primary, null, null, null, null);
|
||||
this(name, columns, is_unique, is_primary, null, null, null, null, Collections.emptyList());
|
||||
}
|
||||
|
||||
public IndexInfo(
|
||||
@@ -31,6 +36,20 @@ public final class IndexInfo {
|
||||
String index_type,
|
||||
List<String> included_columns,
|
||||
String comment
|
||||
) {
|
||||
this(name, columns, is_unique, is_primary, filter, index_type, included_columns, comment, Collections.emptyList());
|
||||
}
|
||||
|
||||
public IndexInfo(
|
||||
String name,
|
||||
List<String> columns,
|
||||
boolean is_unique,
|
||||
boolean is_primary,
|
||||
String filter,
|
||||
String index_type,
|
||||
List<String> included_columns,
|
||||
String comment,
|
||||
List<Boolean> key_is_expression
|
||||
) {
|
||||
this.name = name;
|
||||
this.columns = columns == null ? Collections.emptyList() : columns;
|
||||
@@ -40,6 +59,7 @@ public final class IndexInfo {
|
||||
this.index_type = index_type;
|
||||
this.included_columns = included_columns;
|
||||
this.comment = comment;
|
||||
this.key_is_expression = key_is_expression == null ? Collections.emptyList() : key_is_expression;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -74,6 +94,10 @@ public final class IndexInfo {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public List<Boolean> getKey_is_expression() {
|
||||
return key_is_expression;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
@@ -106,6 +130,10 @@ public final class IndexInfo {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public void setKey_is_expression(List<Boolean> key_is_expression) {
|
||||
this.key_is_expression = key_is_expression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) return true;
|
||||
@@ -118,12 +146,15 @@ public final class IndexInfo {
|
||||
&& Objects.equals(filter, that.filter)
|
||||
&& Objects.equals(index_type, that.index_type)
|
||||
&& Objects.equals(included_columns, that.included_columns)
|
||||
&& Objects.equals(comment, that.comment);
|
||||
&& Objects.equals(comment, that.comment)
|
||||
&& Objects.equals(key_is_expression, that.key_is_expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, columns, is_unique, is_primary, filter, index_type, included_columns, comment);
|
||||
return Objects.hash(
|
||||
name, columns, is_unique, is_primary, filter, index_type, included_columns, comment, key_is_expression
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,6 +167,7 @@ public final class IndexInfo {
|
||||
+ ", index_type=" + index_type
|
||||
+ ", included_columns=" + included_columns
|
||||
+ ", comment=" + comment
|
||||
+ ", key_is_expression=" + key_is_expression
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
@@ -533,47 +533,87 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent {
|
||||
return listIndexesFromCatalogArrays(schema, table, attributeCache);
|
||||
}
|
||||
return unchecked(() -> {
|
||||
List<IndexInfo> result = new ArrayList<>();
|
||||
// One row per index key position rather than array_agg/GROUP BY: avoids
|
||||
// rs.getArray(...), whose JDBC support is flaky enough across these vendor drivers
|
||||
// that readAttributeNumbers() below already has to defensively catch
|
||||
// SQLException/UnsupportedOperationException/IncompatibleClassChangeError around it.
|
||||
// LEFT JOIN pg_attribute (instead of an inner JOIN) so expression key parts
|
||||
// (attnum = 0, e.g. functional index columns) survive instead of being silently
|
||||
// dropped; their text comes from pg_get_indexdef instead of a.attname, and
|
||||
// is_expression records which case applied so Rust never has to guess from
|
||||
// characters in the text (#6312 review).
|
||||
Map<String, IndexBuilder> byName = new LinkedHashMap<>();
|
||||
String sql = "SELECT i.relname AS index_name, am.amname AS index_type, " +
|
||||
"ix.indisunique AS is_unique, ix.indisprimary AS is_primary, " +
|
||||
"array_agg(a.attname ORDER BY k.n) AS columns " +
|
||||
"COALESCE(a.attname, " + profile.catalogPrefixedFunction("get_indexdef") + "(ix.indexrelid, k.n, true)) AS column_text, " +
|
||||
"(a.attname IS NULL) AS is_expression " +
|
||||
"FROM " + profile.catalogRelation("index") + " ix " +
|
||||
"JOIN " + profile.catalogRelation("class") + " t ON t.oid = ix.indrelid " +
|
||||
"JOIN " + profile.catalogRelation("class") + " i ON i.oid = ix.indexrelid " +
|
||||
"JOIN " + profile.catalogRelation("namespace") + " n ON n.oid = t.relnamespace " +
|
||||
"JOIN " + profile.catalogRelation("am") + " am ON am.oid = i.relam " +
|
||||
"JOIN LATERAL (SELECT unnest(ix.indkey) AS attnum, generate_series(1, array_length(ix.indkey, 1)) AS n) AS k ON true " +
|
||||
"JOIN " + profile.catalogRelation("attribute") + " a ON a.attrelid = t.oid AND a.attnum = k.attnum " +
|
||||
"LEFT JOIN " + profile.catalogRelation("attribute") + " a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0 " +
|
||||
"WHERE n.nspname = ? AND t.relname = ? " +
|
||||
"GROUP BY i.relname, am.amname, ix.indisunique, ix.indisprimary " +
|
||||
"ORDER BY i.relname";
|
||||
"ORDER BY i.relname, k.n";
|
||||
try (java.sql.PreparedStatement stmt = requireConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, schema);
|
||||
stmt.setString(2, table);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
Object[] columnArray = (Object[]) rs.getArray("columns").getArray();
|
||||
List<String> columns = new ArrayList<>();
|
||||
for (Object column : columnArray) {
|
||||
columns.add(String.valueOf(column));
|
||||
}
|
||||
result.add(new IndexInfo(
|
||||
rs.getString("index_name"),
|
||||
columns,
|
||||
rs.getBoolean("is_unique"),
|
||||
rs.getBoolean("is_primary"),
|
||||
null,
|
||||
rs.getString("index_type"),
|
||||
null,
|
||||
null
|
||||
));
|
||||
String indexName = rs.getString("index_name");
|
||||
String indexType = rs.getString("index_type");
|
||||
boolean isUnique = rs.getBoolean("is_unique");
|
||||
boolean isPrimary = rs.getBoolean("is_primary");
|
||||
IndexBuilder builder = byName.computeIfAbsent(
|
||||
indexName,
|
||||
name -> new IndexBuilder(name, indexType, isUnique, isPrimary)
|
||||
);
|
||||
builder.columns.add(rs.getString("column_text"));
|
||||
builder.keyIsExpression.add(rs.getBoolean("is_expression"));
|
||||
}
|
||||
}
|
||||
}
|
||||
List<IndexInfo> result = new ArrayList<>();
|
||||
for (IndexBuilder builder : byName.values()) {
|
||||
result.add(new IndexInfo(
|
||||
builder.name,
|
||||
builder.columns,
|
||||
builder.isUnique,
|
||||
builder.isPrimary,
|
||||
null,
|
||||
builder.indexType,
|
||||
null,
|
||||
null,
|
||||
builder.keyIsExpression
|
||||
));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private static final class IndexBuilder {
|
||||
private final String name;
|
||||
private final String indexType;
|
||||
private final boolean isUnique;
|
||||
private final boolean isPrimary;
|
||||
private final List<String> columns = new ArrayList<>();
|
||||
private final List<Boolean> keyIsExpression = new ArrayList<>();
|
||||
|
||||
private IndexBuilder(String name, String indexType, boolean isUnique, boolean isPrimary) {
|
||||
this.name = name;
|
||||
this.indexType = indexType;
|
||||
this.isUnique = isUnique;
|
||||
this.isPrimary = isPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
// No PostgresLikeAgentProfile currently calls withCatalogAttributeArraysMappedInJava(), so this
|
||||
// path is unreachable by any driver today (Highgo and the rest use listIndexes(...) above). It
|
||||
// still filters out expression key parts (attnum <= 0) via mapRequiredAttributeNumbers and
|
||||
// drops the whole index when that happens, and reports no key_is_expression provenance for the
|
||||
// indexes it does return. Left as-is rather than expanding this fix onto dead code; a future
|
||||
// profile enabling this path would need the same treatment as listIndexes(...) above.
|
||||
private List<IndexInfo> listIndexesFromCatalogArrays(
|
||||
String schema,
|
||||
String table,
|
||||
|
||||
Reference in new issue
Block a user