02-custom/

directory
v0.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT

README

02-custom

The DSL cannot express every query. This example adds hand-written SQL and proto next to the generated files, and both halves compile into one typed API.

  • Hand-written custom.sql and custom.proto live beside the generated files and survive regeneration
  • A hand-written service that reuses a generated proto message
  • Virtual fields: Contracts(entlite.PROTO()) gives a field with no column, filled in by the server
  • Choosing the key type per entity: field.Int64("id") on a high volume table
  • A foreign key follows the type of the entity it points at
  • Query level Contracts(): a query that stays in the database layer and gets no rpc
  • Two entities in one schema
  • A multi-word entity name: SensorReading becomes the table sensor_reading, which sqlc reads back as the Go type SensorReading

Entities

sqlite/ent/schema has two:

  • Sensor — a device in the field. Also declares latest_value as Contracts(entlite.PROTO()). That is a virtual field: no column, no place in any generated SQL, but it is in the proto message. The server fills it in.
  • SensorReading — a measurement from a sensor. Declares its own key as field.Int64("id") instead of taking the default int32. Readings are high volume and int32 stops at 2.1B rows. sensor_id stays field.Int because it points at Sensor's int32 key.

SensorReading's Update() is Contracts(entlite.SQLC()). A reading is a recorded fact, so clients never edit it. The database query exists, the rpc does not.

Hand-written files

entlite only writes fixed filenames, so anything else in these folders stays.

Server

sqlite/server/server.go serves all three services from the same generated types. ListWithLatestReading is where the two halves meet: the custom LEFT JOIN returns an embedded sensor row, the generated converter turns it into the same pb.Sensor the CRUD service returns, and latest_value is filled from the joined reading.

int64 key, end to end

The key type travels the whole stack. Proto gets int64 id. SQLite columns are already 64-bit, so the wrapper drops the narrowing convert it emits for int32 keys — compare GetSensorReadingById with GetSensorById in sqlite/ent/gen/db/queries.sql.go.

On the wire an int64 is JSON encoded as a string, so a reading is {"id":"2", ...} and a sensor is {"id":2, ...}. In TypeScript it is a bigint, which is why reading IDs in the frontend use bigIntInput() and not numberInput().

Run

cd sqlite
make run     # serves on :8080

On sqlite filter.Range("recorded_at") emits recorded_at >= @min_recorded_at AND recorded_at <= @max_recorded_at, not BETWEEN: sqlc drops the bounds from the params struct unless the range is the first filter, and the query then binds parameters it never received. custom.sql writes its ranges the same way.

Directories

Path Synopsis
ent

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL