From 524b193e9e95f96c24290f37db590b330c077eee Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Tue, 17 Mar 2026 16:19:24 +0000
Subject: [PATCH 1/4] Integrate pg-tidy clang-tidy plugin into the PostgreSQL
 meson build

Add pg-tidy, a clang-tidy plugin with PostgreSQL-specific checks, to the
meson build system as an optional component (off by default).

The pg_tidy option requires LLVM/clang development libraries and
clang-tidy to be available.  When enabled, it builds the pg_tidy shared
module and runs clang-tidy with only pg-specific checks on all source
files as part of the default build.

The LLVM detection is extended so that pg_tidy=enabled triggers LLVM
detection even when llvm=disabled (for JIT).  A new llvm_jit_enabled
variable gates the JIT module separately from the LLVM dependency.
---
 meson.build                                   |  45 ++++++-
 meson_options.txt                             |   3 +
 src/backend/jit/llvm/meson.build              |   2 +-
 src/meson.build                               |   2 +
 src/tools/pg-tidy/include/pg_tidy.h           |  12 ++
 src/tools/pg-tidy/meson.build                 |  69 ++++++++++
 src/tools/pg-tidy/run_pg_tidy_file.py         |  35 ++++++
 src/tools/pg-tidy/src/PgNoPaddingCheck.cpp    | 108 ++++++++++++++++
 src/tools/pg-tidy/src/PgNoPaddingCheck.h      |  21 ++++
 .../pg-tidy/src/PgNoPaddingInitCheck.cpp      |  44 +++++++
 src/tools/pg-tidy/src/PgNoPaddingInitCheck.h  |  18 +++
 .../pg-tidy/src/PgRequiresNoPaddingCheck.cpp  |  89 +++++++++++++
 .../pg-tidy/src/PgRequiresNoPaddingCheck.h    |  18 +++
 src/tools/pg-tidy/src/PgTidyModule.cpp        |  22 ++++
 src/tools/pg-tidy/test/lit.cfg.py             |   0
 src/tools/pg-tidy/test/lit.site.cfg.py.in     |  15 +++
 src/tools/pg-tidy/test/pg-no-padding-init.c   |  28 +++++
 src/tools/pg-tidy/test/pg-no-padding.c        | 118 ++++++++++++++++++
 .../pg-tidy/test/pg-requires-no-padding.c     |  54 ++++++++
 19 files changed, 697 insertions(+), 6 deletions(-)
 create mode 100644 src/tools/pg-tidy/include/pg_tidy.h
 create mode 100644 src/tools/pg-tidy/meson.build
 create mode 100644 src/tools/pg-tidy/run_pg_tidy_file.py
 create mode 100644 src/tools/pg-tidy/src/PgNoPaddingCheck.cpp
 create mode 100644 src/tools/pg-tidy/src/PgNoPaddingCheck.h
 create mode 100644 src/tools/pg-tidy/src/PgNoPaddingInitCheck.cpp
 create mode 100644 src/tools/pg-tidy/src/PgNoPaddingInitCheck.h
 create mode 100644 src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.cpp
 create mode 100644 src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.h
 create mode 100644 src/tools/pg-tidy/src/PgTidyModule.cpp
 create mode 100644 src/tools/pg-tidy/test/lit.cfg.py
 create mode 100644 src/tools/pg-tidy/test/lit.site.cfg.py.in
 create mode 100644 src/tools/pg-tidy/test/pg-no-padding-init.c
 create mode 100644 src/tools/pg-tidy/test/pg-no-padding.c
 create mode 100644 src/tools/pg-tidy/test/pg-requires-no-padding.c

diff --git a/meson.build b/meson.build
index 5122706477d..b0d24e980af 100644
--- a/meson.build
+++ b/meson.build
@@ -926,13 +926,25 @@ endif
 ###############################################################
 
 llvmopt = get_option('llvm')
+pg_tidy_opt = get_option('pg_tidy')
 llvm = not_found_dep
+llvm_binpath = ''
+pg_tidy_enabled = false
 if have_cxx
-  llvm = dependency('llvm', version: '>=14', method: 'config-tool', required: llvmopt)
+  # Detect LLVM if needed for JIT or pg_tidy
+  llvm_required = llvmopt.enabled() or pg_tidy_opt.enabled()
+  llvm_needed = not llvmopt.disabled() or not pg_tidy_opt.disabled()
+  if llvm_needed
+    llvm = dependency('llvm', version: '>=14', method: 'config-tool',
+                      required: llvm_required)
+  endif
 
   if llvm.found()
 
-    cdata.set('USE_LLVM', 1)
+    llvm_jit_enabled = not llvmopt.disabled()
+    if llvm_jit_enabled
+      cdata.set('USE_LLVM', 1)
+    endif
 
     llvm_binpath = llvm.get_variable(configtool: 'bindir')
 
@@ -941,17 +953,40 @@ if have_cxx
     # Some distros put LLVM and clang in different paths, so fallback to
     # find via PATH, too.
     clang = find_program(llvm_binpath / 'clang', 'clang', required: true)
+  else
+    llvm_jit_enabled = false
   endif
 else
-  msg = 'llvm requires a C++ compiler'
-  if llvmopt.auto()
+  llvm_jit_enabled = false
+  msg = 'llvm/pg_tidy requires a C++ compiler'
+  if llvmopt.auto() and pg_tidy_opt.auto()
     message(msg)
-  elif llvmopt.enabled()
+  elif llvmopt.enabled() or pg_tidy_opt.enabled()
     error(msg)
   endif
 endif
 
 
+###############################################################
+# Tool: pg-tidy (clang-tidy checks for PostgreSQL)
+###############################################################
+
+clang_tidy_prog = disabler()
+clang_cpp_dep = not_found_dep
+if not pg_tidy_opt.disabled() and llvm.found() and have_cxx
+  clang_tidy_prog = find_program(llvm_binpath / 'clang-tidy', 'clang-tidy',
+                                 required: pg_tidy_opt)
+  if clang_tidy_prog.found()
+    clang_cpp_dep = cxx.find_library('clang-cpp', required: pg_tidy_opt)
+    if clang_cpp_dep.found()
+      pg_tidy_enabled = true
+    endif
+  endif
+elif pg_tidy_opt.enabled()
+  error('pg_tidy requires LLVM and a C++ compiler')
+endif
+
+
 
 ###############################################################
 # Library: icu
diff --git a/meson_options.txt b/meson_options.txt
index 6a793f3e479..f8f9b5f4835 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -121,6 +121,9 @@ option('libxslt', type: 'feature', value: 'auto',
 option('llvm', type: 'feature', value: 'disabled',
   description: 'LLVM support')
 
+option('pg_tidy', type: 'feature', value: 'disabled',
+  description: 'pg-tidy clang-tidy checks for PostgreSQL')
+
 option('lz4', type: 'feature', value: 'auto',
   description: 'LZ4 support')
 
diff --git a/src/backend/jit/llvm/meson.build b/src/backend/jit/llvm/meson.build
index 7df8453ad6f..1f6025c4d37 100644
--- a/src/backend/jit/llvm/meson.build
+++ b/src/backend/jit/llvm/meson.build
@@ -1,6 +1,6 @@
 # Copyright (c) 2022-2026, PostgreSQL Global Development Group
 
-if not llvm.found()
+if not llvm_jit_enabled
   subdir_done()
 endif
 
diff --git a/src/meson.build b/src/meson.build
index b57d4a5c259..47d72b0252c 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -15,6 +15,8 @@ subdir('interfaces')
 
 subdir('tools/pg_bsd_indent')
 
+subdir('tools/pg-tidy')
+
 
 ### Generate a Makefile.global that's complete enough for PGXS to work.
 #
diff --git a/src/tools/pg-tidy/include/pg_tidy.h b/src/tools/pg-tidy/include/pg_tidy.h
new file mode 100644
index 00000000000..8157794beca
--- /dev/null
+++ b/src/tools/pg-tidy/include/pg_tidy.h
@@ -0,0 +1,12 @@
+#ifndef PG_TIDY_H
+#define PG_TIDY_H
+
+#if defined(__clang__) || defined(__GNUC__)
+#define PG_NO_PADDING __attribute__((annotate("pg_no_padding")))
+#define PG_REQUIRE_NO_PADDING __attribute__((annotate("pg_requires_no_padding")))
+#else
+#define PG_NO_PADDING
+#define PG_REQUIRE_NO_PADDING
+#endif
+
+#endif
diff --git a/src/tools/pg-tidy/meson.build b/src/tools/pg-tidy/meson.build
new file mode 100644
index 00000000000..c307a8ec48e
--- /dev/null
+++ b/src/tools/pg-tidy/meson.build
@@ -0,0 +1,69 @@
+# pg-tidy: clang-tidy checks for PostgreSQL
+#
+# This builds a clang-tidy plugin with PostgreSQL-specific checks.
+# Requires LLVM/clang development libraries and clang-tidy.
+
+if not pg_tidy_enabled
+  subdir_done()
+endif
+
+pg_tidy_lib = shared_module('pg_tidy',
+  'src/PgTidyModule.cpp',
+  'src/PgNoPaddingCheck.cpp',
+  'src/PgNoPaddingInitCheck.cpp',
+  'src/PgRequiresNoPaddingCheck.cpp',
+  dependencies: [llvm, clang_cpp_dep],
+  cpp_args: ['-fno-exceptions'],
+  override_options: ['cpp_std=c++17'],
+)
+
+# Run clang-tidy with pg-specific checks on each backend source file.
+# Each file gets its own custom_target with a stamp file, so ninja can
+# parallelize the checks and only re-run them when sources change.
+python = find_program('python3')
+pg_tidy_wrapper = meson.current_source_dir() / 'run_pg_tidy_file.py'
+
+foreach srcfile : backend_sources
+  srcfilename = fs.parent(srcfile) / fs.name(srcfile)
+  targetname = 'pg_tidy_' + srcfilename.underscorify()
+
+  custom_target(targetname,
+    input: [srcfile],
+    output: targetname + '.stamp',
+    command: [
+      python, pg_tidy_wrapper,
+      clang_tidy_prog,
+      pg_tidy_lib,
+      meson.project_build_root(),
+      '@INPUT@',
+      '@OUTPUT@',
+    ],
+    depends: [pg_tidy_lib, generated_backend_headers_stamp],
+    build_by_default: true,
+  )
+endforeach
+
+# Tests require lit and FileCheck from LLVM
+lit = find_program('lit', required: false)
+filecheck = find_program('FileCheck', required: false)
+
+if lit.found() and filecheck.found()
+  lit_cfg_data = configuration_data()
+  lit_cfg_data.set('CLANG_TIDY', clang_tidy_prog.full_path())
+  lit_cfg_data.set('FILECHECK', filecheck.full_path())
+  lit_cfg_data.set('PG_TIDY_LIB',
+    meson.current_build_dir() / 'lib' + pg_tidy_lib.name() + '.so')
+  lit_cfg_data.set('TEST_SOURCE_DIR', meson.current_source_dir() / 'test')
+  lit_cfg_data.set('INCLUDE_DIR', meson.current_source_dir() / 'include')
+
+  configure_file(
+    input: 'test/lit.site.cfg.py.in',
+    output: 'lit.site.cfg.py',
+    configuration: lit_cfg_data,
+  )
+
+  test('pg-tidy-lit-tests', lit,
+    args: [meson.current_build_dir(), '-v'],
+    depends: [pg_tidy_lib],
+  )
+endif
diff --git a/src/tools/pg-tidy/run_pg_tidy_file.py b/src/tools/pg-tidy/run_pg_tidy_file.py
new file mode 100644
index 00000000000..c85334bcc85
--- /dev/null
+++ b/src/tools/pg-tidy/run_pg_tidy_file.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+"""Run clang-tidy with pg-tidy checks on a single source file."""
+
+import subprocess
+import sys
+
+
+def main():
+    clang_tidy = sys.argv[1]
+    pg_tidy_lib = sys.argv[2]
+    build_dir = sys.argv[3]
+    source_file = sys.argv[4]
+    stamp_file = sys.argv[5]
+
+    result = subprocess.run([
+        clang_tidy,
+        '--load=' + pg_tidy_lib,
+        '-checks=-*,pg-*',
+        '-warnings-as-errors=pg-*',
+        '-p', build_dir,
+        source_file,
+    ], capture_output=True, text=True)
+
+    if result.returncode != 0:
+        # Print clang-tidy output so the user sees the actual diagnostics
+        if result.stdout:
+            print(result.stdout, file=sys.stderr, end='')
+        sys.exit(1)
+
+    with open(stamp_file, 'w'):
+        pass
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/tools/pg-tidy/src/PgNoPaddingCheck.cpp b/src/tools/pg-tidy/src/PgNoPaddingCheck.cpp
new file mode 100644
index 00000000000..da02607597f
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgNoPaddingCheck.cpp
@@ -0,0 +1,108 @@
+#include "PgNoPaddingCheck.h"
+#include <clang/AST/ASTContext.h>
+#include <clang/AST/Attr.h>
+#include <clang/AST/RecordLayout.h>
+#include <clang/ASTMatchers/ASTMatchFinder.h>
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::pg {
+
+static bool recordHasPadding(const RecordDecl *RD, const ASTContext &Ctx) {
+  if (RD->isUnion() || RD->isInvalidDecl() || !RD->isCompleteDefinition())
+    return false;
+
+  const ASTRecordLayout &Layout = Ctx.getASTRecordLayout(RD);
+  unsigned FieldIdx = 0;
+  uint64_t PrevEnd = 0;
+
+  for (const auto *Field : RD->fields()) {
+    uint64_t FieldOffset = Layout.getFieldOffset(FieldIdx);
+    uint64_t FieldSize = Ctx.getTypeSize(Field->getType());
+
+    if (FieldOffset > PrevEnd)
+      return true;
+
+    QualType FieldType = Field->getType().getCanonicalType();
+    if (const auto *FieldRD = FieldType->getAsRecordDecl()) {
+      if (recordHasPadding(FieldRD, Ctx))
+        return true;
+    }
+
+    PrevEnd = FieldOffset + FieldSize;
+    ++FieldIdx;
+  }
+
+  uint64_t RecordSize = Ctx.getTypeSize(
+      RD->getTypeForDecl()->getCanonicalTypeInternal());
+  return PrevEnd < RecordSize;
+}
+
+static bool hasNoPaddingAnnotation(const RecordDecl *RD) {
+  for (const auto *Attr : RD->specific_attrs<AnnotateAttr>()) {
+    if (Attr->getAnnotation() == "pg_no_padding")
+      return true;
+  }
+  return false;
+}
+
+void PgNoPaddingCheck::registerMatchers(MatchFinder *Finder) {
+  Finder->addMatcher(
+      recordDecl(isDefinition(), hasAttr(attr::Annotate)).bind("record"),
+      this);
+}
+
+void PgNoPaddingCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *RD = Result.Nodes.getNodeAs<RecordDecl>("record");
+  if (!RD || !hasNoPaddingAnnotation(RD))
+    return;
+
+  const ASTContext &Ctx = *Result.Context;
+  checkRecordForPadding(RD, Ctx);
+}
+
+void PgNoPaddingCheck::checkRecordForPadding(const RecordDecl *RD,
+                                              const ASTContext &Ctx) {
+  if (RD->isUnion() || RD->isInvalidDecl() || !RD->isCompleteDefinition())
+    return;
+
+  const ASTRecordLayout &Layout = Ctx.getASTRecordLayout(RD);
+  unsigned FieldIdx = 0;
+  uint64_t PrevEnd = 0;
+
+  for (const auto *Field : RD->fields()) {
+    uint64_t FieldOffset = Layout.getFieldOffset(FieldIdx);
+    uint64_t FieldSize = Ctx.getTypeSize(Field->getType());
+
+    if (FieldOffset > PrevEnd) {
+      uint64_t PadBytes = (FieldOffset - PrevEnd) / Ctx.getCharWidth();
+      diag(Field->getLocation(),
+           "%0 bytes of padding before field %1")
+          << static_cast<unsigned>(PadBytes) << Field;
+    }
+
+    QualType FieldType = Field->getType().getCanonicalType();
+    if (const auto *FieldRD = FieldType->getAsRecordDecl()) {
+      if (FieldRD->isCompleteDefinition() && !FieldRD->isUnion()) {
+        if (recordHasPadding(FieldRD, Ctx)) {
+          diag(Field->getLocation(),
+               "field %0 of type %1 contains internal padding")
+              << Field << FieldType;
+        }
+      }
+    }
+
+    PrevEnd = FieldOffset + FieldSize;
+    ++FieldIdx;
+  }
+
+  uint64_t RecordSize = Ctx.getTypeSize(RD->getTypeForDecl()->getCanonicalTypeInternal());
+  if (PrevEnd < RecordSize) {
+    uint64_t PadBytes = (RecordSize - PrevEnd) / Ctx.getCharWidth();
+    diag(RD->getBraceRange().getEnd(),
+         "%0 bytes of trailing padding in struct %1")
+        << static_cast<unsigned>(PadBytes) << RD;
+  }
+}
+
+} // namespace clang::tidy::pg
diff --git a/src/tools/pg-tidy/src/PgNoPaddingCheck.h b/src/tools/pg-tidy/src/PgNoPaddingCheck.h
new file mode 100644
index 00000000000..2e83c9b3fb2
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgNoPaddingCheck.h
@@ -0,0 +1,21 @@
+#ifndef PG_NO_PADDING_CHECK_H
+#define PG_NO_PADDING_CHECK_H
+
+#include <clang-tidy/ClangTidyCheck.h>
+
+namespace clang::tidy::pg {
+
+class PgNoPaddingCheck : public ClangTidyCheck {
+public:
+  PgNoPaddingCheck(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+
+private:
+  void checkRecordForPadding(const RecordDecl *RD, const ASTContext &Ctx);
+};
+
+} // namespace clang::tidy::pg
+
+#endif
diff --git a/src/tools/pg-tidy/src/PgNoPaddingInitCheck.cpp b/src/tools/pg-tidy/src/PgNoPaddingInitCheck.cpp
new file mode 100644
index 00000000000..dcb8793b7e1
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgNoPaddingInitCheck.cpp
@@ -0,0 +1,44 @@
+#include "PgNoPaddingInitCheck.h"
+#include <clang/AST/ASTContext.h>
+#include <clang/AST/Attr.h>
+#include <clang/ASTMatchers/ASTMatchFinder.h>
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::pg {
+
+static bool hasAnnotation(const Decl *D, StringRef Anno) {
+  for (const auto *Attr : D->specific_attrs<AnnotateAttr>()) {
+    if (Attr->getAnnotation() == Anno)
+      return true;
+  }
+  return false;
+}
+
+void PgNoPaddingInitCheck::registerMatchers(MatchFinder *Finder) {
+  Finder->addMatcher(
+      varDecl(hasLocalStorage(), unless(parmVarDecl())).bind("var"), this);
+}
+
+void PgNoPaddingInitCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *VD = Result.Nodes.getNodeAs<VarDecl>("var");
+  if (!VD)
+    return;
+
+  QualType T = VD->getType().getCanonicalType();
+  const RecordDecl *RD = T->getAsRecordDecl();
+  if (!RD)
+    return;
+
+  if (!hasAnnotation(RD, "pg_no_padding"))
+    return;
+
+  if (VD->hasInit())
+    return;
+
+  diag(VD->getLocation(),
+       "variable %0 of type %1 (marked pg_no_padding) is not zero-initialized")
+      << VD << T;
+}
+
+} // namespace clang::tidy::pg
diff --git a/src/tools/pg-tidy/src/PgNoPaddingInitCheck.h b/src/tools/pg-tidy/src/PgNoPaddingInitCheck.h
new file mode 100644
index 00000000000..b74be98c95b
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgNoPaddingInitCheck.h
@@ -0,0 +1,18 @@
+#ifndef PG_NO_PADDING_INIT_CHECK_H
+#define PG_NO_PADDING_INIT_CHECK_H
+
+#include <clang-tidy/ClangTidyCheck.h>
+
+namespace clang::tidy::pg {
+
+class PgNoPaddingInitCheck : public ClangTidyCheck {
+public:
+  PgNoPaddingInitCheck(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+};
+
+} // namespace clang::tidy::pg
+
+#endif
diff --git a/src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.cpp b/src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.cpp
new file mode 100644
index 00000000000..b5315400fb7
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.cpp
@@ -0,0 +1,89 @@
+#include "PgRequiresNoPaddingCheck.h"
+#include <clang/AST/ASTContext.h>
+#include <clang/AST/Attr.h>
+#include <clang/AST/Expr.h>
+#include <clang/ASTMatchers/ASTMatchFinder.h>
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::pg {
+
+static bool hasAnnotation(const Decl *D, StringRef Anno) {
+  for (const auto *Attr : D->specific_attrs<AnnotateAttr>()) {
+    if (Attr->getAnnotation() == Anno)
+      return true;
+  }
+  return false;
+}
+
+static const QualType resolveArgType(const Expr *Arg) {
+  Arg = Arg->IgnoreParenImpCasts();
+
+  if (const auto *UO = dyn_cast<UnaryOperator>(Arg)) {
+    if (UO->getOpcode() == UO_AddrOf) {
+      return UO->getSubExpr()->IgnoreParenImpCasts()->getType().getCanonicalType();
+    }
+  }
+
+  QualType T = Arg->getType().getCanonicalType();
+  if (T->isPointerType())
+    return T->getPointeeType().getCanonicalType();
+  if (T->isArrayType())
+    return T->getAsArrayTypeUnsafe()->getElementType().getCanonicalType();
+
+  return QualType();
+}
+
+void PgRequiresNoPaddingCheck::registerMatchers(MatchFinder *Finder) {
+  Finder->addMatcher(callExpr().bind("call"), this);
+}
+
+void PgRequiresNoPaddingCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *Call = Result.Nodes.getNodeAs<CallExpr>("call");
+  if (!Call)
+    return;
+
+  const FunctionDecl *FD = Call->getDirectCallee();
+  if (!FD)
+    return;
+
+  for (unsigned i = 0; i < FD->getNumParams() && i < Call->getNumArgs(); ++i) {
+    const ParmVarDecl *Param = FD->getParamDecl(i);
+    if (!hasAnnotation(Param, "pg_requires_no_padding"))
+      continue;
+
+    const Expr *Arg = Call->getArg(i);
+    QualType ArgType = resolveArgType(Arg);
+
+    if (ArgType.isNull()) {
+      diag(Arg->getExprLoc(),
+           "cannot verify no-padding requirement on void* argument");
+      continue;
+    }
+
+    if (ArgType->isVoidType()) {
+      diag(Arg->getExprLoc(),
+           "cannot verify no-padding requirement on void* argument");
+      continue;
+    }
+
+    if (ArgType->isBuiltinType())
+      continue;
+
+    const RecordDecl *RD = ArgType->getAsRecordDecl();
+    if (!RD) {
+      diag(Arg->getExprLoc(),
+           "cannot verify no-padding requirement on void* argument");
+      continue;
+    }
+
+    if (!hasAnnotation(RD, "pg_no_padding")) {
+      diag(Arg->getExprLoc(),
+           "argument type %0 passed to no-padding parameter lacks "
+           "pg_no_padding annotation")
+          << ArgType;
+    }
+  }
+}
+
+} // namespace clang::tidy::pg
diff --git a/src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.h b/src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.h
new file mode 100644
index 00000000000..494af66385d
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgRequiresNoPaddingCheck.h
@@ -0,0 +1,18 @@
+#ifndef PG_REQUIRES_NO_PADDING_CHECK_H
+#define PG_REQUIRES_NO_PADDING_CHECK_H
+
+#include <clang-tidy/ClangTidyCheck.h>
+
+namespace clang::tidy::pg {
+
+class PgRequiresNoPaddingCheck : public ClangTidyCheck {
+public:
+  PgRequiresNoPaddingCheck(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+};
+
+} // namespace clang::tidy::pg
+
+#endif
diff --git a/src/tools/pg-tidy/src/PgTidyModule.cpp b/src/tools/pg-tidy/src/PgTidyModule.cpp
new file mode 100644
index 00000000000..3ab486dd087
--- /dev/null
+++ b/src/tools/pg-tidy/src/PgTidyModule.cpp
@@ -0,0 +1,22 @@
+#include "PgNoPaddingCheck.h"
+#include "PgNoPaddingInitCheck.h"
+#include "PgRequiresNoPaddingCheck.h"
+#include <clang-tidy/ClangTidyModule.h>
+#include <clang-tidy/ClangTidyModuleRegistry.h>
+
+namespace clang::tidy::pg {
+
+class PgTidyModule : public ClangTidyModule {
+public:
+  void addCheckFactories(ClangTidyCheckFactories &CheckFactories) override {
+    CheckFactories.registerCheck<PgNoPaddingCheck>("pg-no-padding");
+    CheckFactories.registerCheck<PgNoPaddingInitCheck>("pg-no-padding-init");
+    CheckFactories.registerCheck<PgRequiresNoPaddingCheck>(
+        "pg-requires-no-padding");
+  }
+};
+
+static ClangTidyModuleRegistry::Add<PgTidyModule>
+    X("pg-module", "Checks specific to PostgreSQL.");
+
+} // namespace clang::tidy::pg
diff --git a/src/tools/pg-tidy/test/lit.cfg.py b/src/tools/pg-tidy/test/lit.cfg.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/tools/pg-tidy/test/lit.site.cfg.py.in b/src/tools/pg-tidy/test/lit.site.cfg.py.in
new file mode 100644
index 00000000000..f208966b486
--- /dev/null
+++ b/src/tools/pg-tidy/test/lit.site.cfg.py.in
@@ -0,0 +1,15 @@
+import os
+import lit.formats
+
+config.name = "pg-tidy"
+config.test_format = lit.formats.ShTest(True)
+config.suffixes = ['.c']
+
+config.test_source_root = '@TEST_SOURCE_DIR@'
+
+config.substitutions.append(('%clang_tidy', '@CLANG_TIDY@'))
+config.substitutions.append(('%pg_tidy_lib', '@PG_TIDY_LIB@'))
+config.substitutions.append(('%FileCheck', '@FILECHECK@'))
+config.substitutions.append(('%include_dir', '@INCLUDE_DIR@'))
+
+lit_config.load_config(config, os.path.join(config.test_source_root, 'lit.cfg.py'))
diff --git a/src/tools/pg-tidy/test/pg-no-padding-init.c b/src/tools/pg-tidy/test/pg-no-padding-init.c
new file mode 100644
index 00000000000..28e7dc2e7df
--- /dev/null
+++ b/src/tools/pg-tidy/test/pg-no-padding-init.c
@@ -0,0 +1,28 @@
+// RUN: %clang_tidy --load=%pg_tidy_lib -checks=-*,pg-no-padding-init %s \
+// RUN:   -- -I%include_dir 2>&1 | %FileCheck %s
+// CHECK: 1 warning generated.
+
+#include "pg_tidy.h"
+
+struct PG_NO_PADDING AnnotatedStruct {
+  int a;
+  int b;
+};
+
+struct PlainStruct {
+  int a;
+  int b;
+};
+
+void test(void) {
+  // CHECK: :[[@LINE+1]]:26: warning: variable 'bad' of type 'struct AnnotatedStruct' (marked pg_no_padding) is not zero-initialized [pg-no-padding-init]
+  struct AnnotatedStruct bad;
+  bad.a = 1;
+  bad.b = 2;
+
+  struct AnnotatedStruct good = {0};
+  good.a = 1;
+
+  struct PlainStruct no_warning;
+  no_warning.a = 1;
+}
diff --git a/src/tools/pg-tidy/test/pg-no-padding.c b/src/tools/pg-tidy/test/pg-no-padding.c
new file mode 100644
index 00000000000..eca3a84347a
--- /dev/null
+++ b/src/tools/pg-tidy/test/pg-no-padding.c
@@ -0,0 +1,118 @@
+// RUN: %clang_tidy --load=%pg_tidy_lib -checks=-*,pg-no-padding %s \
+// RUN:   -- -I%include_dir 2>&1 | %FileCheck %s
+// CHECK: 10 warnings generated.
+
+#include "pg_tidy.h"
+
+// --- Non-annotated struct: no warnings ---
+struct NotAnnotated {
+  char a;
+  int b;
+};
+
+// --- Annotated struct with no padding: no warnings ---
+struct PG_NO_PADDING NoPadding {
+  int a;
+  int b;
+};
+
+// --- Annotated struct with inter-field padding ---
+// CHECK: :[[@LINE+3]]:7: warning: 3 bytes of padding before field 'b' [pg-no-padding]
+struct PG_NO_PADDING InterFieldPad {
+  char a;
+  int b;
+};
+
+// --- Annotated struct with trailing padding ---
+// CHECK: :[[@LINE+4]]:1: warning: 3 bytes of trailing padding in struct 'TrailingPad' [pg-no-padding]
+struct PG_NO_PADDING TrailingPad {
+  int a;
+  char b;
+};
+
+// --- Annotated struct with both inter-field and trailing padding ---
+// CHECK: :[[@LINE+4]]:7: warning: 3 bytes of padding before field 'b' [pg-no-padding]
+// CHECK: :[[@LINE+5]]:1: warning: 3 bytes of trailing padding in struct 'BothPad' [pg-no-padding]
+struct PG_NO_PADDING BothPad {
+  char a;
+  int b;
+  char c;
+};
+
+// --- Nested struct with internal padding ---
+struct InnerPadded {
+  char x;
+  int y;
+};
+
+// CHECK: :[[@LINE+3]]:22: warning: field 'inner' of type 'struct InnerPadded' contains internal padding [pg-no-padding]
+struct PG_NO_PADDING HasPaddedInner {
+  int a;
+  struct InnerPadded inner;
+};
+
+// --- Nested struct without padding: no extra warnings ---
+struct InnerClean {
+  int x;
+  int y;
+};
+
+struct PG_NO_PADDING HasCleanInner {
+  int a;
+  struct InnerClean inner;
+};
+
+// --- Deeply nested padding ---
+struct DeepInner {
+  char a;
+  long b;
+};
+
+struct MidLevel {
+  struct DeepInner d;
+};
+
+// CHECK: :[[@LINE+4]]:19: warning: 4 bytes of padding before field 'mid' [pg-no-padding]
+// CHECK: :[[@LINE+3]]:19: warning: field 'mid' of type 'struct MidLevel' contains internal padding [pg-no-padding]
+struct PG_NO_PADDING DeepNest {
+  int a;
+  struct MidLevel mid;
+};
+
+// --- Empty annotated struct: no warnings ---
+struct PG_NO_PADDING AnnotatedEmpty {};
+
+// --- Single field, no padding ---
+struct PG_NO_PADDING SingleField {
+  int x;
+};
+
+// --- All same-size fields, no padding ---
+struct PG_NO_PADDING AllSameSize {
+  int a;
+  int b;
+  int c;
+};
+
+// --- Multiple padding sites ---
+// CHECK: :[[@LINE+5]]:7: warning: 3 bytes of padding before field 'b' [pg-no-padding]
+// CHECK: :[[@LINE+6]]:7: warning: 3 bytes of padding before field 'c' [pg-no-padding]
+// CHECK: :[[@LINE+7]]:1: warning: 3 bytes of trailing padding in struct 'MultiPad' [pg-no-padding]
+struct PG_NO_PADDING MultiPad {
+  char a;
+  int b;
+  char x;
+  int c;
+  char z;
+};
+
+// --- Annotated struct that itself is nested in a non-annotated struct: should still check ---
+struct PG_NO_PADDING AnnotatedInner {
+  int a;
+  int b;
+};
+
+struct OuterNotAnnotated {
+  char x;
+  struct AnnotatedInner inner;
+};
diff --git a/src/tools/pg-tidy/test/pg-requires-no-padding.c b/src/tools/pg-tidy/test/pg-requires-no-padding.c
new file mode 100644
index 00000000000..8436bfed207
--- /dev/null
+++ b/src/tools/pg-tidy/test/pg-requires-no-padding.c
@@ -0,0 +1,54 @@
+// RUN: %clang_tidy --load=%pg_tidy_lib -checks=-*,pg-requires-no-padding %s \
+// RUN:   -- -I%include_dir 2>&1 | %FileCheck %s
+// CHECK: 3 warnings generated.
+
+#include "pg_tidy.h"
+
+typedef unsigned int uint32;
+
+struct PG_NO_PADDING AnnotatedStruct {
+  int a;
+  int b;
+};
+
+struct NotAnnotatedStruct {
+  int a;
+  int b;
+};
+
+extern void XLogRegisterData(const void *data PG_REQUIRE_NO_PADDING, uint32 len);
+
+extern void NormalFunc(const void *data, uint32 len);
+
+void test_functions(void) {
+  struct AnnotatedStruct good;
+  struct NotAnnotatedStruct bad;
+  int primitive;
+  void *opaque = &good;
+
+  XLogRegisterData(&good, sizeof(good));
+
+  // CHECK: :[[@LINE+1]]:20: warning: argument type 'struct NotAnnotatedStruct' passed to no-padding parameter lacks pg_no_padding annotation [pg-requires-no-padding]
+  XLogRegisterData(&bad, sizeof(bad));
+
+  XLogRegisterData(&primitive, sizeof(primitive));
+
+  // CHECK: :[[@LINE+1]]:20: warning: cannot verify no-padding requirement on void* argument [pg-requires-no-padding]
+  XLogRegisterData(opaque, sizeof(good));
+
+  NormalFunc(&bad, sizeof(bad));
+  NormalFunc(opaque, sizeof(good));
+}
+
+void test_pointer_variable(void) {
+  struct AnnotatedStruct good;
+  struct NotAnnotatedStruct bad;
+
+  struct AnnotatedStruct *good_ptr = &good;
+  struct NotAnnotatedStruct *bad_ptr = &bad;
+
+  XLogRegisterData(good_ptr, sizeof(*good_ptr));
+
+  // CHECK: :[[@LINE+1]]:20: warning: argument type 'struct NotAnnotatedStruct' passed to no-padding parameter lacks pg_no_padding annotation [pg-requires-no-padding]
+  XLogRegisterData(bad_ptr, sizeof(*bad_ptr));
+}
-- 
2.43.0

