Nomes de coluna (snake_case no banco, camelCase no código)¶
Manter a convenção do SQL no banco sem deixar ela vazar para o domínio TypeScript.
O problema¶
snake_case é a convenção universal em SQL. camelCase é a convenção universal
em TypeScript. Se o nome da coluna for sempre o nome da propriedade, você é
forçado a escolher entre duas coisas ruins:
- Propriedade em
snake_case— funciona, masInferModelpassa a render{ consumer_name: string, rate_limit_burst: number }, e esse tipo circula por service, controller e schema de resposta. A convenção do banco vaza até a borda HTTP. - Renomear as colunas do banco — quebra a convenção SQL, exige aspas em toda query manual e, num banco em produção, é migração de renomeação em tabela quente.
O SQLAlchemy resolve com mapped_column("consumer_name"), o Django com
db_column, o Prisma com @map. Aqui são duas formas.
Por coluna — .name()¶
import { Model, column } from "tempest-db-js";
class ApiKey extends Model {
static tablename = "api_keys";
id = column.integer().primaryKey();
consumerName = column.text().name("consumer_name").notNull(); // (1)!
rateLimitBurst = column.integer().name("rate_limit_burst").notNull();
}
- A propriedade continua
consumerNameno TypeScript; a coluna éconsumer_nameno banco.
Por tabela — static naming¶
Quando o schema inteiro segue uma convenção só, anotar coluna por coluna é ruído:
class ApiKey extends Model {
static tablename = "api_keys";
static naming = "snake_case"; // (1)!
id = column.integer().primaryKey();
consumerName = column.text().notNull(); // -> consumer_name
rateLimitBurst = column.integer().notNull(); // -> rate_limit_burst
}
- Os valores são
"preserve"(o padrão — nome da propriedade, verbatim) e"snake_case".
.name() continua valendo e vence a estratégia da tabela, para a exceção que
todo schema real tem:
class ApiKey extends Model {
static tablename = "api_keys";
static naming = "snake_case";
consumerName = column.text().notNull(); // -> consumer_name
legacyId = column.text().name("legacyID").notNull(); // -> legacyID
}
O mapeamento vale em todo lugar¶
Não é um detalhe do SELECT. O nome traduzido aparece em toda cláusula que chega
ao SQL, e o nome da propriedade em tudo que volta para o TypeScript:
select(ApiKey, ["consumerName"])
.where({ consumerName: { ieq: "acme" } })
.orderBy("rateLimitBurst", "desc");
// SELECT "consumer_name" FROM "api_keys"
// WHERE lower("consumer_name") = lower($1) ORDER BY "rate_limit_burst" DESC
const row = await session.execute(select(ApiKey).where({ consumerName: "acme" })).one();
row.consumerName; // ✅ propriedade, não "consumer_name"
Cobertura completa: select (projeção, where, orderBy, groupBy,
agregações), insert (colunas, ON CONFLICT target e predicado, returning),
update (set e where), del, joins (qualificação "alias"."coluna"),
BaseRepository, active-record, e o IR das migrações.
Sem drift falso
O IR de migração é gerado em espaço de nome de coluna, igual ao que a
introspecção lê do banco. Por isso checkDriftPostgres continua limpo — se o
mapeamento só valesse na query, toda coluna renomeada apareceria como
"missing from the database".
Colisão é erro, não última-escrita-vence
Duas propriedades que resolvem para a mesma coluna levantam erro na primeira reflexão do modelo:
Custo¶
Um modelo que não renomeia nada tem mapa null, e o compilador nem consulta —
o caminho quente fica idêntico ao de antes. O mapa é memoizado por classe, como o
resto da reflexão de modelo.
Recap¶
.name("coluna")renomeia uma coluna;static naming = "snake_case"renomeia a tabela toda..name()sobrescreve a estratégia da tabela.- O mapeamento cobre query, mutação, join, repository e o IR de migração — sem drift falso.
- A linha que volta é sempre em nome de propriedade.
- Colisão de nomes falha alto.