<?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: Donghyun Park</title>
    <description>The latest articles on DEV Community by Donghyun Park (@sqemo).</description>
    <link>https://dev.to/sqemo</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%2F4098182%2F634bba4c-b929-495e-a989-2ec0f7b472e5.png</url>
      <title>DEV Community: Donghyun Park</title>
      <link>https://dev.to/sqemo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sqemo"/>
    <language>en</language>
    <item>
      <title>Database Naming Conventions: A Practical Guide for Tables and Columns</title>
      <dc:creator>Donghyun Park</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:54:21 +0000</pubDate>
      <link>https://dev.to/sqemo/database-naming-conventions-a-practical-guide-for-tables-and-columns-45pm</link>
      <guid>https://dev.to/sqemo/database-naming-conventions-a-practical-guide-for-tables-and-columns-45pm</guid>
      <description>&lt;p&gt;Every schema ends up with a naming convention, whether anyone chose one or not. The question isn't whether you'll have a convention — it's whether it's the same convention everywhere, or three different ones fighting each other across &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;tbl_orders&lt;/code&gt;, and &lt;code&gt;OrderItem&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naming conventions matter more than the convention
&lt;/h2&gt;

&lt;p&gt;The specific rules you pick matter less than you'd think. What matters is that everyone follows the same ones. A schema where every table uses &lt;code&gt;snake_case&lt;/code&gt; and every table uses &lt;code&gt;camelCase&lt;/code&gt; are both fine in isolation. A schema where half the tables use one and half use the other is a liability.&lt;/p&gt;

&lt;p&gt;Here's a concrete version of the cost. Say your &lt;code&gt;users&lt;/code&gt; table has a primary key column named &lt;code&gt;id&lt;/code&gt;, but a migration two years ago added a denormalized copy of it to an &lt;code&gt;orders&lt;/code&gt; table as &lt;code&gt;userId&lt;/code&gt;, and a third table — added by a contractor who was used to a different codebase — calls it &lt;code&gt;usr_id&lt;/code&gt;. Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new engineer writing a join has to grep the schema (or worse, guess) to find out which spelling applies to which table.&lt;/li&gt;
&lt;li&gt;Search tooling breaks. Searching for &lt;code&gt;user_id&lt;/code&gt; across the codebase silently misses every reference to &lt;code&gt;userId&lt;/code&gt; and &lt;code&gt;usr_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Code generation and ORMs that infer foreign keys from naming patterns (&lt;code&gt;user_id&lt;/code&gt; → &lt;code&gt;users.id&lt;/code&gt;) fail silently on the columns that don't match, and someone has to hand-write the mapping.&lt;/li&gt;
&lt;li&gt;Onboarding takes longer, because "how do we name things" isn't answerable by looking at one example — it requires reading enough of the schema to notice the exceptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is expensive to prevent. It's expensive to &lt;em&gt;unwind&lt;/em&gt; once forty tables depend on the inconsistency. That asymmetry is the entire argument for writing a convention down early, even a short one, and enforcing it before the schema grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case style: pick snake_case and move on
&lt;/h2&gt;

&lt;p&gt;The pragmatic reason to prefer &lt;code&gt;snake_case&lt;/code&gt; for tables and columns isn't aesthetics — it's that SQL databases don't agree on how they treat identifier case, and &lt;code&gt;snake_case&lt;/code&gt; is the one style that survives all of them unscathed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; folds unquoted identifiers to lowercase. Write &lt;code&gt;CREATE TABLE UserAccount&lt;/code&gt;, and Postgres stores it as &lt;code&gt;useraccount&lt;/code&gt;. Query it as &lt;code&gt;UserAccount&lt;/code&gt; without quotes and it still resolves to &lt;code&gt;useraccount&lt;/code&gt; — but the moment anyone quotes it (&lt;code&gt;"UserAccount"&lt;/code&gt;), Postgres treats that as a &lt;em&gt;different&lt;/em&gt;, case-sensitive identifier. Mixed-case naming plus inconsistent quoting is how you end up with two tables that look like one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; ties table-name case sensitivity to the underlying filesystem and the &lt;code&gt;lower_case_table_names&lt;/code&gt; server setting. A schema that works fine in development on Windows or macOS can break in production on Linux because table names became case-sensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle&lt;/strong&gt; folds unquoted identifiers to &lt;em&gt;uppercase&lt;/em&gt;, the mirror image of Postgres, with the same quoting trap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;snake_case&lt;/code&gt; sidesteps all of it because it never depends on case folding being consistent — there's no mixed case to fold incorrectly in the first place.&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="c1"&gt;-- Bad: relies on case being preserved and matched consistently&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;UserAccount&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: unambiguous under every dialect's folding rules&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_account&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick &lt;code&gt;snake_case&lt;/code&gt;, write it in the team's style guide, and stop relitigating it per pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Singular or plural table names?
&lt;/h2&gt;

&lt;p&gt;Both sides have a real argument. The singular camp says a table row represents &lt;em&gt;one instance&lt;/em&gt; of an entity, so &lt;code&gt;user&lt;/code&gt; reads correctly as "the user table" and composes cleanly with singular model class names. The plural camp says a table is a &lt;em&gt;collection&lt;/em&gt; of rows, so &lt;code&gt;users&lt;/code&gt; reads correctly as "the set of all users," which is how you'd describe it in English and how most ORMs (Rails, Django) default their table-name inference.&lt;/p&gt;

&lt;p&gt;Neither argument wins outright — frameworks and prominent open-source schemas exist on both sides. The only real mistake is not deciding.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Reads as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Singular&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;order_item&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;"the user table" — one row, one instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plural&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;users&lt;/code&gt;, &lt;code&gt;order_items&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;"the users table" — a collection of rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick one, write it down, and apply it to every table without exception — including join tables and lookup tables, which are the ones people forget to check against the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abbreviations: the silent killer
&lt;/h2&gt;

&lt;p&gt;Case style and pluralization are visible, one-time decisions. Abbreviations are worse, because they accumulate silently, one column at a time, usually introduced by whoever is typing fastest that day. &lt;code&gt;cust&lt;/code&gt; shows up next to &lt;code&gt;cstmr&lt;/code&gt;, &lt;code&gt;amt&lt;/code&gt; sits beside &lt;code&gt;amount&lt;/code&gt;, and six months later nobody can tell you which one is correct because both are "correct" — they're both in production.&lt;/p&gt;

&lt;p&gt;The fix isn't "abbreviate less" or "abbreviate more." It's a single &lt;strong&gt;approved abbreviation list&lt;/strong&gt;: a short, shared table that maps full terms to their one sanctioned short form, checked in reviews the same way you'd check a variable name against a linter.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Approved abbreviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;identifier&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quantity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qty&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;amount&lt;/td&gt;
&lt;td&gt;&lt;code&gt;amt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;&lt;code&gt;desc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cust&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The list doesn't need to be long, and it doesn't need every abbreviation you'll ever use — it needs to exist, be visible to everyone writing DDL, and win every time someone is tempted to invent a new shorthand on the spot. The deeper version of this mechanism — deriving every physical name from business terms through the list, instead of checking names against it after the fact — is the subject of &lt;a href="https://sqemo.com/blog/logical-vs-physical-data-models/" rel="noopener noreferrer"&gt;logical vs. physical data models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's also the step the international standard leaves to you. &lt;a href="https://sqemo.com/blog/iso-11179-naming-convention/" rel="noopener noreferrer"&gt;ISO/IEC 11179&lt;/a&gt; specifies how to compose a data element name from controlled vocabularies, but stops short of turning that name into a column — which is exactly why an abbreviation list, not the standard, is what decides whether two teams produce the same schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keys, foreign keys, and constraint names
&lt;/h2&gt;

&lt;p&gt;Primary keys have a real trade-off between &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;&amp;lt;table&amp;gt;_id&lt;/code&gt;. A bare &lt;code&gt;id&lt;/code&gt; is short and consistent — every table's primary key is spelled the same way, and &lt;code&gt;SELECT id FROM orders&lt;/code&gt; reads cleanly. &lt;code&gt;&amp;lt;table&amp;gt;_id&lt;/code&gt; (e.g., &lt;code&gt;order_id&lt;/code&gt; on the &lt;code&gt;orders&lt;/code&gt; table) is more verbose but disambiguates automatically in joins and in any context where the column travels without its table name attached — log lines, API payloads, CSV exports. Either is defensible; what breaks things is doing &lt;code&gt;id&lt;/code&gt; on some tables and &lt;code&gt;&amp;lt;table&amp;gt;_id&lt;/code&gt; on others.&lt;/p&gt;

&lt;p&gt;Foreign keys should name themselves after what they reference, not after the local table: a &lt;code&gt;orders&lt;/code&gt; row referencing &lt;code&gt;customers&lt;/code&gt; gets &lt;code&gt;customer_id&lt;/code&gt;, not &lt;code&gt;order_customer&lt;/code&gt; or bare &lt;code&gt;customer&lt;/code&gt;. That naming makes the join implicit — &lt;code&gt;orders.customer_id = customers.id&lt;/code&gt; reads correctly even to someone who's never seen the schema.&lt;/p&gt;

&lt;p&gt;Constraint names matter more than people give them credit for, because they're the string that shows up in the error message when the constraint fires in production. A predictable pattern makes those errors greppable instead of cryptic.&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_number&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customers&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uq_orders_order_number&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_number&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;&lt;code&gt;fk_&amp;lt;table&amp;gt;_&amp;lt;referenced_table&amp;gt;&lt;/code&gt; and &lt;code&gt;uq_&amp;lt;table&amp;gt;_&amp;lt;columns&amp;gt;&lt;/code&gt; are enough structure to make any constraint violation self-explanatory from the name alone — no need to go look up what &lt;code&gt;orders_customer_id_fkey&lt;/code&gt; (Postgres's auto-generated default) actually constrains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserved words and portability
&lt;/h2&gt;

&lt;p&gt;Avoid naming tables or columns after SQL reserved words — &lt;code&gt;order&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;group&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;table&lt;/code&gt; — even when the database in front of you happens to tolerate it unquoted or with minimal fuss. The problem isn't today's dialect; it's every future context the name travels through: a different database engine after a migration, a query builder that doesn't quote identifiers by default, a raw SQL string pasted into a script without the quoting the ORM would have added automatically. &lt;code&gt;order&lt;/code&gt; is a particularly common trap because it's also an extremely natural table name for e-commerce schemas — &lt;code&gt;customer_order&lt;/code&gt; or &lt;code&gt;purchase_order&lt;/code&gt; costs you nothing and removes the trap entirely. The general rule of portability: assume your schema will eventually be queried by a tool, dialect, or person that doesn't share your current database's tolerances, and name defensively for that case rather than the one in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it stick: from wiki page to enforcement
&lt;/h2&gt;

&lt;p&gt;A naming convention that lives only on a wiki page gets followed for about two weeks. After that, deadlines happen, someone new joins without reading the wiki, and the first exception becomes precedent for the next five. Documentation alone doesn't hold a line — it just tells you what the line &lt;em&gt;was supposed to be&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The realistic path is to escalate enforcement in stages, matching effort to how much the convention has already drifted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write it down&lt;/strong&gt; — a short style guide (case, pluralization, the abbreviation list, key/constraint patterns) that's easy to skim, not a spec nobody reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put it in code review&lt;/strong&gt; — a checklist item reviewers actually check against new migrations, not an assumption that people remember the wiki page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate it&lt;/strong&gt; — &lt;a href="https://sqemo.com/docs/mcp/" rel="noopener noreferrer"&gt;lint DDL in CI&lt;/a&gt; against the documented rules, or &lt;a href="https://sqemo.com/docs/naming-standards/" rel="noopener noreferrer"&gt;generate physical names from a shared word list&lt;/a&gt; so the convention is applied mechanically instead of remembered by each contributor. A tool-neutral version of this: keep one word list of approved terms and abbreviations, and derive every physical column name from it programmatically rather than typing each one by hand — that's the only stage where "follow the convention" stops being a matter of individual discipline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams stop at stage one and wonder why the schema still drifts. Stage two catches most of what's left. Stage three is the only stage that survives a team turning over.&lt;/p&gt;

&lt;p&gt;That shared word list has a companion beyond naming: a term's &lt;em&gt;definition&lt;/em&gt; belongs in the same model as the words that name it, so the convention and the meaning don't end up in separate documents that drift apart — see &lt;a href="https://sqemo.com/blog/data-dictionary-vs-business-glossary/" rel="noopener noreferrer"&gt;data dictionary vs business glossary&lt;/a&gt; for why keeping them together matters.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sqemo.com/blog/database-naming-conventions/" rel="noopener noreferrer"&gt;sqemo.com&lt;/a&gt;. Sqemo is an ERD tool that enforces database naming conventions — free in the browser, no signup.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>postgres</category>
      <category>bestpractices</category>
    </item>
    <item>
      <title>Building a Schema With an AI Agent Without Naming a Single Column</title>
      <dc:creator>Donghyun Park</dc:creator>
      <pubDate>Wed, 09 Sep 2026 11:45:27 +0000</pubDate>
      <link>https://dev.to/sqemo/building-a-schema-with-an-ai-agent-without-naming-a-single-column-o9m</link>
      <guid>https://dev.to/sqemo/building-a-schema-with-an-ai-agent-without-naming-a-single-column-o9m</guid>
      <description>&lt;p&gt;Ask an AI agent for a discussion-board schema and you'll get one in about four seconds. Here is the part nobody mentions: you'll also get &lt;code&gt;is_deleted&lt;/code&gt; in this table and &lt;code&gt;del_yn&lt;/code&gt; in the next one, because the model is pattern-matching against every schema on GitHub, and those schemas disagree with each other and with you.&lt;/p&gt;

&lt;p&gt;The fix is not a better prompt. It is giving the agent the same thing you'd give a new hire — the list of words your team actually uses — and making it impossible to ignore.&lt;/p&gt;

&lt;p&gt;I did that with a threaded discussion board. I never typed a column name. The whole run fits in three and a half minutes:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/MqHWtiHfBI0" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup, in one call
&lt;/h2&gt;

&lt;p&gt;The agent starts by binding a new project to our team standard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="err"&gt;create_erd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Threaded Discussion Board"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="na"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;get_erd_overview&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// → { glossaryLinked: true, dictionaryEntryCount: 28, domainCount: 15 }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty-eight words and fifteen domains arrive with the project. That is the whole trick. From here the agent isn't &lt;em&gt;choosing&lt;/em&gt; names — it's &lt;em&gt;resolving&lt;/em&gt; them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describing the work
&lt;/h2&gt;

&lt;p&gt;Then I talked to it the way I'd talk to a colleague: &lt;em&gt;members post articles, a post can reply to another post, members comment on posts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The agent called the tools with &lt;strong&gt;logical&lt;/strong&gt; names — business terms, not column names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="err"&gt;upsert_entity&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;logicalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Post"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="c1"&gt;// → POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;upsert_attribute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;logicalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Post Content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Content"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// → POST_CNTS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;upsert_attribute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;logicalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delete Flag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flag"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="c1"&gt;// → DELETE_YN&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Content&lt;/code&gt; → &lt;code&gt;CNTS&lt;/code&gt; and &lt;code&gt;Flag&lt;/code&gt; → &lt;code&gt;YN&lt;/code&gt; aren't the model's taste. They're our word list's abbreviations, applied exactly as they were applied in every table we've built before. The domain carries the type, so &lt;code&gt;Content&lt;/code&gt; is &lt;code&gt;varchar(1000)&lt;/code&gt; everywhere — nobody decides column widths per table anymore.&lt;/p&gt;

&lt;p&gt;Notice what didn't happen: I didn't review a name and correct it. There was nothing to correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most tools get wrong
&lt;/h2&gt;

&lt;p&gt;Then the board needed threading — a post that replies to another post. A self-referencing foreign key.&lt;/p&gt;

&lt;p&gt;This is where diagram tools quietly fall over. The child column can't reuse the parent's name (&lt;code&gt;POST_NO&lt;/code&gt; referencing &lt;code&gt;POST_NO&lt;/code&gt; in the same table), so something has to give it a role. Most tools either refuse, or let you type whatever you want, which is how you end up with &lt;code&gt;parent_id&lt;/code&gt; in one table and &lt;code&gt;prnt_post_no&lt;/code&gt; in another.&lt;/p&gt;

&lt;p&gt;Here the role prefix is itself a naming rule, resolved through the same word list as everything else:&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="nv"&gt;`PARENT_POST_NO`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'Parent Post Number'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register &lt;code&gt;Parent&lt;/code&gt; → &lt;code&gt;PRNT&lt;/code&gt; in the word list and it becomes &lt;code&gt;PRNT_POST_NO&lt;/code&gt; instead — team-wide, in every self-referencing relationship anyone ever draws. The prefix is a decision made once, by the standard's owner, not per-table by whoever is typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when a word is missing
&lt;/h2&gt;

&lt;p&gt;Real modelling hits words nobody registered. Ours didn't have "Delete", which is why &lt;code&gt;DELETE_YN&lt;/code&gt; came out unabbreviated. The lint said so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="err"&gt;lint_erd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// → { code: "unknown-word", severity: "warning", objectName: "Delete Flag" }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's move here is the interesting one. It does &lt;strong&gt;not&lt;/strong&gt; invent &lt;code&gt;DEL&lt;/code&gt;. It files a proposal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="err"&gt;propose_dictionary_word&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="na"&gt;logicalWord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;physicalWord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DELETE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;abbreviation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Found while modelling the discussion board"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// → { status: "pending" }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A human approves it, and the abbreviation propagates to every connected project. The agent is a proposer, not an authority. That distinction is the entire difference between an assistant and a liability — and it's the thing a chatbot with no shared state cannot do, no matter how good the model gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking work that already exists
&lt;/h2&gt;

&lt;p&gt;The same computation runs backwards. Give it a name someone already wrote and it tells you what the standard would have produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="err"&gt;check_naming&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;logicalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer Phone Number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="na"&gt;physicalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUSTOMER_PHONE_NUMBER"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// → { generatedPhysicalName: "CUST_TEL_NO", compliant: false }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs in CI with no agent involved — &lt;code&gt;npx sqemo-mcp lint schema.erd.json&lt;/code&gt; exits 1 on violations. Humans, agents, and pull requests all get held to the same computation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`POST`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;`POST_NO`&lt;/span&gt;        &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'Post Number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`POST_CNTS`&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'Post Content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`DELETE_YN`&lt;/span&gt;      &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'N'&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'Delete Flag'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`PARENT_POST_NO`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'Parent Post Number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`POST_NO`&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;The business terms survive as column comments, so the meaning reaches the database instead of dying in a diagram nobody opens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;Being straight about the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The self-reference prefix is one global rule, not a per-relationship choice.&lt;/strong&gt; It defaults to &lt;code&gt;Parent&lt;/code&gt;, and abbreviating it means registering &lt;code&gt;Parent&lt;/code&gt; in the word list like any other word. A project that already has a self-referencing relationship keeps the prefix those columns were built with, so their names don't shift underneath it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naming rules aren't retroactive.&lt;/strong&gt; Change a rule and existing physical names keep what they have. Self-referencing FKs are the exception: their names are derived, so they get rewritten on their next edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent still models badly if you describe the work badly.&lt;/strong&gt; Nothing here checks whether your entities make sense — only that they're named the way your team agreed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;Look at who decided what:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Made by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Which tables and relationships exist&lt;/td&gt;
&lt;td&gt;Me, in business terms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abbreviations, separators, case&lt;/td&gt;
&lt;td&gt;The team standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data types&lt;/td&gt;
&lt;td&gt;Domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New words entering the standard&lt;/td&gt;
&lt;td&gt;The standard's owner, by approval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent did the typing and none of the deciding. A naming convention in a wiki asks every person and every tool to remember it. This one is computable, which means it can be applied, checked, and enforced without anyone remembering anything.&lt;/p&gt;

&lt;p&gt;The step-by-step version, with every call and its output, is in the &lt;a href="https://sqemo.com/docs/ai-walkthrough" rel="noopener noreferrer"&gt;walkthrough&lt;/a&gt;. If you want the argument before the tutorial, &lt;a href="https://sqemo.com/blog/erd-mcp-server" rel="noopener noreferrer"&gt;an ERD MCP server&lt;/a&gt; covers what these tools are and where they stop; &lt;a href="https://sqemo.com/blog/database-naming-conventions" rel="noopener noreferrer"&gt;database naming conventions&lt;/a&gt; covers what to put in the word list in the first place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.sqemo.com" rel="noopener noreferrer"&gt;Try it in the app&lt;/a&gt; — no signup — or point your agent at &lt;code&gt;npx sqemo-mcp&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sqemo.com/blog/schema-without-naming-columns" rel="noopener noreferrer"&gt;sqemo.com&lt;/a&gt;. Sqemo is an ERD tool that enforces database naming conventions — free in the browser, no signup.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>database</category>
      <category>claude</category>
    </item>
    <item>
      <title>An ERD MCP Server: AI Agents That Follow Your Naming Standard</title>
      <dc:creator>Donghyun Park</dc:creator>
      <pubDate>Fri, 28 Aug 2026 01:04:15 +0000</pubDate>
      <link>https://dev.to/sqemo/an-erd-mcp-server-ai-agents-that-follow-your-naming-standard-5dee</link>
      <guid>https://dev.to/sqemo/an-erd-mcp-server-ai-agents-that-follow-your-naming-standard-5dee</guid>
      <description>&lt;p&gt;AI agents already write database schemas. Ask Claude or Cursor for a feature and the migration file comes back with tables, columns, and foreign keys — named however the model's training data leans that day. &lt;code&gt;user_id&lt;/code&gt; here, &lt;code&gt;userId&lt;/code&gt; there, &lt;code&gt;usr_no&lt;/code&gt; when it read one too many legacy dumps. The agent isn't wrong; it just has no idea your team writes &lt;code&gt;cust_no&lt;/code&gt;, because your naming convention lives in a wiki the model has never seen.&lt;/p&gt;

&lt;p&gt;That's the actual problem an ERD MCP server solves. Not "AI can draw diagrams now" — but that schema work done by agents can follow the same standard as schema work done by people. This post explains what &lt;a href="https://www.npmjs.com/package/sqemo-mcp" rel="noopener noreferrer"&gt;sqemo-mcp&lt;/a&gt; does, how the naming part works, and — in the same honest-comparison spirit as our &lt;a href="https://sqemo.com/blog/dbdiagram-alternative" rel="noopener noreferrer"&gt;dbdiagram comparison&lt;/a&gt; — what it doesn't do yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an MCP server is, in one paragraph
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP (Model Context Protocol)&lt;/a&gt; is the open standard for giving AI agents tools. A server exposes typed operations — "list entities," "add an attribute," "export SQL" — and any MCP-capable client (Claude Code, Claude Desktop, Cursor, Codex, and a growing list) can call them. Instead of the agent hallucinating your schema from half-remembered context, it reads the real one and edits it through operations that enforce your rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent can do with your ERD
&lt;/h2&gt;

&lt;p&gt;sqemo-mcp exposes 36 tools over your ERD files. Grouped by what they're for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read and edit the model.&lt;/strong&gt; &lt;code&gt;get_erd_overview&lt;/code&gt;, &lt;code&gt;get_entity&lt;/code&gt;, &lt;code&gt;upsert_entity&lt;/code&gt;, &lt;code&gt;upsert_attribute&lt;/code&gt;, &lt;code&gt;upsert_relationship&lt;/code&gt;, and their delete counterparts. The agent works on the same &lt;code&gt;.erd.json&lt;/code&gt; file you edit in the &lt;a href="https://app.sqemo.com" rel="noopener noreferrer"&gt;app&lt;/a&gt; — one source of truth, not a parallel copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import and export.&lt;/strong&gt; &lt;code&gt;import_sql&lt;/code&gt; and &lt;code&gt;export_sql&lt;/code&gt; in seven dialects (MySQL, PostgreSQL, Oracle, SQL Server, SQLite, H2, CUBRID), plus DBML both ways. Paste a raw &lt;code&gt;pg_dump -s&lt;/code&gt; or &lt;code&gt;mysqldump&lt;/code&gt; output and it parses. "Import this legacy dump and normalize the names against our word list" is a one-prompt task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate and check names.&lt;/strong&gt; &lt;code&gt;generate_physical_name&lt;/code&gt;, &lt;code&gt;check_naming&lt;/code&gt;, &lt;code&gt;search_dictionary&lt;/code&gt; — more on these below, because they're the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality and hygiene.&lt;/strong&gt; &lt;code&gt;lint_erd&lt;/code&gt;, &lt;code&gt;validate_erd&lt;/code&gt;, &lt;code&gt;diff_erds&lt;/code&gt;, &lt;code&gt;auto_layout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose, don't decree.&lt;/strong&gt; When the agent needs a word that isn't in the team word list, &lt;code&gt;propose_dictionary_word&lt;/code&gt; files a proposal into the team's &lt;a href="https://sqemo.com/docs/naming-standards" rel="noopener noreferrer"&gt;approval queue&lt;/a&gt; instead of silently inventing an abbreviation. A human approves it; then it's standard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the naming tools are the interesting part
&lt;/h2&gt;

&lt;p&gt;Most schema tools treat names as free text, so an agent's naming is only as good as its prompt. Sqemo treats the physical name as &lt;strong&gt;computed&lt;/strong&gt;: you register a word list (customer → &lt;code&gt;cust&lt;/code&gt;, number → &lt;code&gt;no&lt;/code&gt;) and naming rules once, and the physical column name is generated from the logical one — same input, same output, for humans and agents alike.&lt;/p&gt;

&lt;p&gt;That changes what the agent is asked to do. It doesn't guess that your team abbreviates "customer" as &lt;code&gt;cust&lt;/code&gt; — it says "Customer Number" and &lt;code&gt;generate_physical_name&lt;/code&gt; returns &lt;code&gt;cust_no&lt;/code&gt;, per your rules, deterministically. And because the correct name is computable, drift is detectable: &lt;code&gt;check_naming&lt;/code&gt; and &lt;code&gt;lint_erd&lt;/code&gt; flag any column that deviates from what the word list would generate. The agent's output is held to the same standard as a human's — mechanically, not by code-review vigilance. If you want the fuller argument for generating names from a word list, that's &lt;a href="https://sqemo.com/blog/database-naming-conventions" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Node.js 22+, no install — it runs via &lt;code&gt;npx&lt;/code&gt;. For Claude Code, add to &lt;code&gt;.mcp.json&lt;/code&gt; at your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sqemo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqemo-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Desktop and most other clients take the same &lt;code&gt;mcpServers&lt;/code&gt; JSON; the &lt;a href="https://sqemo.com/docs/mcp" rel="noopener noreferrer"&gt;docs page&lt;/a&gt; has per-client paths. Local &lt;code&gt;.erd.json&lt;/code&gt; files work fully offline with no account. To let an agent touch ERDs saved on the Sqemo server, sign in once with &lt;code&gt;npx sqemo-mcp login&lt;/code&gt; — credentials are stored locally, and your password itself never is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same enforcement, without an agent
&lt;/h2&gt;

&lt;p&gt;The package doubles as a plain CLI, which is how the standard reaches CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx sqemo-mcp lint schema.erd.json   &lt;span class="c"&gt;# exit code 1 on naming violations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put that in a GitHub Action and a pull request that drifts from the word list fails the build — the same check the agent runs interactively, enforced mechanically on every merge. There's also &lt;code&gt;export&lt;/code&gt; for generating SQL or DBML in a pipeline. If you're deciding what artifact should be the source of truth in the first place, we wrote about &lt;a href="https://sqemo.com/blog/dbml-vs-sql-ddl" rel="noopener noreferrer"&gt;DBML vs SQL DDL&lt;/a&gt; — the &lt;code&gt;.erd.json&lt;/code&gt; model sits a layer above both, and the CLI derives either from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;Honest limits, so you can decide with eyes open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a local stdio server, not a hosted one.&lt;/strong&gt; It runs on your machine via &lt;code&gt;npx&lt;/code&gt;. There's no remote HTTPS endpoint yet, so clients that only support hosted MCP servers can't connect — remote MCP is on our roadmap, not shipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server code is open, the app isn't.&lt;/strong&gt; The MCP server is on &lt;a href="https://www.npmjs.com/package/sqemo-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt; and the &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;official MCP Registry&lt;/a&gt; (&lt;code&gt;io.github.sqemo/sqemo&lt;/code&gt;); the web app itself is not open source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three tools are paid.&lt;/strong&gt; &lt;code&gt;export_alter_sql&lt;/code&gt; (migration ALTER scripts from a baseline diff) and the two live-database tools, &lt;code&gt;introspect_db&lt;/code&gt; and &lt;code&gt;check_db_drift&lt;/code&gt; (checking the ERD against a real database), are Pro features ($9/mo). The other 33 tools, including everything above, work on the free plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;If your schema work is entirely human and entirely solo, an MCP server is a nice-to-have. The moment either stops being true — an agent writes your migrations, or more than one person names columns — the question becomes how the standard gets enforced. Our answer: make the correct name computable, then let humans, CI, and agents all be checked against the same computation. &lt;a href="https://app.sqemo.com" rel="noopener noreferrer"&gt;Try it in the app&lt;/a&gt; (no signup), or point your agent at &lt;code&gt;npx sqemo-mcp&lt;/code&gt; and ask it to import your schema. If you'd rather see the whole thing end to end first, the &lt;a href="https://sqemo.com/docs/ai-walkthrough" rel="noopener noreferrer"&gt;walkthrough&lt;/a&gt; goes from an empty project to standards-compliant DDL — including the case most tools get wrong, a post that replies to another post. It's also a &lt;a href="https://youtu.be/MqHWtiHfBI0" rel="noopener noreferrer"&gt;three-and-a-half-minute video&lt;/a&gt;, if reading is not your thing today.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is an ERD MCP server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An MCP (Model Context Protocol) server that exposes your entity-relationship diagram to AI agents as typed tools — list entities, add an attribute, import SQL, export DBML, check a name against the naming standard — so the agent reads and edits the real model instead of guessing the schema from context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which AI clients work with sqemo-mcp?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any MCP-capable client that supports local stdio servers: Claude Code, Claude Desktop, Codex CLI, Cursor, and others. It runs via npx (Node.js 22 or later) with a one-line mcpServers entry; no separate install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the Sqemo MCP server free?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. 33 of its 36 tools, including import, export, naming generation, and lint, work on the free plan with no account for local .erd.json files. Three tools are Pro ($9/month): export_alter_sql, introspect_db, and check_db_drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it a hosted (remote) MCP server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not yet. sqemo-mcp is a local stdio server that runs on your machine via npx. Clients that only support hosted HTTPS MCP endpoints can't connect today; remote MCP is on the roadmap.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sqemo.com/blog/erd-mcp-server" rel="noopener noreferrer"&gt;sqemo.com&lt;/a&gt;. Sqemo is an ERD tool that enforces database naming conventions — free in the browser, no signup.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>database</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
