check-db-naming.py (4470B)
1 #!/usr/bin/env python3 2 """Enforce the sync database layer naming conventions. 3 4 Adapted from the equivalent checker in the exchange repository 5 (contrib/check-db-naming.py) to the conventions used by sync's syncdb: 6 7 1. every implementation file syncdb_<X>.c defines a function named 8 SYNCDB_<X>, and a matching header <X>.h exists in the public 9 include directory declaring exactly that function 10 2. the stem <X> starts with one of the approved operation prefixes, 11 or the file is exempt (internal modules, standalone tools) 12 3. a .sql file (if any) belongs to the file it is named after 13 4. every prepared statement is unique across all implementation files 14 5. no numeric suffixes on stems 15 6. no repeated 'syncdb'/'SYNCDB' library prefix anywhere 16 17 Run from the top of the source tree; exits non-zero on any violation. 18 """ 19 import re 20 import sys 21 import pathlib 22 23 IMPL_DIR = pathlib.Path("src/syncdb") 24 HDR_DIR = pathlib.Path("src/include/sync/sync-database") 25 26 PREFIX = "SYNCDB_" 27 28 # Approved operation prefixes for the part of the file name after 29 # 'syncdb_' (e.g. syncdb_lookup_account_TR.c -> 'lookup_account_TR'). 30 PREFIXES = ( 31 "append_", "delete_", "update_", "lookup_", "store_", "increment_", 32 "create_", "drop_", "preflight_", 33 ) 34 35 # Exact stems (without the 'syncdb_' prefix) that are not operations. 36 EXACT = { 37 "gc", 38 } 39 40 # Files that are not part of the API surface at all. 41 SKIP = {"pg", "sync-dbinit"} 42 43 PREP = re.compile(r'PREPARE\s*\(\s*\w+\s*,\s*"([A-Za-z0-9_]+)"', re.S) 44 DECL = re.compile(r"^" + re.escape(PREFIX) + r"(\w+)\s*\(", re.M) 45 46 errors = [] 47 48 49 def check(): 50 if not IMPL_DIR.is_dir(): 51 return 52 stmt_owner = {} 53 54 for c in sorted(IMPL_DIR.glob("*.c")): 55 stem = c.stem 56 if stem in SKIP or stem.startswith("test_"): 57 continue 58 if stem.startswith("syncdb_"): 59 body = stem[len("syncdb_"):] 60 else: 61 body = stem 62 63 # 2. approved prefix, or exempt 64 if (not body.startswith(PREFIXES) 65 and body not in EXACT): 66 errors.append(f"{c}: '{body}' uses no approved prefix") 67 68 # 5. numeric suffix 69 if re.search(r"\d$", body): 70 errors.append(f"{c}: '{body}' ends in a digit") 71 72 h = HDR_DIR / (body + ".h") 73 74 # 1a. matching header 75 if not h.exists(): 76 errors.append(f"{c}: no matching header {h}") 77 else: 78 names = DECL.findall(h.read_text(errors="replace")) 79 # 1b. the header declares exactly the function of this file 80 if names != [body]: 81 errors.append( 82 f"{h}: declares {names}, expected exactly ['{body}']") 83 84 text = c.read_text(errors="replace") 85 86 # 1c. the file defines its function 87 if not re.search(r"^" + re.escape(PREFIX + body) + r"\s*\(", text, 88 re.M): 89 errors.append(f"{c}: does not define {PREFIX}{body}") 90 91 # 3. a .sql file shares the name 92 sql = c.with_suffix(".sql") 93 if sql.exists() and sql.stem != stem: 94 errors.append(f"{c}: .sql file {sql} does not share the name") 95 96 # 4. prepared statement uniqueness 97 for s in dict.fromkeys(PREP.findall(text)): 98 if s in stmt_owner and stmt_owner[s] != str(c): 99 errors.append( 100 f"{c}: prepared statement '{s}' also prepared in " 101 f"{stmt_owner[s]}") 102 stmt_owner[s] = str(c) 103 104 # Also check .sql files that are not covered by a .c file: they must 105 # either belong to a .c file or be one of the schema/fragment files. 106 covered = set() 107 for c in IMPL_DIR.glob("syncdb_*.c"): 108 covered.add(c.stem) 109 for s in IMPL_DIR.glob("syncdb_*.sql"): 110 if s.stem in covered: 111 continue 112 if s.stem not in ("procedures",): 113 errors.append( 114 f"{s}: .sql file without a matching .c and not 'procedures'") 115 116 # 6. repeated library prefix anywhere in the tree 117 for f in list(IMPL_DIR.glob("*.[ch]")) + list(HDR_DIR.glob("*.h")): 118 if "unc-backup" in f.name: 119 continue 120 if re.search(r"\bSYNCDB_syncdb\b|\bsyncdb_syncdb\b", f.read_text( 121 errors="replace")): 122 errors.append(f"{f}: repeated 'sync[d]b' prefix") 123 124 125 for e in errors: 126 print("ERROR:", e) 127 print(f"\ndb-naming: {len(errors)} violation(s)") 128 sys.exit(1 if errors else 0) 129 130 131 if __name__ == "__main__": 132 check()