Ent ORM
SkillDatabases & dataLets your agent edit database schemas and fix query or type issues using the Ent ORM.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Ent ORM skill
About this capability
Work with Ent ORM schemas and generated code. Use when modifying ent schemas, debugging ent query issues, or dealing with Postgres type mappings.
What this skill tells your AI
The instructions your AI receives, as published by openmeterio/openmeter in .agents/skills/ent/SKILL.md and read by ahel’s review.
Guidance for working with Ent in the OpenMeter codebase.
Schema Location
- Schema definitions:
openmeter/ent/schema/*.go(source of truth) - Generated code:
openmeter/ent/db/(DO NOT edit manually)
After any schema change, regenerate with make generate before running tests.
Postgres Array Columns (text[])
- Do NOT use
field.Strings(...)for Postgres array columns. Without an explicit schema type it createsjsonb, and withSchemaType(map[string]string{dialect.Postgres: "text[]"})it changes DDL without changing the runtime encode/decode path forfield.Strings. - Prefer
field.Other(..., pq.StringArray{}).SchemaType(map[string]string{dialect.Postgres: "text[]"})for native Postgrestext[]encode/decode with Ent methods (create/query/update). Importgithub.com/lib/pqin the schema file.
Custom Selects & Joins
- For custom Ent selects/joins, prefer the generated selected-value parsing helpers from the
entselectedparseextension (openmeter/ent/db/selectedparse.go) instead of hand-written scanners. - Use
db.Parse<Entity>FromSelectedValues(prefix, row.Value)for aliased joined columns.- Example:
db.ParseLedgerDimensionFromSelectedValues("dimension_", row.Value)
- Example:
Common Patterns
- Soft-delete unique indexes include
deleted_atin the unique constraint (e.g.,index.Fields("namespace", "key", "deleted_at").Unique()) — always filter withWhere(<entity>db.DeletedAtIsNil())in queries. - Foreign keys use
char(26)schema type to match ULID IDs. - Cascade deletes use
entsql.OnDelete(entsql.Cascade)on the parent edge. - PostgreSQL identifier length is 63 bytes by default (PostgreSQL docs, “Lexical Structure” /
NAMEDATALEN). Long Ent-generated table, index, and FK names can truncate and collide even when their full names differ. When a schema/entity/edge name is verbose, proactively shorten generated FK symbols withStorageKey(edge.Symbol("..."))and shorten index names withStorageKey("...")before generating migrations. - JSONB fields use
entutils.JSONStringValueScanner— seeopenmeter/ent/schema/llmcostprice.go. - Non-empty strings at the DB layer:
field.String(...).NotEmpty()enforces Ent-side validation, but Atlas may still diff onlySET NOT NULLfor existing tables. If the database must reject empty strings too, add an explicitentsql.Checks(...)annotation in the schema or mixin alongsideNotEmpty(). - Upserts with nullable/optional fields:
UpdateNewValues()only updates fields that were set on the create mutation. If an upsert must clear a previously set nullable/optional column, explicitly chain the generatedUpdate<Field>()method for that field afterUpdateNewValues()(for exampleUpdateDescription()orUpdateDeletedAt()). This lets Ent use the excluded insert value, includingNULL, instead of leaving the old value untouched. See billing adapter upserts such asopenmeter/billing/adapter/stdinvoicelines.go. - Generated
SetOrClear<Field>helpers: prefer these helpers for nullable/optional update fields when they have a straightforward signature. For awkward generated signatures such as double-pointer JSON fields, prefer the explicit pattern used for fields like normalized metadata:if value != nil { update = update.Set<Field>(value) } else { update = update.Clear<Field>() }. This avoids passing the address of a nil pointer, which can make Ent treat the field as set with a nil value and then panic or fail in generated validators. - No
SetNillable<Field>for pointer GoTypes: when an optional field'sGoTypeis already a pointer (e.g.field.String(...).GoType(&productcatalog.UnitConfig{}).ValueScanner(...)), Ent does not generate aSetNillable<Field>variant —Set<Field>itself takes the pointer. On create paths, guard withif value != nil { create = create.Set<Field>(value) }(seeunit_configinopenmeter/billing/charges/usagebased/adapter/charge.goandapplied_unit_configinopenmeter/billing/adapter/stdinvoicelines.go); leaving the field unset writes NULL on insert and leaves the existing value untouched on conflict-update.
Regeneration
Depending on the change, the generators need to be re-run. For schema changes, edit files under openmeter/ent/schema/ and run the repo generation target:
# Regenerate all generated Go code, including Ent
make generate
During local iteration, you can use the narrower Ent-only command when you intentionally want to avoid a full generation run:
# Regenerate Ent only after schema changes
go generate ./openmeter/ent/...
Signals
- GitHub stars
- 2k
- Forks
- 214
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
ent- Source
- github.com/openmeterio/openmeter