<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Euro Format</title>
    <description>The latest articles on DEV Community by Euro Format (@euroformat).</description>
    <link>https://dev.to/euroformat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3752688%2F22885a53-fede-49b3-9a2a-c3b5a0bbd337.png</url>
      <title>DEV Community: Euro Format</title>
      <link>https://dev.to/euroformat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/euroformat"/>
    <language>en</language>
    <item>
      <title>How We Tamed 300+ Stored Procedures in a Legacy PostgreSQL Project</title>
      <dc:creator>Euro Format</dc:creator>
      <pubDate>Wed, 18 Feb 2026 11:11:53 +0000</pubDate>
      <link>https://dev.to/euroformatsro/how-we-tamed-300-stored-procedures-in-a-legacy-postgresql-project-2478</link>
      <guid>https://dev.to/euroformatsro/how-we-tamed-300-stored-procedures-in-a-legacy-postgresql-project-2478</guid>
      <description>&lt;p&gt;A client came to us with a deceptively simple task.&lt;/p&gt;

&lt;p&gt;Legacy PostgreSQL project.&lt;br&gt;
300+ stored procedures.&lt;br&gt;
Remove a deprecated field &lt;code&gt;account_id&lt;/code&gt; from the &lt;code&gt;transactions&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Sounds easy — until you realize no one knows where that field is used.&lt;/p&gt;

&lt;p&gt;The business logic lived inside the database — not in the repository, not in migrations, not in version control. The database was the source of truth.&lt;/p&gt;

&lt;p&gt;And you can’t &lt;code&gt;Ctrl+Shift+F&lt;/code&gt; inside a running database.&lt;/p&gt;


&lt;h2&gt;
  
  
  First idea: use PostgreSQL system catalogs
&lt;/h2&gt;

&lt;p&gt;PostgreSQL is not a black box. You can inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_proc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pg_depend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;information_schema&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pg_catalog&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can even build partial dependency graphs.&lt;/p&gt;

&lt;p&gt;We explored that path.&lt;/p&gt;

&lt;p&gt;The problem? It works well for structural dependencies. But it starts breaking down with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dynamic SQL (&lt;code&gt;EXECUTE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;string-based references&lt;/li&gt;
&lt;li&gt;functions calling other functions indirectly&lt;/li&gt;
&lt;li&gt;conditional logic embedded in procedures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get partial visibility. And partial visibility in legacy systems is often worse than no visibility — it creates false confidence.&lt;/p&gt;

&lt;p&gt;We needed something deterministic.&lt;/p&gt;


&lt;h2&gt;
  
  
  The pragmatic solution
&lt;/h2&gt;

&lt;p&gt;We wrote a small Python script.&lt;/p&gt;

&lt;p&gt;It connects to the database, extracts every stored procedure using &lt;code&gt;pg_get_functiondef&lt;/code&gt;, and saves each one as a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;catalog / schema / function|procedure / routine_name__identity_args.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To handle overloaded routines deterministically, we export them by OID and include their identity arguments in the filename to ensure stable and predictable output.&lt;/p&gt;

&lt;p&gt;Then we committed everything into the repository.&lt;/p&gt;

&lt;p&gt;Now the database layer was searchable.&lt;/p&gt;

&lt;p&gt;A simple &lt;code&gt;Find in Files&lt;/code&gt; in the IDE surfaced every usage of &lt;code&gt;account_id&lt;/code&gt; in seconds.&lt;/p&gt;

&lt;p&gt;No guesswork. No fragile dependency assumptions. Just text search over actual definitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this mattered more than it seems
&lt;/h2&gt;

&lt;p&gt;On the surface, this looks like a utility script.&lt;/p&gt;

&lt;p&gt;In practice, it did something more important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It created a snapshot of the database logic.&lt;/li&gt;
&lt;li&gt;It moved business logic into version control.&lt;/li&gt;
&lt;li&gt;It reduced operational blindness.&lt;/li&gt;
&lt;li&gt;It gave the team ownership of the DB layer again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Legacy is not old code.&lt;/p&gt;

&lt;p&gt;Legacy is code without ownership.&lt;/p&gt;

&lt;p&gt;Exporting procedures into git was the first step toward reclaiming that ownership.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is this perfect?
&lt;/h2&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;If someone edits a procedure directly in pgAdmin and doesn’t re-run the script, the files go stale.&lt;/p&gt;

&lt;p&gt;We considered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cron jobs&lt;/li&gt;
&lt;li&gt;CI/CD triggers&lt;/li&gt;
&lt;li&gt;automatic diff checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those solve the root issue: the database should not be the primary editing surface.&lt;/p&gt;

&lt;p&gt;But here’s the trade-off we accepted:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Staleness is cheaper than blindness&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The files don’t need to be perfectly up to date — only up to date when auditing or planning structural changes.&lt;/p&gt;

&lt;p&gt;And stored procedures in legacy systems tend to change far less frequently than application code.&lt;/p&gt;

&lt;p&gt;So we re-run the script before any structural change.&lt;br&gt;
That’s enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  What would be the “right” way?
&lt;/h2&gt;

&lt;p&gt;Ideally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL lives in the repository.&lt;/li&gt;
&lt;li&gt;Changes go through versioned migrations.&lt;/li&gt;
&lt;li&gt;Flyway, Liquibase, or plain versioned .sql files define the schema.&lt;/li&gt;
&lt;li&gt;The database becomes a result of code execution — not the source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a different project.&lt;/p&gt;

&lt;p&gt;We had a production system and a task to complete safely.&lt;/p&gt;

&lt;p&gt;The goal wasn’t architectural purity.&lt;br&gt;
The goal was controlled change.&lt;/p&gt;

&lt;p&gt;Full script available here:&lt;br&gt;
&lt;a href="https://github.com/euroformatsro/postgresql-procedure-export" rel="noopener noreferrer"&gt;https://github.com/euroformatsro/postgresql-procedure-export&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The outcome
&lt;/h2&gt;

&lt;p&gt;The script took less than an hour to write.&lt;/p&gt;

&lt;p&gt;The audit took minutes instead of days.&lt;/p&gt;

&lt;p&gt;We removed the field with confidence.&lt;/p&gt;

&lt;p&gt;We didn’t modernize the system.&lt;/p&gt;

&lt;p&gt;We just made it understandable.&lt;/p&gt;

&lt;p&gt;Sometimes that’s all you need.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>python</category>
      <category>refactoring</category>
      <category>database</category>
    </item>
  </channel>
</rss>
