sync

Backup service to store encrypted wallet databases (experimental)
Log | Files | Refs | Submodules | README | LICENSE

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()