check-db-naming.py (5322B)
1 #!/usr/bin/env python3 2 """Enforce the database layer naming convention. 3 4 Rules checked (see the commit series "database naming cleanup"): 5 6 1. every public function's name starts with the name of the file it lives in 7 2. the file's own name uses a prefix from the closed set, or is exempt 8 3. iterate_ iff the function takes a caller-supplied callback; get_ iff not 9 4. a .h exists beside every .c, and a .sql (if any) shares the name 10 5. every prepared statement is named after the function preparing it 11 6. no prepared statement name is used in two files 12 7. no numeric suffixes and no repeated library prefix 13 14 Run from the top of the source tree; exits non-zero on any violation. 15 """ 16 import re 17 import sys 18 import pathlib 19 import collections 20 21 LAYERS = [ 22 ("src/exchangedb", "src/include/exchange-database", "TALER_EXCHANGEDB_"), 23 ("src/auditordb", "src/include/auditor-database", "TALER_AUDITORDB_"), 24 ] 25 26 # Prefixes that describe what the caller gets. 27 PREFIXES = ("get_", "get_count_", "get_exists_", "iterate_", 28 "insert_", "update_", "update_to_", "delete_", "do_") 29 30 # Not CRUD: transaction control, DDL, events, memory, session plumbing, 31 # cancellable handle APIs, shard lifecycle, and multi-function helper modules. 32 EXEMPT = { 33 "start", "start_read_only", "start_read_committed", 34 "start_deferred_wire_out", "commit", "rollback", "preflight", "disconnect", 35 "create_tables", "drop_tables", "inject_auditor_triggers", "gc", 36 "event_listen", "event_listen_cancel", "event_notify", 37 "free_coin_transaction_list", "free_reserve_history", 38 "compute_shard", "enable_rules", "disable_rules", "begin_rule_update", 39 "begin_shard", "begin_revolving_shard", "complete_shard", "abort_shard", 40 "release_revolving_shard", 41 "account_history", "exchangedb_accounts", "exchangedb_transactions", 42 } 43 44 # Transaction functions intentionally share this implementation file. 45 STATEMENT_OWNERS = { 46 "src/auditordb/start.c": ("start", "commit", "rollback"), 47 } 48 49 # Files that are not part of the API surface at all. 50 SKIP = {"pg", "template", "helper", "bench_db"} 51 52 PREP = re.compile(r'PREPARE\s*\(\s*\w+\s*,\s*"([A-Za-z0-9_]+)"', re.S) 53 54 errors = [] 55 56 57 def check_layer(impl_dir, hdr_dir, prefix): 58 impl, hdr = pathlib.Path(impl_dir), pathlib.Path(hdr_dir) 59 if not impl.is_dir(): 60 return 61 decl = re.compile(r"^" + re.escape(prefix) + r"(\w+)\s*\((.*?)\)\s*;", 62 re.S | re.M) 63 stmt_owner = {} 64 65 for c in sorted(impl.glob("*.c")): 66 stem = c.stem 67 if stem in SKIP or stem.startswith("test_"): 68 continue 69 70 # 2. prefix from the closed set, or exempt 71 if stem not in EXEMPT and not stem.startswith(PREFIXES): 72 errors.append(f"{c}: '{stem}' uses no approved prefix") 73 74 # 7. numeric suffix 75 if re.search(r"\d$", stem): 76 errors.append(f"{c}: '{stem}' ends in a digit") 77 78 # 4. matching header 79 h = hdr / (stem + ".h") 80 if stem not in EXEMPT and not h.exists(): 81 errors.append(f"{c}: no matching header {h}") 82 83 text = c.read_text(errors="replace") 84 85 # 5. prepared statements named after the function 86 owners = STATEMENT_OWNERS.get(c.as_posix(), (stem,)) 87 for s in dict.fromkeys(PREP.findall(text)): 88 if not any(s == owner or s.startswith(owner + "_") 89 for owner in owners): 90 errors.append( 91 f"{c}: prepared statement '{s}' is not named after '{stem}'") 92 # 6. cross-file uniqueness 93 if s in stmt_owner and stmt_owner[s] != str(c): 94 errors.append( 95 f"{c}: prepared statement '{s}' also prepared in " 96 f"{stmt_owner[s]}") 97 stmt_owner[s] = str(c) 98 99 if not h.exists(): 100 continue 101 htext = re.sub(r"/\*.*?\*/", "", h.read_text(errors="replace"), 102 flags=re.S) 103 for name, params in decl.findall(htext): 104 # 1. declared names belong to their file. Exempt entries are the 105 # documented multi-function modules, so this does not apply there. 106 if (stem not in EXEMPT 107 and name != stem and not name.startswith(stem + "_")): 108 errors.append( 109 f"{h}: declares '{name}', which does not belong to " 110 f"'{stem}'") 111 # 3. iterate_ iff callback 112 has_cb = bool(re.search(r"\w*(Callback|Iterator)\s+\w+", params)) 113 if name.startswith("iterate_") and not has_cb: 114 errors.append(f"{h}: '{name}' is iterate_ but takes no callback") 115 if name.startswith("get_") and has_cb: 116 errors.append(f"{h}: '{name}' takes a callback; use iterate_") 117 118 # 7. repeated library prefix anywhere in the tree 119 for f in list(impl.glob("*.[ch]")) + list(hdr.glob("*.h")): 120 if "unc-backup" in f.name: 121 continue 122 if re.search(r"\b" + re.escape(prefix.split("_")[0]) + r"_" + 123 re.escape(prefix), f.read_text(errors="replace")): 124 errors.append(f"{f}: repeated '{prefix}' prefix") 125 126 127 for a in LAYERS: 128 check_layer(*a) 129 130 for e in errors: 131 print("ERROR:", e) 132 print(f"\ndb-naming: {len(errors)} violation(s)") 133 sys.exit(1 if errors else 0)