Skip to content

Commit 217974c

Browse files
author
mforce
committed
fix(#417): codex round 4 — exact type comparison, assembly-leak detector, namespaced untagged refs
- Column type cells now compare exactly against format_type with tbls's single observed normalization (character varying → varchar; every other type cell across all 38 tables is format_type verbatim). A stable rendering regression — varchar(120) shown as text — previously survived every check including the byte-diff; a future type the mapping doesn't cover fails loudly and gets added consciously. - The portability guard rejects assembly file names (.dll/.pdb) — the same stable-leak class MigrationSecurityReviewTests fences on the migration digest, invisible to a byte-diff by definition. - The untagged-image detector accepts registry/namespace prefixes (docker.io/library/postgres floats to latest just like the bare name). Mutation-verified, each by name: varchar(120)→text on Accounts.Name, an injected Cluckwork.Api.dll mention, and a namespaced untagged compose image.
1 parent 4947936 commit 217974c

1 file changed

Lines changed: 34 additions & 18 deletions

File tree

tests/Cluckwork.Api.IntegrationTests/SchemaDocsTests.cs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ public void PostgresImagePin_IsOneIdenticalStringAcrossEveryTrackedFile()
8989
// no colon for the pattern above to see, so it needs its own
9090
// detector, scoped to the two syntaxes where a bare name is a live
9191
// image reference rather than prose.
92-
var untaggedPattern = new Regex(@"(?im)^\s*(?:image:\s*|FROM\s+)[""']?postgres[""']?(?=\s|$)");
92+
// Namespaced/registry-qualified forms (docker.io/library/postgres,
93+
// registry:5000/ns/postgres) float to latest exactly the same way,
94+
// so the optional prefix segments are part of the detector.
95+
var untaggedPattern = new Regex(@"(?im)^\s*(?:image:\s*|FROM\s+)[""']?(?:[a-z0-9.-]+(?::\d+)?/)*postgres[""']?(?=\s|$)");
9396
var hits = new Dictionary<string, List<string>>();
9497

9598
foreach (var relative in TrackedFiles())
@@ -145,6 +148,10 @@ public void CommittedSchemaDocs_CarryNoEnvironmentSpecificContent()
145148
("absolute windows path", new Regex(@"(?i)[a-z]:\\")),
146149
("timestamp", new Regex(@"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}")),
147150
("connection URI (would carry the ephemeral password)", new Regex(@"postgres(?:ql)?://")),
151+
// Same portability class MigrationSecurityReviewTests fences on
152+
// the migration digest: an assembly file name is stable across
153+
// machines, so the byte-diff can't catch it either.
154+
("assembly artifact", new Regex(@"[\w.-]+\.(?:dll|pdb)\b", RegexOptions.IgnoreCase)),
148155
};
149156

150157
var failures = new StringBuilder();
@@ -217,11 +224,13 @@ void RequireRow(string table, string name, string def, string kind)
217224
// so a page-wide cell search would accept an omitted column that
218225
// happens to be named like a header (Customers.Name). Only the data
219226
// rows of the Columns table count — and the row's METADATA cells are
220-
// held to the catalog too: default and nullability are exact (tbls
221-
// prints pg_get_expr / true|false verbatim), the type cell must at
222-
// least be non-empty (tbls normalizes type names — e.g. varchar(16)
223-
// for character varying — and mirroring that normalizer here would be
224-
// a parity chase; the byte-diff pins the exact text instead).
227+
// held to the catalog exactly: type, default, and nullability. The
228+
// type comparison uses format_type with tbls's single observed
229+
// normalization (character varying → varchar; every other type cell
230+
// across all 38 tables is format_type verbatim) — a future type this
231+
// mapping doesn't cover fails loudly and gets added consciously,
232+
// which beats both silently trusting the cell and mirroring tbls's
233+
// whole normalizer.
225234
foreach (var col in await QueryColumnsAsync(conn))
226235
{
227236
var lines = LinesOf(col.Table);
@@ -243,8 +252,9 @@ void RequireRow(string table, string name, string def, string kind)
243252
missing.AppendLine($"column absent from the Columns section of public.{col.Table}.md: {col.Column}");
244253
continue;
245254
}
246-
if (row[2].Length == 0)
247-
missing.AppendLine($"column type cell empty in public.{col.Table}.md: {col.Column}");
255+
var expectedType = col.Type.Replace("character varying", "varchar");
256+
if (row[2] != expectedType)
257+
missing.AppendLine($"column type mismatch in public.{col.Table}.md: {col.Column} — docs \"{row[2]}\", catalog \"{expectedType}\"");
248258
if (row[3] != col.Default)
249259
missing.AppendLine($"column default mismatch in public.{col.Table}.md: {col.Column} — docs \"{row[3]}\", catalog \"{col.Default}\"");
250260
if (row[4] != col.Nullable)
@@ -307,24 +317,30 @@ private static async Task<List<string>> QueryStringsAsync(
307317
return results;
308318
}
309319

310-
private static async Task<List<(string Table, string Column, string Default, string Nullable)>>
320+
private static async Task<List<(string Table, string Column, string Type, string Default, string Nullable)>>
311321
QueryColumnsAsync(System.Data.Common.DbConnection conn)
312322
{
313-
var results = new List<(string, string, string, string)>();
323+
var results = new List<(string, string, string, string, string)>();
314324
await using var cmd = conn.CreateCommand();
315-
// column_default and is_nullable are rendered by tbls verbatim
316-
// (pg_get_expr text; "true"/"false") — comparable exactly.
325+
// Defaults and nullability are rendered by tbls verbatim
326+
// (pg_get_expr text; "true"/"false"); types via format_type.
317327
cmd.CommandText =
318328
"""
319-
SELECT table_name, column_name, COALESCE(column_default, ''),
320-
CASE WHEN is_nullable = 'YES' THEN 'true' ELSE 'false' END
321-
FROM information_schema.columns
322-
WHERE table_schema = 'public'
323-
ORDER BY table_name, ordinal_position
329+
SELECT c.relname, a.attname, format_type(a.atttypid, a.atttypmod),
330+
COALESCE(pg_get_expr(d.adbin, d.adrelid), ''),
331+
CASE WHEN a.attnotnull THEN 'false' ELSE 'true' END
332+
FROM pg_attribute a
333+
JOIN pg_class c ON c.oid = a.attrelid
334+
JOIN pg_namespace n ON n.oid = c.relnamespace
335+
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
336+
WHERE n.nspname = 'public' AND c.relkind = 'r'
337+
AND a.attnum > 0 AND NOT a.attisdropped
338+
ORDER BY c.relname, a.attnum
324339
""";
325340
await using var reader = await cmd.ExecuteReaderAsync();
326341
while (await reader.ReadAsync())
327-
results.Add((reader.GetString(0), reader.GetString(1), reader.GetString(2), reader.GetString(3)));
342+
results.Add((reader.GetString(0), reader.GetString(1), reader.GetString(2),
343+
reader.GetString(3), reader.GetString(4)));
328344
return results;
329345
}
330346

0 commit comments

Comments
 (0)