Best Practices for AI-Assisted App Development: Reviewing AI-Generated Code for Production AI coding assistants trained overwhelmingly on single-node MySQL and PostgreSQL code produce AI-generated database code that encodes single-node assumptions, creating silent correctness bugs when run on distributed SQL databases such as TiDB, according to a PingCAP blog post. The post recommends treating generated database code as a first draft and reviewing schema, transactions, indexes, write patterns, and queries before production, since unsupported syntax fails loudly while a missing ORDER BY returns plausible results that change between runs. The review checklist targets primary key types, ID ordering, composite index order, and write hotspots, and notes that MySQL compatibility keeps the review surface to a short, checkable list. Key Takeaways - Treat AI-generated database code as a first draft. Assistants learn from single-node MySQL and Postgres, so their assumptions don’t all hold on distributed SQL. - The dangerous failures are silent. Unsupported syntax errors loudly; a missing ORDER BY just returns plausible results that change between runs.- Review schema, transactions, indexes, writes, and queries. Primary key types, ID ordering, composite index order, and write hotspots are where generated code breaks. - MySQL compatibility keeps the review surface small. Most generated code works as written; the gaps are a short, checkable list. AI-assisted coding — prompting an AI coding assistant to write application code, schemas, and queries instead of hand-writing them — is now how most feature work starts. The generated code compiles, the tests pass, and the PR looks clean. What the assistant cannot tell you is whether that code is correct on your database . This blog is a review checklist for AI-generated database code: what to verify in schemas, transactions, indexes, write patterns, and SQL before any of it reaches production on TiDB. What Is AI-Assisted Coding — and Where Does It Break at the Database Layer? AI-assisted coding means a developer describes intent and an assistant produces the implementation. In practice that covers GitHub Copilot completing a repository method, Cursor scaffolding a migration, or Claude Code generating an entire data access layer from a prompt. AI-assisted app development extends the same pattern across the stack: schema, API, tests, and the SQL underneath. The upside is real and worth naming plainly. Boilerplate disappears. Prototypes that took a sprint take an afternoon. A developer who has never written a Spring @Transactional block gets a working one on the first try. That speed is why the shift happened. The failure mode is quieter. AI coding assistants are trained overwhelmingly on single-node MySQL and PostgreSQL code — decades of it. So AI-generated code encodes single-node assumptions: that a row lock is cheap and local, that auto-increment IDs come back in order, that result sets have a natural order, that a SAVEPOINT behaves identically everywhere. On a distributed SQL database, some of those assumptions hold and some of them produce silent correctness bugs — code that runs, returns results, and is wrong. Instead of treating AI-generated database code as finished work, treat it as a first draft written by someone who has never seen your storage layer. The rest of this blog is what to check, section by section. Choosing the Best AI Coding Assistant for Database Work Roundups of the best AI coding assistant rank tools on autocomplete quality and IDE integration. For database work, three different criteria decide whether generated code survives production: - Does it know your actual database, not just “generic SQL”? An assistant that has your schema, your TiDB version, and your MySQL-compatibility context in scope generates different DDL than one pattern-matching on “SQL database.” Feeding it your SHOW CREATE TABLE output and the relevant docs page costs one prompt and changes the output materially. - Can it explain its schema and index choices? If the assistant cannot justify why a composite index is ordered tenant id, created at rather than the reverse, neither can your reviewer. Reviewable reasoning matters more than confident output. - Does it work inside your stack? Migrations, test fixtures, and CI are where database mistakes get caught. AI coding tools that only operate in the editor push the entire review burden onto humans. None of that removes the review step. It narrows what you have to catch. For SDKs, templates, and reference implementations to point an assistant at, see Build AI Applications https://www.pingcap.com/developers/build-ai-apps/ . Why AI-Generated Database Code Needs a Review Layer This page is written for database architects, database administrators, infrastructure engineers, and application developers — the same audience as always, now reviewing far more code than they write. As an open source distributed SQL database, TiDB in most cases serves as a scale-out MySQL database without manual sharding. Because of its distributed nature, there are differences between TiDB and traditional relational databases like MySQL. For the full list, see TiDB MySQL Compatibility https://docs.pingcap.com/tidb/stable/mysql-compatibility/ . Those differences used to surface during migration, when a human was reading every line. They now surface in AI-generated pull requests, at volume, from developers who never learned the MySQL assumptions they are inheriting. The gap between “MySQL-compatible” and “identical to MySQL” is exactly where AI-generated code needs a second look — and it is a narrow, enumerable gap, which is what makes this reviewable. Reviewing AI-Generated Transactions and Locking Concurrency is where assistants produce the most confident and least examined code. Ask for “safely decrement inventory” and you will get a SELECT ... FOR UPDATE pattern lifted from standalone MySQL, often wrapped in nested transaction logic. Here is what to verify on TiDB. TiDB supports snapshot isolation. For the underlying model, see Transaction Overview https://docs.pingcap.com/tidb/stable/transaction-overview/ and TiDB Transaction Isolation Levels https://docs.pingcap.com/tidb/stable/transaction-isolation-levels/ . Optimistic vs. Pessimistic Locking in AI-Generated Concurrency Code Start by establishing which transaction mode the code actually runs under. Since TiDB v3.0.8, pessimistic transaction mode is the default, so SELECT ... FOR UPDATE blocks and waits much as it does in MySQL InnoDB. Under optimistic mode — still available via tidb txn mode or BEGIN OPTIMISTIC — TiDB caches writes and validates at commit, retrying and backing off on conflict. If the assistant generated code that disables retry or explicitly opens optimistic transactions while using SELECT ... FOR UPDATE , the later transactions in a conflict set roll back rather than queue. Mode aside, hot-row contention is the real review item. These are the patterns to flag: - Counters , where one field is incremented continuously. - Flash sales , where newly listed inventory sells out in seconds. - Account balances in financial workflows, where the same row is modified concurrently. In any database, concurrent SELECT ... FOR UPDATE transactions against one row serialize. In a distributed database, each lock acquisition is also a network round trip, so the serialized queue is slower than it would be on a single node. Correctness holds; throughput collapses. The best practice has not changed: move the hot counter out of the row. Implement it in a cache Redis, Codis and reconcile to TiDB, or shard the counter across N rows and sum on read. When an assistant hands you an inventory-decrement or hot-counter implementation, this is the refactor to require before merge. Nested Transactions and Savepoints That Won’t Survive Review Under ACID semantics, concurrent transactions are isolated from one another, so transactions cannot truly be “nested.” At read committed, repeated reads inside one transaction see data committed in between — non-repeatable reads. Most RDBMS products default to RC, and some developers treat that as a feature and build “nested transaction” logic on top of it. AI assistants reproduce the pattern because it fills their training data. Consider a T1–T8 timeline. Session 1 opens a transaction at T2 and runs a query. Between T3 and T5, session 2 opens, writes a row, and commits. Session 1 then updates that row at T6, commits at T7, and at T8 queries the val that session 2 wrote. At RC, T8 returns 102 and the nesting appears to work — but only because one thread simulated it. Under real concurrency, transactions interleave and the result is unpredictable. Under snapshot isolation or repeatable read, session 1’s view is fixed at T2, so session 2’s write does not change what session 1 reads at T6: Row 0 is updated instead, and T8 returns 2 . The fix is application logic — commit at T2 before proceeding. Savepoints need a version check. Spring’s PROPAGATION NESTED starts a subtransaction backed by a savepoint: BEGIN; INSERT INTO T2 VALUES 100 ; SAVEPOINT svp1; INSERT INTO T2 VALUES 200 ; ROLLBACK TO SAVEPOINT svp1; RELEASE SAVEPOINT svp1; COMMIT; TiDB has supported SAVEPOINT , ROLLBACK TO SAVEPOINT , and RELEASE SAVEPOINT since v6.2.0, so PROPAGATION NESTED works — with one flag for review. In a pessimistic transaction, ROLLBACK TO SAVEPOINT does not release locks taken after the savepoint; all locks clear at commit or rollback. Code expecting contention to ease mid-transaction behaves differently than on MySQL. On v6.1 or earlier, savepoints are unsupported and the nested logic must go. Oversized Transactions in AI-Generated Batch Code Ask an assistant for a backfill or a data migration and it will typically write one loop inside one transaction. TiKV, TiDB’s storage engine, is built on RocksDB and an LSM-tree; TiDB uses two-phase commit, and large transactions are bounded accordingly. TiKV stores data as key-value pairs, and the limits are expressed in those terms. One table row maps to one KV pair, and so does each index entry — a table with two secondary indexes writes three KV pairs per inserted row. On current versions: - A single row is limited to 6 MiB by default txn-entry-size-limit , adjustable up to 120 MiB. - Total transaction size defaults to 100 MiB txn-total-size-limit , with a maximum of 1 TB. Since v6.5.0 this configuration is no longer the recommended control; transaction memory accrues to session memory usage and tidb mem quota query applies. - The 5,000-statement ceiling stmt-count-limit applies only to retryable optimistic transactions. Pessimistic transactions and optimistic transactions with retry disabled are not bound by it. For bulk CREATE , DELETE , and UPDATE work, rewrite the single large transaction as paged statements committed in phases, using ORDER BY with LIMIT offsets: update tab set value='new value' where id in select id from tab order by id limit 0,10000 ; commit; update tab set value='new value' where id in select id from tab order by id limit 10000,10000 ; commit; update tab set value='new value' where id in select id from tab order by id limit 20000,10000 ; commit; Each batch commits independently, so a failure costs one page rather than the whole job. If the generated migration has no commit inside the loop, that is the edit to make. Reviewing AI-Generated Schema, Keys, and Constraints Schema is the first thing an assistant writes and the thing reviewers skim hardest — the DDL looks like every other CREATE TABLE they have read. Three things to check. Don’t Let AI Assume Sequential Auto-Increment IDs TiDB’s auto-increment IDs are guaranteed unique and incremental within a single TiDB server, but not allocated sequentially across the cluster. IDs are allocated in batches per instance, so with concurrent inserts across multiple tidb-server instances, a row inserted later can receive a smaller ID. Gaps are also normal: if no primary key is specified, tidb rowid shares an allocator with the auto-increment column, which is why IDs can advance by two. mysql CREATE TABLE t id INT UNIQUE KEY AUTO INCREMENT ; mysql INSERT INTO t VALUES ; mysql INSERT INTO t VALUES ; mysql INSERT INTO t VALUES ; mysql SELECT tidb rowid, id FROM t; +-------------+------+ | tidb rowid | id | +-------------+------+ | 2 | 1 | | 4 | 3 | | 6 | 5 | +-------------+------+ Any AI-generated code that treats the ID as an ordering or completeness guarantee is a bug: keyset pagination on id , “the newest record is MAX id “, gap detection as a data-quality check, or cursors that assume no holes. Order by an explicit timestamp or a monotonic column instead. If the application genuinely requires sequential allocation, TiDB provides an AUTO INCREMENT MySQL compatibility mode https://docs.pingcap.com/tidb/stable/auto-increment/ mysql-compatibility-mode — enable it deliberately, not by assumption. Note also that from v7.0.0, auto-increment columns no longer have to be a primary key or index prefix. Primary Key Types: Prefer bigint unsigned Auto-increment IDs usually exist to enforce uniqueness, so they are declared as the primary key or a unique index, with not null . The column should be an integer type, and bigint specifically. int auto-increment columns run out even on standalone databases; TiDB handles far more data and allocates IDs across multiple instances in parallel, so int exhausts faster. Since IDs are not negative, adding unsigned doubles the usable range — unsigned int tops out at 4,294,967,295, unsigned bigint at 18,446,744,073,709,551,615. auto inc id bigint unsigned not null primary key auto increment comment 'auto-increment ID' Assistants frequently default to INT AUTO INCREMENT because that is the most common pattern in their training data. Check the type on every generated table. Do not assign auto-increment values manually. Manual assignment triggers frequent updates to the global maximum and degrades write performance. Leave the column out of the INSERT , or pass NULL and let TiDB allocate: mysql create table autoid auto inc id bigint unsigned not null primary key auto increment comment 'auto-increment ID', b int ; mysql insert into autoid b values 100 ; mysql insert into autoid values null,1000 ; mysql select from autoid; +-------------+------+ | auto inc id | b | +-------------+------+ | 1 | 100 | | 2 | 1000 | +-------------+------+ Foreign Keys and Unique Constraints AI Tools Take for Granted TiDB enforces UNIQUE constraints through primary keys and unique indexes, with two operational differences: adding or dropping a CLUSTERED primary key is unsupported, and DROP COLUMN will not remove a primary key column. Generated migrations that plan to “fix the primary key later” will not apply. Foreign keys are a version question, and one where older blog posts and older training data both mislead. TiDB has supported foreign key constraints with referential integrity checks since v6.6.0. Foreign keys created before v6.6.0 stay ineffective after upgrade — SHOW CREATE TABLE marks them / FOREIGN KEY INVALID / — and must be dropped and recreated. And in pessimistic transactions, foreign key checks take an exclusive lock on the parent row by default, so an AI-generated child table taking high-concurrency writes against few parent rows is a contention source; from v8.5.6, tidb foreign key check in shared lock switches those checks to shared locks. Unique-constraint timing is subtler, and mode-dependent. In pessimistic transactions — the default since v3.0.8 — TiDB checks uniqueness when the statement executes tidb constraint check in place pessimistic , default ON . Optimistic transactions skip that read and verify at commit, which is faster on batch inserts but means a duplicate surfaces only at COMMIT and rolls back the whole batch: mysql create table t1 a int key ; mysql insert into t1 values 1 ; mysql begin; mysql insert into t1 values 1 ; Query OK, 1 row affected 0.00 sec mysql insert into t1 values 1 ; ERROR 1062 23000 : Duplicate entry '1' for key 'PRIMARY' mysql commit; ERROR 1062 23000 : Duplicate entry '1' for key 'PRIMARY' The first error fires because both duplicates sit inside one transaction; the second fires at commit, when those records are finally compared against the table. Setting tidb constraint check in place=1 at session level restores in-place checking for optimistic transactions. The review question is narrow: does the error handling assume the duplicate-key error arrives at INSERT time? Under optimistic transactions it arrives at COMMIT , and a try/catch around the individual statement never fires. Reviewing AI-Generated Indexes Indexes are data and occupy storage. In TiDB, indexes are stored as key-value pairs in the storage engine just like table rows — one index row is one KV pair. A table with 10 indexes writes 11 KV pairs per inserted row. Assistants add indexes liberally, one per query they were asked to optimize, and that write amplification is invisible in a code review that only reads the SELECT statements. TiDB supports primary key indexes, unique indexes, and secondary indexes, on single or multiple columns. FULLTEXT indexes are unsupported outside TiDB Cloud Starter in certain AWS regions, and descending indexes are unsupported — generated DDL containing either will not do what it appears to. Indexes can be used when the query predicate is among: =, , <, =, <=, like '...%', not like '...%', in, not in, < , =, is null, is not null The optimizer decides whether to use them. Indexes cannot be used for: js like '%...', like '%...%', not like '%...', not like '%...%', <= Leading-wildcard LIKE is worth calling out separately, because “search by name” prompts reliably produce LIKE '%term%' alongside an index the assistant just created to support it. That index will not be used. Composite Index Column Order: The Mistake AI Makes Most A composite index is declared as key tablekeyname a,b,c . The ground rule matches other databases: put high-selectivity columns first so fewer rows survive the first filter. The TiDB-specific detail is how a range predicate on a leading column affects the columns behind it. Consider: select a,b,c from tablename where a