Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>

<artifactId>database-audits-core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Database Audits Core</name>
Expand Down Expand Up @@ -39,7 +39,7 @@
<connection>scm:git:https://github.com/database-audits/core.git</connection>
<developerConnection>scm:git:https://github.com/database-audits/core.git</developerConnection>
<url>https://github.com/database-audits/core</url>
<tag>HEAD</tag>
<tag>v2.0.0</tag>
</scm>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,67 +45,8 @@ record ForeignKey(String tableName, String constraintName,
String referencedTable, List<String> columns) {
}

private static final String POSTGRESQL_FK_SQL =
"""
SELECT cl.relname AS table_name,
c.conname AS constraint_name,
ref.relname AS referenced_table,
a.attname AS column_name
FROM pg_constraint c
JOIN pg_class cl ON cl.oid = c.conrelid
JOIN pg_class ref ON ref.oid = c.confrelid
CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY AS k(attnum, ordinal)
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
WHERE c.contype = 'f'
AND c.connamespace = ?::regnamespace
ORDER BY 1, 2, k.ordinal
""";

/**
* key_column_usage carries the referenced table directly on MySQL/MariaDB.
*/
private static final String MYSQL_FK_SQL = """
SELECT k.table_name AS table_name,
k.constraint_name AS constraint_name,
k.referenced_table_name AS referenced_table,
k.column_name AS column_name
FROM information_schema.key_column_usage k
WHERE k.table_schema = ?
AND k.referenced_table_name IS NOT NULL
ORDER BY 1, 2, k.ordinal_position
""";

/**
* Standard information_schema; constraint names are unique per schema on
* H2, so the joins are exact.
*/
private static final String H2_FK_SQL = """
SELECT tc.table_name AS table_name,
tc.constraint_name AS constraint_name,
ref_tc.table_name AS referenced_table,
kcu.column_name AS column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_schema = tc.constraint_schema
AND kcu.constraint_name = tc.constraint_name
AND kcu.table_name = tc.table_name
JOIN information_schema.referential_constraints rc
ON rc.constraint_schema = tc.constraint_schema
AND rc.constraint_name = tc.constraint_name
LEFT JOIN information_schema.table_constraints ref_tc
ON ref_tc.constraint_schema = rc.unique_constraint_schema
AND ref_tc.constraint_name = rc.unique_constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = ?
ORDER BY 1, 2, kcu.ordinal_position
""";

String sql() {
return switch (platform) {
case POSTGRESQL -> POSTGRESQL_FK_SQL;
case MYSQL, MARIADB -> MYSQL_FK_SQL;
case H2 -> H2_FK_SQL;
};
return platform.catalogDialect().foreignKeysSql();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,8 @@ public class ForeignKeyNotNullAudit {
private final CatalogQueries catalogQueries;
private final DatabasePlatform platform;

/**
* Standard information_schema, valid as-is on PostgreSQL, MySQL, MariaDB,
* and H2. The join includes {@code table_name} because constraint names are
* only unique per table on PostgreSQL and MySQL.
*/
private static final String INFORMATION_SCHEMA_NULLABLE_FK_COLUMN_SQL = """
SELECT kcu.table_name AS table_name,
kcu.constraint_name AS constraint_name,
kcu.column_name AS column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_schema = tc.constraint_schema
AND kcu.constraint_name = tc.constraint_name
AND kcu.table_name = tc.table_name
JOIN information_schema.columns col
ON col.table_schema = kcu.table_schema
AND col.table_name = kcu.table_name
AND col.column_name = kcu.column_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = ?
AND col.is_nullable = 'YES'
ORDER BY 1, 2, 3
""";

String sql() {
return switch (platform) {
case POSTGRESQL, MYSQL, MARIADB, H2 ->
INFORMATION_SCHEMA_NULLABLE_FK_COLUMN_SQL;
};
return platform.catalogDialect().nullableForeignKeyColumnSql();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,107 +33,8 @@ public class ForeignKeyTypeMatchAudit {
private final CatalogQueries catalogQueries;
private final DatabasePlatform platform;

/**
* pg_catalog pairs each FK column with its referenced column positionally
* via {@code conkey}/{@code confkey}; {@code format_type} renders the full
* declared type (with modifiers, e.g. {@code character varying(10)}).
*/
private static final String POSTGRESQL_FK_COLUMN_TYPES_SQL =
"""
SELECT cl.relname AS table_name,
c.conname AS constraint_name,
a.attname AS column_name,
format_type(a.atttypid, a.atttypmod) AS column_type,
ref.relname AS referenced_table,
ra.attname AS referenced_column,
format_type(ra.atttypid, ra.atttypmod) AS referenced_type
FROM pg_constraint c
JOIN pg_class cl ON cl.oid = c.conrelid
JOIN pg_class ref ON ref.oid = c.confrelid
CROSS JOIN LATERAL unnest(c.conkey, c.confkey)
WITH ORDINALITY AS k(attnum, refattnum, ordinal)
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
JOIN pg_attribute ra ON ra.attrelid = c.confrelid AND ra.attnum = k.refattnum
WHERE c.contype = 'f'
AND c.connamespace = ?::regnamespace
ORDER BY 1, 2, k.ordinal
""";

/**
* key_column_usage carries the referenced table and column directly on
* MySQL/MariaDB; {@code column_type} is the full declared type including
* length and signedness (e.g. {@code varchar(10)},
* {@code bigint unsigned}).
*/
private static final String MYSQL_FK_COLUMN_TYPES_SQL = """
SELECT k.table_name AS table_name,
k.constraint_name AS constraint_name,
k.column_name AS column_name,
col.column_type AS column_type,
k.referenced_table_name AS referenced_table,
k.referenced_column_name AS referenced_column,
rcol.column_type AS referenced_type
FROM information_schema.key_column_usage k
JOIN information_schema.columns col
ON col.table_schema = k.table_schema
AND col.table_name = k.table_name
AND col.column_name = k.column_name
JOIN information_schema.columns rcol
ON rcol.table_schema = k.referenced_table_schema
AND rcol.table_name = k.referenced_table_name
AND rcol.column_name = k.referenced_column_name
WHERE k.table_schema = ?
AND k.referenced_table_name IS NOT NULL
ORDER BY 1, 2, k.ordinal_position
""";

/**
* Standard information_schema: {@code position_in_unique_constraint} maps
* each FK column to the referenced unique/PK constraint's column at that
* position. The declared type is composed from {@code data_type} plus the
* character length when present ({@code '(' || NULL || ')'} concatenates to
* NULL, so COALESCE drops it).
*/
private static final String H2_FK_COLUMN_TYPES_SQL =
"""
SELECT tc.table_name AS table_name,
tc.constraint_name AS constraint_name,
kcu.column_name AS column_name,
col.data_type || COALESCE('(' || col.character_maximum_length || ')', '') AS column_type,
ref_kcu.table_name AS referenced_table,
ref_kcu.column_name AS referenced_column,
rcol.data_type || COALESCE('(' || rcol.character_maximum_length || ')', '') AS referenced_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_schema = tc.constraint_schema
AND kcu.constraint_name = tc.constraint_name
AND kcu.table_name = tc.table_name
JOIN information_schema.referential_constraints rc
ON rc.constraint_schema = tc.constraint_schema
AND rc.constraint_name = tc.constraint_name
JOIN information_schema.key_column_usage ref_kcu
ON ref_kcu.constraint_schema = rc.unique_constraint_schema
AND ref_kcu.constraint_name = rc.unique_constraint_name
AND ref_kcu.ordinal_position = kcu.position_in_unique_constraint
JOIN information_schema.columns col
ON col.table_schema = kcu.table_schema
AND col.table_name = kcu.table_name
AND col.column_name = kcu.column_name
JOIN information_schema.columns rcol
ON rcol.table_schema = ref_kcu.table_schema
AND rcol.table_name = ref_kcu.table_name
AND rcol.column_name = ref_kcu.column_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = ?
ORDER BY 1, 2, kcu.ordinal_position
""";

String sql() {
return switch (platform) {
case POSTGRESQL -> POSTGRESQL_FK_COLUMN_TYPES_SQL;
case MYSQL, MARIADB -> MYSQL_FK_COLUMN_TYPES_SQL;
case H2 -> H2_FK_COLUMN_TYPES_SQL;
};
return platform.catalogDialect().foreignKeyColumnTypesSql();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,8 @@ public class PrimaryKeyPresenceAudit {
private final CatalogQueries catalogQueries;
private final DatabasePlatform platform;

/**
* Standard information_schema, valid as-is on PostgreSQL, MySQL, MariaDB,
* and H2.
*/
private static final String INFORMATION_SCHEMA_TABLES_WITHOUT_PK_SQL = """
SELECT t.table_name
FROM information_schema.tables t
WHERE t.table_schema = ?
AND t.table_type = 'BASE TABLE'
AND NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints tc
WHERE tc.table_schema = t.table_schema
AND tc.table_name = t.table_name
AND tc.constraint_type = 'PRIMARY KEY'
)
ORDER BY t.table_name
""";

String sql() {
return switch (platform) {
case POSTGRESQL, MYSQL, MARIADB, H2 ->
INFORMATION_SCHEMA_TABLES_WITHOUT_PK_SQL;
};
return platform.catalogDialect().tablesWithoutPrimaryKeySql();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private List<String> findingsOf(final TreeMap<String, String> violations) {
*/
protected final void collectChildFindings(final JsonNode node,
final List<String> findings, final Set<String> excludedRelations) {
final JsonNode planNodes = node.get("Plans");
final JsonNode planNodes = node.get(PlanJson.PLANS);
if (planNodes != null) {
for (final JsonNode planNode : planNodes) {
collectFindings(planNode, findings, excludedRelations);
Expand All @@ -205,11 +205,11 @@ protected final void collectChildFindings(final JsonNode node,
return null;
}
final String relation =
queryPlanExplainer.textOf(node, "Relation Name");
queryPlanExplainer.textOf(node, PlanJson.RELATION_NAME);
if (relation != null) {
return relation;
}
final JsonNode planNodes = node.get("Plans");
final JsonNode planNodes = node.get(PlanJson.PLANS);
if (planNodes != null) {
for (final JsonNode planNode : planNodes) {
final String found = firstRelationName(planNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ protected void collectFindings(final JsonNode node,

private void addSurvivingHashOrMergeJoin(final JsonNode node,
final List<String> findings, final Set<String> excludedRelations) {
final String type = queryPlanExplainer.textOf(node, "Node Type");
if ("Hash Join".equals(type) || "Merge Join".equals(type)) {
final String type = queryPlanExplainer.textOf(node, PlanJson.NODE_TYPE);
if (PlanJson.HASH_JOIN.equals(type)
|| PlanJson.MERGE_JOIN.equals(type)) {
final String relation = firstRelationName(innerChildOf(node));
addJoinFinding(type, relation, joinConditionOf(node, null),
findings, excludedRelations);
Expand All @@ -91,13 +92,13 @@ private void addSurvivingHashOrMergeJoin(final JsonNode node,

private void addNestedLoopWithInnerSeqScan(final JsonNode node,
final List<String> findings, final Set<String> excludedRelations) {
if ("Nested Loop"
.equals(queryPlanExplainer.textOf(node, "Node Type"))) {
if (PlanJson.NESTED_LOOP
.equals(queryPlanExplainer.textOf(node, PlanJson.NODE_TYPE))) {
final JsonNode innerScan = unwrapPassThrough(innerChildOf(node));
if (innerScan != null && "Seq Scan".equals(
queryPlanExplainer.textOf(innerScan, "Node Type"))) {
final String relation =
queryPlanExplainer.textOf(innerScan, "Relation Name");
if (innerScan != null && PlanJson.SEQ_SCAN.equals(
queryPlanExplainer.textOf(innerScan, PlanJson.NODE_TYPE))) {
final String relation = queryPlanExplainer.textOf(innerScan,
PlanJson.RELATION_NAME);
addJoinFinding("Nested Loop with inner Seq Scan", relation,
joinConditionOf(node, innerScan), findings,
excludedRelations);
Expand All @@ -119,22 +120,22 @@ private void addJoinFinding(final String description, final String relation,
private String joinConditionOf(final JsonNode joinNode,
final JsonNode innerScan) {
final String hashCond =
queryPlanExplainer.textOf(joinNode, "Hash Cond");
queryPlanExplainer.textOf(joinNode, PlanJson.HASH_COND);
if (hashCond != null) {
return hashCond;
}
final String mergeCond =
queryPlanExplainer.textOf(joinNode, "Merge Cond");
queryPlanExplainer.textOf(joinNode, PlanJson.MERGE_COND);
if (mergeCond != null) {
return mergeCond;
}
final String joinFilter =
queryPlanExplainer.textOf(joinNode, "Join Filter");
queryPlanExplainer.textOf(joinNode, PlanJson.JOIN_FILTER);
if (joinFilter != null) {
return joinFilter;
}
final String innerFilter = innerScan == null ? null
: queryPlanExplainer.textOf(innerScan, "Filter");
: queryPlanExplainer.textOf(innerScan, PlanJson.FILTER);
return innerFilter == null ? "(join condition not shown)" : innerFilter;
}

Expand All @@ -143,13 +144,13 @@ private String joinConditionOf(final JsonNode joinNode,
* have to serve.
*/
private JsonNode innerChildOf(final JsonNode node) {
final JsonNode planNodes = node.get("Plans");
final JsonNode planNodes = node.get(PlanJson.PLANS);
if (planNodes == null) {
return null;
}
for (final JsonNode planNode : planNodes) {
if ("Inner".equals(queryPlanExplainer.textOf(planNode,
"Parent Relationship"))) {
if (PlanJson.INNER.equals(queryPlanExplainer.textOf(planNode,
PlanJson.PARENT_RELATIONSHIP))) {
return planNode;
}
}
Expand All @@ -163,8 +164,8 @@ private JsonNode innerChildOf(final JsonNode node) {
private JsonNode unwrapPassThrough(final JsonNode node) {
JsonNode current = node;
while (current != null && isPassThrough(
queryPlanExplainer.textOf(current, "Node Type"))) {
final JsonNode planNodes = current.get("Plans");
queryPlanExplainer.textOf(current, PlanJson.NODE_TYPE))) {
final JsonNode planNodes = current.get(PlanJson.PLANS);
current =
planNodes != null && !planNodes.isEmpty() ? planNodes.get(0)
: null;
Expand All @@ -173,9 +174,10 @@ private JsonNode unwrapPassThrough(final JsonNode node) {
}

private boolean isPassThrough(final String nodeType) {
return "Hash".equals(nodeType) || "Sort".equals(nodeType)
|| "Incremental Sort".equals(nodeType)
|| "Materialize".equals(nodeType) || "Memoize".equals(nodeType);
return PlanJson.HASH.equals(nodeType) || PlanJson.SORT.equals(nodeType)
|| PlanJson.INCREMENTAL_SORT.equals(nodeType)
|| PlanJson.MATERIALIZE.equals(nodeType)
|| PlanJson.MEMOIZE.equals(nodeType);
}

@Override
Expand Down
Loading