Skip to main content

Column Types

Type Mapping

Scalar Types

Proto TypeGo TypeSQL TypeColumn Constructor
int32int32INTEGERIntegerColumn
int64int64BIGINTBigIntColumn
uint32uint32INTEGERIntegerColumn
uint64uint64BIGINTBigIntColumn
floatfloat32REALRealColumn
doublefloat64DOUBLE PRECISIONDoubleColumn
boolboolBOOLEANBooleanColumn
stringstringTEXTTextColumn
bytes[]byteBYTEAByteaColumn

Well-Known Types

Proto TypeGo TypeSQL TypeColumn Constructor
google.protobuf.Timestamptime.TimeTIMESTAMPTZTimestamptzColumn
google.protobuf.Durationtime.DurationINTERVALIntervalColumn
google.protobuf.Structjson.RawMessageJSONBJSONBColumn

Wrapper Types (Nullable)

Wrapper types map to nullable columns:

Proto TypeGo TypeSQL TypeColumn Constructor
google.protobuf.Int32Value*int32INTEGER NULLNullIntegerColumn
google.protobuf.Int64Value*int64BIGINT NULLNullBigIntColumn
google.protobuf.UInt32Value*uint32INTEGER NULLNullIntegerColumn
google.protobuf.UInt64Value*uint64BIGINT NULLNullBigIntColumn
google.protobuf.FloatValue*float32REAL NULLNullRealColumn
google.protobuf.DoubleValue*float64DOUBLE PRECISION NULLNullDoubleColumn
google.protobuf.BoolValue*boolBOOLEAN NULLNullBooleanColumn
google.protobuf.StringValue*stringTEXT NULLNullTextColumn
google.protobuf.BytesValue[]byteBYTEA NULLNullByteaColumn

Array Types (repeated)

Repeated scalar fields map to PostgreSQL array columns:

Proto TypeGo TypeSQL TypeColumn Constructor
repeated string[]stringTEXT[]TextArrayColumn
repeated int32[]int32INTEGER[]IntegerArrayColumn
repeated int64[]int64BIGINT[]BigIntArrayColumn
repeated bool[]boolBOOLEAN[]BooleanArrayColumn
repeated float[]float32REAL[]RealArrayColumn
repeated double[]float64DOUBLE PRECISION[]DoublePrecisionArrayColumn
repeated bytes[][]byteBYTEA[]ByteaArrayColumn
message Product {
repeated string tags = 5; // → TEXT[] NOT NULL
repeated int64 category_ids = 6; // → BIGINT[] NOT NULL
}

Enum Types

Protobuf enum fields are stored as TEXT in PostgreSQL. Ratel automatically converts enum values to their string names for storage and back to enum values when reading.

Proto TypeScanner TypeSQL TypeColumn Constructor
enumstringTEXTTextColumn
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_NEW = 1;
ORDER_STATUS_PAID = 2;
}

message Order {
OrderStatus status = 3; // → TEXT NOT NULL, Scanner field: string
}

The generated IntoPlain() converts enum to string (pb.Status.String()), and IntoPb() converts back (OrderStatus_value[p.Status]). Unknown string values map to the zero enum value (typically UNSPECIFIED).

No enum_as_string annotation is needed — protoc-gen-ratel enables this automatically for all enum fields in ratel tables.

Oneof (Embedded)

Protobuf oneof fields with (goplain.oneof).embed = true are flattened into the Scanner struct. Each variant becomes a separate column, plus a discriminator _case column.

For message variants, use (goplain.field).serialize = true to store them as JSONB (serialized via protojson.Marshal/protojson.Unmarshal). All oneof columns are nullable since only one variant is active at a time.

message CreateAction {
option (goplain.message).generate = true;
string created_name = 1;
}

message DeleteAction {
option (goplain.message).generate = true;
int64 deleted_id = 1;
string reason = 2;
}

message AuditLog {
option (goplain.message).generate = true;
option (ratel.table) = { generate: true, table_name: "audit_logs" };

int64 id = 1;
string action = 2;

oneof detail {
option (goplain.oneof).embed = true;
CreateAction create_action = 10 [(goplain.field).serialize = true];
DeleteAction delete_action = 11 [(goplain.field).serialize = true];
}
}

Generated columns:

ColumnSQL TypeScanner TypeNotes
create_action_create_actionJSONB NULL[]byteSerialized as JSON via protojson
delete_action_delete_actionJSONB NULL[]byteSerialized as JSON via protojson
detail_caseTEXTstring"create_action" or "delete_action"

The generated IntoPlain() serializes the active variant and sets DetailCase. IntoPb() deserializes based on DetailCase and reconstructs the oneof wrapper. Inactive variants produce []byte{} (empty, not nil) in the Scanner.

Serial Types

Primary key fields with integer types automatically use serial variants:

Proto TypeWith primary_key: trueSQL Type
int32YesSERIAL
int64YesBIGSERIAL

Nullability

Regular proto3 fields generate NOT NULL columns. A column is nullable only when:

  1. The field uses a wrapper type (Int64Value, StringValue, etc.)
  2. The field uses proto3 optional keyword
message User {
// NOT NULL — regular proto3 field
string email = 1;

// NULL — wrapper type
google.protobuf.StringValue bio = 2;

// NULL — optional keyword
optional string nickname = 3;
}

Generated DDL:

"email" text NOT NULL,
"bio" text NULL,
"nickname" text NULL

Type Aliases

Define custom types that map to scalar types:

// EntityID resolves to int64 → BIGINT
message EntityID {
option (goplain.message) = { generate: true, type_alias: "int64" };
}

message User {
EntityID id = 1 [(ratel.column) = {
constraints: { primary_key: true }
}];
// Generates: BIGSERIAL PRIMARY KEY
}

Type Conversion (Casters)

Protobuf message types don't map directly to Go/SQL types. Ratel uses a caster system to convert between protobuf and Scanner representations at runtime.

How It Works

The conversion pipeline is: Proto MessageScanner StructDatabase.

For each well-known type, ratel registers a pair of existing casters — functions that convert in both directions. These are called automatically inside IntoPlain() / IntoPb() methods generated by protoc-gen-go-plain.

Built-in Casters

Proto TypeScanner TypeToPlainToPb
*timestamppb.Timestamptime.TimeTimestampToTimeTimeToTimestamp
*timestamppb.Timestamp*time.TimeTimestampToNullableTimeNullableTimeToTimestamp
*durationpb.Durationtime.DurationDurationPbToDurationDurationToDurationPb
*durationpb.Duration*time.DurationDurationPbToNullableDurationNullableDurationToDurationPb
*wrapperspb.Int64Valueint64Int64ValueToInt64Int64ToInt64Value
*structpb.Structjson.RawMessageStructToRawMessageRawMessageToStruct

All functions are in the github.com/yaroher/ratel/pkg/ratelcast package.

Existing vs Parameter Casters

There are two kinds of casters:

  1. Existing casters — pre-defined functions registered in protoc-gen-ratel. When both directions (ToPlain and ToPb) have existing casters, the generated IntoPlain() / IntoPb() methods call them directly with no extra parameters:

    // Generated code (no caster parameter needed):
    func (pb *User) IntoPlain() *UserScanner {
    p.CreatedAt = ratelcast.TimestampToTime(pb.CreatedAt)
    // ...
    }

    var UserConverter = repository.Converter[*UserScanner, *User]{
    ToScanner: (*User).IntoPlain,
    ToProto: (*UserScanner).IntoPb,
    }
  2. Parameter casters — when a field has a type override without a registered existing caster, IntoPlain / IntoPb get a *Casters struct parameter. The generated Converter becomes a function instead of a variable:

    // Generated code (caster parameter needed):
    func (pb *Widget) IntoPlain(c *WidgetScannerCasters) *WidgetScanner { ... }

    func WidgetConverterWith(c *WidgetScannerCasters) repository.Converter[*WidgetScanner, *Widget] {
    return repository.Converter[*WidgetScanner, *Widget]{
    ToScanner: func(pb *Widget) *WidgetScanner { return pb.IntoPlain(c) },
    ToProto: func(p *WidgetScanner) *Widget { return p.IntoPb(c) },
    }
    }

All built-in type overrides (Timestamp, Duration, Int64Value, Struct) have existing casters, so the standard flow produces simple Converter variables with no extra ceremony.

Serialized Fields

For message fields with (goplain.field).serialize = true, the value is stored as []byte in the Scanner using proto.Marshal / proto.Unmarshal. This works for any protobuf message type, including fields inside embedded messages:

message ThemeSettings {
ui.Theme selected_ui_theme = 1 [(goplain.field).serialize = true];
}

Generated Scanner field: SelectedUiTheme []byte, stored as BYTEA in PostgreSQL.

Virtual Columns

Virtual columns are database columns without a corresponding proto field. They exist in the DB schema and Scanner, but not in the protobuf message. Common use cases: password_hash, audit timestamps managed by triggers, computed fields.

Declaration

Define virtual columns in ratel.table.virtual_columns:

message User {
option (ratel.table) = {
generate: true
table_name: "users"
virtual_columns: [
{ sql_name: "password_hash", sql_type: "TEXT" },
{ sql_name: "db_created_at", sql_type: "TIMESTAMPTZ",
constraints: { default_value: "now()" } }
]
};

int64 id = 1;
string email = 2;
}

No need to declare anything in goplain.message — ratel injects virtual fields into the Scanner automatically.

What Gets Generated

Virtual columns are full participants in the generated code:

FeatureIncludedNotes
Scanner struct fieldYesPasswordHash string
With* chainable setterYesscanner.WithPasswordHash(v)
Column constantYesUserColumnPasswordHash
Table struct accessorYesUsers.PasswordHash
DDL (CREATE TABLE)YesWith type, constraints
GetTarget / GetSetter / GetValueYes
AllSetters()Yes
IntoPlain() / IntoPb()SkippedNo proto field to convert

Usage

// Convert proto to scanner, set virtual field, insert
scanner := user.IntoPlain().WithPasswordHash(hashedPassword)
insert := tbl.Insert().From(scanner.AllSetters()...)

// Or set after conversion
scanner := user.IntoPlain()
scanner.PasswordHash = hashedPassword

SQL Type Mapping

Virtual column sql_type maps to Scanner Go type:

SQL TypeGo Type in Scanner
TEXT, VARCHARstring
BIGINT, INT8int64
INTEGER, INT4int32
BOOLEAN, BOOLbool
REAL, FLOAT4float32
DOUBLE PRECISIONfloat64
BYTEA[]byte
Everything else (TIMESTAMPTZ, JSONB, UUID, ...)string

AllSetters

Every Scanner gets an AllSetters() method that returns all column values as setters, ready for Insert().From() or Update().Set():

// Instead of manually listing every field:
insert := tbl.Insert().From(
tbl.Id.Set(id),
tbl.Email.Set(email),
tbl.Name.Set(name),
)

// Use AllSetters:
insert := tbl.Insert().From(scanner.AllSetters()...)

AllSetters() includes all columns — both regular and virtual. It returns []set.ValueSetter[ColumnAlias].

Available Column Constructors (Go)

// Integer types
schema.SmallIntColumn[C](alias, opts...)
schema.IntegerColumn[C](alias, opts...)
schema.BigIntColumn[C](alias, opts...)
schema.SerialColumn[C](alias, opts...)
schema.SmallSerialColumn[C](alias, opts...)
schema.BigSerialColumn[C](alias, opts...)

// Floating point
schema.RealColumn[C](alias, opts...)
schema.DoubleColumn[C](alias, opts...)
schema.NumericColumn[C](alias, precision, scale, opts...)

// Text
schema.TextColumn[C](alias, opts...)
schema.VarcharColumn[C](alias, length, opts...)
schema.CharColumn[C](alias, length, opts...)

// Date/Time
schema.DateColumn[C](alias, opts...)
schema.TimeColumn[C](alias, opts...)
schema.TimestampColumn[C](alias, opts...)
schema.TimestamptzColumn[C](alias, opts...)

// Other
schema.BooleanColumn[C](alias, opts...)
schema.UUIDColumn[C](alias, opts...)
schema.JSONColumn[C](alias, opts...)
schema.JSONBColumn[C](alias, opts...)
schema.ByteaColumn[C](alias, opts...)

Every type has a Null variant (e.g., NullBigIntColumn) for nullable columns.