Ent ORM

SkillDatabases & data

Lets 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.

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 creates jsonb, and with SchemaType(map[string]string{dialect.Postgres: "text[]"}) it changes DDL without changing the runtime encode/decode path for field.Strings.
  • Prefer field.Other(..., pq.StringArray{}).SchemaType(map[string]string{dialect.Postgres: "text[]"}) for native Postgres text[] encode/decode with Ent methods (create/query/update). Import github.com/lib/pq in the schema file.

Custom Selects & Joins

  • For custom Ent selects/joins, prefer the generated selected-value parsing helpers from the entselectedparse extension (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)

Common Patterns

  • Soft-delete unique indexes include deleted_at in the unique constraint (e.g., index.Fields("namespace", "key", "deleted_at").Unique()) — always filter with Where(<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 with StorageKey(edge.Symbol("...")) and shorten index names with StorageKey("...") before generating migrations.
  • JSONB fields use entutils.JSONStringValueScanner — see openmeter/ent/schema/llmcostprice.go.
  • Non-empty strings at the DB layer: field.String(...).NotEmpty() enforces Ent-side validation, but Atlas may still diff only SET NOT NULL for existing tables. If the database must reject empty strings too, add an explicit entsql.Checks(...) annotation in the schema or mixin alongside NotEmpty().
  • 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 generated Update<Field>() method for that field after UpdateNewValues() (for example UpdateDescription() or UpdateDeletedAt()). This lets Ent use the excluded insert value, including NULL, instead of leaving the old value untouched. See billing adapter upserts such as openmeter/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's GoType is already a pointer (e.g. field.String(...).GoType(&productcatalog.UnitConfig{}).ValueScanner(...)), Ent does not generate a SetNillable<Field> variant — Set<Field> itself takes the pointer. On create paths, guard with if value != nil { create = create.Set<Field>(value) } (see unit_config in openmeter/billing/charges/usagebased/adapter/charge.go and applied_unit_config in openmeter/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