Naming
The field name is the TypeScript name. Casing or a name option sets the SQL name.
Names are the SQL spelling PostgreSQL stores, and casing rewrites a name you did not set. The names section sets a SQL name, a previous name, or a comment. The casing section chooses snake case or camel case.
Names
Where this fits
TypeScript sees the field name. Names are the SQL name, a rename that keeps the old column, the comment, and casing. Visibility decides who can read or write it.
Insert, update, and where use the field name. The SQL name is what the statement quotes.
SQL name
The SQL name is the field name, unless you replace it. Casing "snake" and "camel" rewrite a name you did not set. publishedAt is published_at. userID is user_id. Casing does not rewrite a name you set.
| Call | Result |
|---|---|
.sqlName("headline") | That column is stored as headline. |
sqlName: "note" | The table is stored as note. |
column
.sqlName() replaces one column. Insert still uses the field name.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text().sqlName("headline"),
});
const app = schema({ tables: [notes] });
export async function add(url: string, title: string) {
await using db = connect(url, { schema: app });
return await db.notes.insert({ title });
}table
sqlName on the table replaces the table name. The client property stays notes.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const notes = table(
"notes",
{ id: t.identity(), title: t.text() },
{ sqlName: "note" },
);
const app = schema({ tables: [notes] });
export async function add(url: string, title: string) {
await using db = connect(url, { schema: app });
return await db.notes.insert({ title });
}OKM1060
An empty column or table sqlName is refused.
OKM1122
A reserved word, a control character, or a name longer than 63 bytes is refused.
OKM1023
Two tables that store the same SQL name are refused. The message lists the table names. Two names longer than 63 bytes that shorten to the same stored name and hash suffix are refused the same way.
Previous name
A migration plans ALTER TABLE … RENAME TO or RENAME COLUMN. Renaming an identity column also renames the sequence {table}_{column}_seq. A table rename does the same for each identity column. Dropping one name and adding another, with no renamedFrom, is refused and the fix names the line to add. A previous name that the last snapshot does not contain is refused. Remove renamedFrom once the migration that used it has been applied. With no snapshot, that check does not run.
| Call | Result |
|---|---|
.renamedFrom("headline") | The previous column SQL name. |
renamedFrom: "posts" | The previous table SQL name. |
column
.renamedFrom() is the previous column.
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.identity(),
title: t.text().renamedFrom("headline"),
});table
renamedFrom on the table is the previous table name.
import { table, t } from "okmodel/pg";
export const notes = table(
"notes",
{ id: t.identity(), title: t.text() },
{ renamedFrom: "posts" },
);OKM1060
An empty column or table renamedFrom is refused.
OKM1020
Table renamedFrom … is not in the previous snapshot. A column message names the table and the previous column.
OKM1530
Drop headline and add title could be a rename. The fix is .renamedFrom("headline") on the new column, or renamedFrom: "posts" on the new table.
Comment
.comment() and a table comment are checked. The catalog has no comment field, so neither is emitted. Omit the call to leave it off. An empty table comment is refused. An empty column comment is accepted.
column
.comment() is checked on that column and not stored.
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.identity(),
title: t.text().comment("Shown in the list"),
});table
A table comment is checked and not stored.
import { table, t } from "okmodel/pg";
export const notes = table(
"notes",
{ id: t.identity(), title: t.text() },
{ comment: "One row per note" },
);OKM1060
An empty table comment is refused.
OKM1122
A comment that contains a control character is refused.
Casing
casing is the SQL spelling of a name you did not set. Insert, update, and where still use the field name. The client name stays the name you passed to table().
| Value | Result |
|---|---|
| omitted | The SQL name is the field name, and the table name. |
"snake" | Words are lowercased and joined with _. A run of two or more capitals is one word. A single capital is its own word. |
"camel" | The first word is lowercase and the others start with a capital. |
| any other value | Refused. |
omitted
Leave casing out and Postgres stores the name you wrote.
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
publishedAt: t.timestamptz().nullable(),
});
export default schema({ tables: [notes] });snake
"snake" rewrites a table name and a field. A run of capitals is one word only when it has two or more letters, and a single capital is its own word. userID is user_id, getURLs is get_urls, and HTMLParser is html_parser. iOS is i_os, IPv6Address is i_pv6_address, and userId2FA is user_id2_fa. A digit stays with the word before it: address2 is address2 and line1Text is line1_text. An underscore at either end is kept, and a letter outside ASCII passes through unchanged.
sqlName and renamedFrom are stored as you wrote them. Indexes, checks, and foreign keys then use that SQL name.
import { schema, table, t } from "okmodel/pg";
const notes = table("publishedNotes", {
id: t.identity(),
publishedAt: t.timestamptz().nullable(),
headline: t.text().sqlName("title"),
});
export default schema({
tables: [notes],
casing: "snake",
});Stored names
The table is published_notes. publishedAt is published_at. headline is title, because sqlName wins.
camel
"camel" splits a name into the same words and joins them as camelCase. user_id is userId, and HTMLParser is htmlParser. iOS is iOs, IPv6Address is iPv6Address, and userId2FA is userId2Fa. publishedNotes and publishedAt stay as written. Use it when the database already spells names that way.
import { schema, table, t } from "okmodel/pg";
const notes = table("publishedNotes", {
id: t.identity(),
publishedAt: t.timestamptz().nullable(),
});
export default schema({
tables: [notes],
casing: "camel",
});pull
okm pull keeps each database name as the TypeScript key. --casing snake or --casing camel is opt-in. The key is the other spelling, so schema({ casing }) can write the database name back. user_id with --casing snake is userId. userId with --casing camel is user_id.
.sqlName() is added when that key does not spell the database name exactly. a_b_c with --casing snake is aBC, and aBC spells a_bc, so the column is aBC: t.text().sqlName("a_b_c"). Each of those names is a line in the pull report.
What casing covers
Casing spells every name you did not set: tables, columns, and the names derived from them (constraints, indexes, identity sequences, policies, and the okm_touch function). Views, enums, domains, functions, and triggers use the name you wrote.
Changing casing
The stored name changes, so a database that already holds the old spelling gets a rename plan, not a silent rewrite. An unexplained change is OKM1530, and the message names casing as the cause. Keep the stored name with sqlName (.sqlName() on a column, sqlName on a table), or declare the rename with .renamedFrom() or renamedFrom.
Refused
OKM1023
Two fields of one table that spell the same SQL name, such as userID and user_id under "snake", are refused with a message that names both. Name one with sqlName.
OKM1122
An empty name, or a name over 63 bytes after casing, is refused at schema().
Any value other than "snake" or "camel" is OKM1060: schema() casing <value> must be snake or camel.
| Next | You use it for |
|---|---|
| Traits | Columns added to one table or to every table. |