<?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: Shashank</title>
    <description>The latest articles on DEV Community by Shashank (@shashank026).</description>
    <link>https://dev.to/shashank026</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1416984%2F5a2cd337-b2d1-4f59-ad2c-4b5111b82c90.png</url>
      <title>DEV Community: Shashank</title>
      <link>https://dev.to/shashank026</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shashank026"/>
    <language>en</language>
    <item>
      <title>How we let an AI read production databases without trusting it</title>
      <dc:creator>Shashank</dc:creator>
      <pubDate>Mon, 09 Nov 2026 05:45:08 +0000</pubDate>
      <link>https://dev.to/shashank026/how-we-let-an-ai-read-production-databases-without-trusting-it-3a6p</link>
      <guid>https://dev.to/shashank026/how-we-let-an-ai-read-production-databases-without-trusting-it-3a6p</guid>
      <description>&lt;p&gt;Every engineering team knows this ticket: &lt;strong&gt;"I was charged twice."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Someone stops what they're doing, queries the database, greps the code, reads recent deploys and checks the logs. Eventually they explain the answer to support, who explain it to the customer.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://tracexpro.com" rel="noopener noreferrer"&gt;TraceX&lt;/a&gt;, which does that investigation. To do it, it has to read production data, and the obvious reaction to "an AI reads your production database" is "absolutely not". That reaction is reasonable, so most of our design work has gone into one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you make this safe without relying on the model to behave?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what we ended up with, including what's still weak. The rule behind all of it: anything that matters is enforced in code, not requested in a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't trust the credentials: check them
&lt;/h2&gt;

&lt;p&gt;Asking people to "please use a read-only user" isn't a control. So TraceX checks the account's privileges when you connect, and refuses it if it can write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'information_schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;has_table_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%I.%I'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="s1"&gt;'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemata&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'information_schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;has_schema_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CREATE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;has_database_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_database&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'CREATE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns true, the connection is rejected with a message telling you to use a read-only account.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The model never writes SQL
&lt;/h2&gt;

&lt;p&gt;This is the decision I'd make again first. Giving a model a SQL tool means your safety depends on parsing whatever it generates. Instead, the model can only ask for a structured lookup: a table, some filters, some columns and a limit.&lt;/p&gt;

&lt;p&gt;That request is checked against the introspected schema, then compiled into a parameterized statement. An unknown table or column fails validation. There's no string for the model to inject into, because it never produces a query string.&lt;/p&gt;

&lt;p&gt;You lose flexibility. It can't write a clever five-way join. For "show me the records for this order", which is most of what an investigation needs, that's a trade I'm happy with.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Limit what a query can do, even when it's valid
&lt;/h2&gt;

&lt;p&gt;Every query runs inside a read-only transaction, with a timeout and a row cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET LOCAL statement_timeout = 3000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# at most 20 rows
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the database itself refuses writes, even if the checks above had a bug.&lt;/p&gt;

&lt;p&gt;A commenter on our launch pointed out the next problem: read-only doesn't help if your connection pool is already saturated during an incident. They were right. So TraceX uses one connection per investigation, closes it afterwards, and tells you whether you've connected a replica or a primary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_is_in_recovery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it's a primary, you still can connect, but you get a warning suggesting a read replica. You can also add your own ceiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;tracex&lt;/span&gt; &lt;span class="k"&gt;CONNECTION&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Be honest about what reaches the model
&lt;/h2&gt;

&lt;p&gt;Secret-looking fields and values are masked before a prompt is built. But the records TraceX reads do go to the model provider, because that's how it reasons about them. I'd rather say that plainly than imply otherwise. You can limit what it sees by restricting the database user to specific tables, and you can bring your own API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Don't let the model grade itself
&lt;/h2&gt;

&lt;p&gt;Ask a model how confident it is and you'll get a confident answer. So the confidence score is computed from the findings, not taken from the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;overall_confidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;supporting_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contradicting_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="n"&gt;grounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;supporting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;supporting_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_grounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best_confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# no evidence, so it can't sound sure
&lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;independent_sources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# logs, database and code agree
&lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;grounded_findings_against&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contradicting_ids&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A finding is "grounded" only if it cites real evidence: a record, a code location or a log line. A cause with no evidence is capped at 30%, however persuasive it sounds. The score never reaches 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Show what was ruled out
&lt;/h2&gt;

&lt;p&gt;This came directly from another comment we got:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A root-cause answer I can't check is just another confident guess, and the ruled-out hypotheses are the bit I'd actually want in front of a customer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When we looked, we found a gap: our dashboard had a "Ruled out" section, but real investigations never filled it in. So now they do, with one strict rule: &lt;strong&gt;an explanation is only listed as ruled out if it cites a real finding that rules it out.&lt;/strong&gt; If the model can't back it up, it's dropped, so "ruled out" can't become another unchecked claim.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokfxq5yd27e58irqpuwt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokfxq5yd27e58irqpuwt.gif" alt="TraceX ruling out two explanations and confirming a third" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same comment asked what happens when evidence points two ways. If a competing cause is supported within 0.15 of the leading one, TraceX shows both side by side, caps confidence at 60%, and doesn't draft a fix. A close call should go to a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. A fix is a suggestion, and some suggestions need a grown-up
&lt;/h2&gt;

&lt;p&gt;TraceX can write a fix, but it never opens a pull request on its own. A person clicks, and nothing is merged without your team.&lt;/p&gt;

&lt;p&gt;Risky fixes have stricter rules. A change is high risk if it touches migrations, the schema, auth, payments or config, or runs over 80 lines. We also check the content of the change, so a rename hidden in a file with an innocent name still counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DDL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(ALTER|DROP|CREATE|TRUNCATE|RENAME)\s+(TABLE|INDEX|COLUMN|TYPE|SCHEMA|CONSTRAINT)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|\bop\.(add_column|drop_column|alter_column|create_table|drop_table|rename_table)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That's shortened; the real pattern covers Django and Rails migrations too.) High-risk fixes open only as draft pull requests, only an admin can open them, and the PR explains why it's flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still weak
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It only connects to PostgreSQL and MongoDB.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't always reach an answer.&lt;/strong&gt; Sometimes it comes back inconclusive and says what's missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The structured queries limit it.&lt;/strong&gt; Some questions really do need a join it can't express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The DDL check is a pattern match,&lt;/strong&gt; so it can miss things. It's a safety net, not a guarantee, which is why a human still reviews every pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The numbers are judgment calls.&lt;/strong&gt; The 0.15 penalty and 60% cap are our best current choices, not measured constants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruled-out explanations depend on the model&lt;/strong&gt; thinking of the alternatives. We can check that each one cites evidence, but not that it considered everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;TraceX is in private beta. There's a &lt;a href="https://app.tracexpro.com/sandbox" rel="noopener noreferrer"&gt;sandbox&lt;/a&gt; with made-up data and no signup. Open "Refunds stuck in pending since Tuesday's release" and look at the Resolution tab to see the ruled-out explanations.&lt;/p&gt;

&lt;p&gt;I'd really like to hear from you: &lt;strong&gt;what would you need to see before letting a tool like this read your production systems?&lt;/strong&gt; And which of these guardrails do you think is weakest?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>devops</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
