<?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: Derek Cai</title>
    <description>The latest articles on DEV Community by Derek Cai (@caiderek).</description>
    <link>https://dev.to/caiderek</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%2F4148409%2F3317f790-2c54-4eca-a796-1e40b192cf81.jpg</url>
      <title>DEV Community: Derek Cai</title>
      <link>https://dev.to/caiderek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/caiderek"/>
    <language>en</language>
    <item>
      <title>Is "more human oversight" actually the right fix for AI agent incidents?</title>
      <dc:creator>Derek Cai</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:25:58 +0000</pubDate>
      <link>https://dev.to/caiderek/is-more-human-oversight-actually-the-right-fix-for-ai-agent-incidents-18oe</link>
      <guid>https://dev.to/caiderek/is-more-human-oversight-actually-the-right-fix-for-ai-agent-incidents-18oe</guid>
      <description>&lt;p&gt;A few things I've come across recently, news and write-ups, keep coming back to me.&lt;/p&gt;

&lt;p&gt;In April 2026, an AI coding agent at a company called PocketOS was working through a routine task in staging. It hit a credential mismatch, decided on its own to use a completely unrelated API token that happened to carry far too much access, and deleted the production database along with its backups. Nine seconds. Afterward the agent wrote out something close to: I violated every principle I was given, I should have asked you first, but I decided to do it anyway.&lt;/p&gt;

&lt;p&gt;Late September 2026, a different shape of failure. AI coding agents at over 300 companies (Fortune 500s, and one frontier AI lab) couldn't attach screenshots to pull requests through their CLI tooling, so they found a way around the limitation: created public GitHub repos and put the screenshots there. 13,000 internal screenshots leaked, including billing records and unreleased product features.&lt;/p&gt;

&lt;p&gt;Neither of these was a breach. In both cases the agent was authorized to do a thing, and then came up with a method nobody had thought to rule out. Replit's July 2025 production-database deletion is the same pattern, so this isn't new. It just looks like it's happening more often.&lt;/p&gt;

&lt;p&gt;Agents are also getting more capable, and wiring them into production is probably not something anyone is going to stop. So my guess is this category of problem grows rather than shrinks.&lt;/p&gt;

&lt;p&gt;The common answer right now is: restrict what the agent is allowed to do, add more human checkpoints. That sounds reasonable. But isn't it a step backward? It walks us back toward the early days, when the models were bad enough that you needed people in the loop constantly to get anything usable out of them. If running an agent needs that much human backstop, what's left of the reason to run one.&lt;/p&gt;

&lt;p&gt;Here's what I've been thinking instead, and I'm honestly not sure it holds up.&lt;/p&gt;

&lt;p&gt;Maybe human involvement belongs in the design phase, and the enforcement should be handed to a program. Broken out, five steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Permissions, set by a human&lt;/li&gt;
&lt;li&gt;The instructions, given by a human&lt;/li&gt;
&lt;li&gt;A program that blocks the agent's action, with a human brought in only when the program can't make the call&lt;/li&gt;
&lt;li&gt;The audit record&lt;/li&gt;
&lt;li&gt;The recovery mechanism&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are design work humans do up front. The last three are what actually absorbs the hit once something goes wrong. The part I think matters: human intervention sits inside step 3 as an exception branch, not as the main path. Most of the time the program should decide and block on its own, and a person steps in only when the program genuinely can't tell. That's a different thing from "restrict the agent, add more human review," which puts people back on the main path.&lt;/p&gt;

&lt;p&gt;When I went and read how the industry wrote up the PocketOS post-mortems, the direction mostly lines up: scope boundaries, confirmation gates so destructive operations can't complete on the agent's own authority, audit trails on every action, and a kill switch to stop it immediately. But the published versions seem to stop at the kill switch. Recovery doesn't get its own layer.&lt;/p&gt;

&lt;p&gt;PocketOS lost the database and the backups together. That didn't happen because nobody was logging, and it didn't happen because nothing could stop the agent. My read is that what was missing was a fast way to put it back.&lt;/p&gt;

&lt;p&gt;So I'll put the question out: does anyone have a better process for this, and when you design around it, does recovery actually get built in, or does it stay a backup-and-hope thing?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Empty SMALLINT Column That Led Me to Nine More Missing Types</title>
      <dc:creator>Derek Cai</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:20:37 +0000</pubDate>
      <link>https://dev.to/caiderek/the-empty-smallint-column-that-led-me-to-nine-more-missing-types-22p4</link>
      <guid>https://dev.to/caiderek/the-empty-smallint-column-that-led-me-to-nine-more-missing-types-22p4</guid>
      <description>&lt;p&gt;I had written a stock market analysis program before. One of its tables had a SMALLINT column. When I tested LogCarver against that table, every row through it came back empty.&lt;/p&gt;

&lt;p&gt;After finding that, I tested a batch of column types my own program rarely uses, plus a few types I had never specifically tested before. Turned out it wasn't just one missing type. Ten fixed-length types were missing: tinyint, smallint, bit, uniqueidentifier, money, smallmoney, datetime, smalldatetime, float, real. Any table using one of these lost that column's data, silently, no error, just nothing there.&lt;/p&gt;

&lt;p&gt;Adding them back, same old method: go back to fn_dblog, check the byte format against a real instance, one type at a time. A few things turned out different from what I expected.&lt;/p&gt;

&lt;p&gt;bit columns pack into a single byte, up to eight. I verified this by checking sys.system_internals_partition_columns: two different bit columns on the same table reported the identical leaf_offset. Their values sit in bit 0 and bit 1 of that one byte.&lt;/p&gt;

&lt;p&gt;The old DATETIME type (not DATETIME2) stores time as 1/300-second ticks since midnight. The problem is 300 doesn't divide evenly into SQL Server's 10,000,000 tick resolution. Multiply before dividing versus divide first, and you get a real, measurable difference. I measured it at about half a second off.&lt;/p&gt;

&lt;p&gt;While adding this batch, I also caught something unrelated to the new types: fn_dblog's own RowLog Contents columns cap out around 8000 bytes. A large enough in-row value can push a row past that cap. The old code treated that as corruption and dropped the whole row. Now it just flags the one column that went over and decodes the rest normally. All of this, plus the ten types, is in v0.1.12.&lt;/p&gt;

&lt;p&gt;Source and more detail: &lt;a href="https://github.com/caiderek/LogCarver" rel="noopener noreferrer"&gt;https://github.com/caiderek/LogCarver&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full background on LogCarver, why I built it and how I reverse-engineered the fn_dblog format: How I Recovered Deleted SQL Server Rows Without Ever Enabling CDC or Audit (&lt;a href="https://dev.to/caiderek/how-i-recovered-deleted-sql-server-rows-without-ever-enabling-cdc-or-audit-m10"&gt;https://dev.to/caiderek/how-i-recovered-deleted-sql-server-rows-without-ever-enabling-cdc-or-audit-m10&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The bit packing, the DATETIME tick math, and the RowLog Contents 8000-byte cap were worked out with AI.&lt;/p&gt;

</description>
      <category>database</category>
      <category>debugging</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQL Server / MySQL / PostgreSQL — the index differences</title>
      <dc:creator>Derek Cai</dc:creator>
      <pubDate>Fri, 02 Oct 2026 18:58:05 +0000</pubDate>
      <link>https://dev.to/caiderek/sql-server-mysql-postgresql-the-index-differences-30oj</link>
      <guid>https://dev.to/caiderek/sql-server-mysql-postgresql-the-index-differences-30oj</guid>
      <description>&lt;p&gt;I was reading an article comparing PostgreSQL and MySQL recently.&lt;/p&gt;

&lt;p&gt;PostgreSQL: cool features, lots of knobs, but index writes are expensive.&lt;br&gt;
MySQL: simpler to pick up, fewer knobs, cheaper index writes, but not as fast as PostgreSQL.&lt;/p&gt;

&lt;p&gt;That got me thinking — I use both of these, so let me add SQL Server too, since that's the one I use the most.&lt;/p&gt;

&lt;p&gt;SQL Server has two kinds of indexes: clustered and non-clustered.&lt;br&gt;
Clustered index: wherever the index is, the data is right there. No extra step needed.&lt;br&gt;
Non-clustered index: it first finds the clustered index's position, then goes to get the data. Or if there's no clustered index, it goes straight to a physical row locator, then gets the data. Either way, that's one extra step.&lt;/p&gt;

&lt;p&gt;MySQL's InnoDB forces every table to have an index.&lt;br&gt;
If there's a primary key, it automatically becomes the (clustered) index. No primary key? It picks a UNIQUE column for you. Nothing UNIQUE either? It generates a hidden row ID. Same as SQL Server: the clustered index is one step, but any non-clustered index still takes one extra step.&lt;/p&gt;

&lt;p&gt;PostgreSQL is a completely different idea.&lt;br&gt;
Every row has an address pointer called a TID. No matter what index you build, what gets stored is index value → TID. So querying an index always means: find the index entry, get the TID, then go fetch the data. That's one extra step too.&lt;/p&gt;

&lt;p&gt;So why does PostgreSQL's write cost get bigger? Say this row has two indexes — one on ID, one on date+order number. You update the order number. The TID changes.&lt;br&gt;
Now every index that was pointing at the old TID is stale, so anything tied to that TID has to get updated too.&lt;/p&gt;

&lt;p&gt;Remember what I said above — a PostgreSQL index's value points at a TID. The TID just changed, so every index has to update along with it: ID → new TID, date+order-number → new TID.&lt;br&gt;
So PostgreSQL ends up doing: update the row itself, plus update the ID index, plus update the date+order-number index. One logical update, three writes.&lt;/p&gt;

&lt;p&gt;MySQL and SQL Server, though? They only update the row itself, and whichever index actually got touched. If nothing got touched, it's just the one row write.&lt;br&gt;
In this example the update was to order number, which happens to be part of the date+order-number index — so it's: one write for the row, one for that index. Two writes total.&lt;br&gt;
If the update had been to some unrelated notes field instead, it would just be the one row write.&lt;/p&gt;

&lt;p&gt;That's why PostgreSQL's index write cost blows up the way it does.&lt;/p&gt;

</description>
      <category>sqlserver</category>
      <category>mysql</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How I Recovered Deleted SQL Server Rows Without Ever Enabling CDC or Audit</title>
      <dc:creator>Derek Cai</dc:creator>
      <pubDate>Tue, 29 Sep 2026 05:57:28 +0000</pubDate>
      <link>https://dev.to/caiderek/how-i-recovered-deleted-sql-server-rows-without-ever-enabling-cdc-or-audit-m10</link>
      <guid>https://dev.to/caiderek/how-i-recovered-deleted-sql-server-rows-without-ever-enabling-cdc-or-audit-m10</guid>
      <description>&lt;p&gt;Most data-recovery tools assume you turned on Change Data Capture, Change Tracking, or Audit &lt;em&gt;before&lt;/em&gt; the incident happened. In the real world — small and mid-sized companies running their own SQL Server — almost nobody does that. By the time someone notices bad data, it's already too late to turn those features on retroactively.&lt;/p&gt;

&lt;p&gt;SQL Server's transaction log already records every change. The problem is &lt;code&gt;fn_dblog&lt;/code&gt;, the function that reads it, is almost entirely undocumented — the row image byte layout isn't published anywhere. The only way to figure it out was to run real experiments against a live database and reverse-engineer the format from actual output, byte by byte.&lt;/p&gt;

&lt;p&gt;That's what became &lt;strong&gt;LogCarver&lt;/strong&gt;: a tool that reads &lt;code&gt;fn_dblog&lt;/code&gt; directly and reconstructs a table's full INSERT / UPDATE (before &amp;amp; after values) / DELETE history, with reviewable Undo and Replay SQL — no prior setup required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two bugs that only showed up on real data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After shipping it, I tested it against a perfectly ordinary table — one with no clustered index (a Heap table, which is also what you get from a &lt;code&gt;PRIMARY KEY NONCLUSTERED&lt;/code&gt;). Result: 0 events found. Worse, the tool's own error message &lt;em&gt;sounded&lt;/em&gt; reasonable — it said the VLF had probably been recycled and the data was likely gone.&lt;/p&gt;

&lt;p&gt;That explanation was wrong.&lt;/p&gt;

&lt;p&gt;Digging into &lt;code&gt;fn_dblog&lt;/code&gt;'s raw output, the real cause was much simpler and much worse: row operations on Heap tables are tagged with &lt;code&gt;LCX_HEAP&lt;/code&gt; in their Context field, but my filter only recognized &lt;code&gt;LCX_CLUSTERED&lt;/code&gt; and &lt;code&gt;LCX_MARK_AS_GHOST&lt;/code&gt;. Every single Heap-table record had been silently excluded from the start — nothing to do with VLF recycling at all. If I hadn't gone back to the raw &lt;code&gt;fn_dblog&lt;/code&gt; output, I would have believed my own tool's plausible-sounding, wrong explanation and moved on.&lt;/p&gt;

&lt;p&gt;Fixing that surfaced a second bug: any table with &lt;em&gt;any&lt;/em&gt; extra index — a &lt;code&gt;NONCLUSTERED PRIMARY KEY&lt;/code&gt; on a Heap table, or just an ordinary index someone added for query performance — broke the table-matching logic, which guessed at the right &lt;code&gt;AllocUnitName&lt;/code&gt; string pattern from the table name. An index's own internal maintenance records use a similar naming pattern, so they got matched too, mixing two completely unrelated pieces of internal SQL Server bookkeeping together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: stop guessing at string patterns. Query &lt;code&gt;sys.indexes&lt;/code&gt; for the table's actual storage structure (&lt;code&gt;index_id&lt;/code&gt; 0 = Heap, 1 = clustered index), build the exact &lt;code&gt;AllocUnitName&lt;/code&gt; from that, and match exactly instead of loosely. The same missing &lt;code&gt;index_id&lt;/code&gt; filter existed in the schema-reading query too — once fixed, extra indexes stopped polluting the column layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule that held throughout&lt;/strong&gt;: every assumption gets checked against a real SQL Server instance, never just inferred from docs or intuition — because for &lt;code&gt;fn_dblog&lt;/code&gt;, there mostly aren't any docs. None of these three bugs were found by reading documentation; all three were found by connecting to a real database and looking at what &lt;code&gt;fn_dblog&lt;/code&gt; actually returned. I reran the same test matrix against SQL Server 2016, 2019, and 2022 the same day to confirm the fix held across versions before calling any of them "validated."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current limitations&lt;/strong&gt;: &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;bigint&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;datetime2&lt;/code&gt;, &lt;code&gt;decimal&lt;/code&gt;/&lt;code&gt;numeric&lt;/code&gt;, &lt;code&gt;char&lt;/code&gt;/&lt;code&gt;nchar&lt;/code&gt;, &lt;code&gt;varchar&lt;/code&gt;/&lt;code&gt;nvarchar&lt;/code&gt; are decoded — a handful of less common fixed-length types (&lt;code&gt;money&lt;/code&gt;, &lt;code&gt;smallint&lt;/code&gt;, &lt;code&gt;bit&lt;/code&gt;, &lt;code&gt;uniqueidentifier&lt;/code&gt;, and a few others) are still on the list. &lt;code&gt;TRUNCATE TABLE&lt;/code&gt; is currently (over-cautiously) treated like a schema change, which hides events before it; known, not yet fixed.&lt;/p&gt;

&lt;p&gt;LogCarver is MIT-licensed: &lt;a href="https://github.com/caiderek/LogCarver" rel="noopener noreferrer"&gt;github.com/caiderek/LogCarver&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>sql</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
