Skip to content

## Bug: V ORM binds u8 as PG "char" instead of smallint #27986

Description

@Jengro777

Minimal Reproduction

1. Model (model.v):

module main
import time

@[table: 'demo']
pub struct Demo {
    id     string    @[primary; sql_type: 'CHAR(36)']
    status u8        @[sql_type: 'smallint']
    label  string    @[sql_type: 'VARCHAR(64)']
    // Audit fields
    created_at time.Time  @[sql_type: 'TIMESTAMP']
    updated_at time.Time  @[sql_type: 'TIMESTAMP']
    del_flag   u8         @[default: 0; sql_type: 'smallint']
    deleted_at ?time.Time @[sql_type: 'TIMESTAMP']
}

2. Init with seed data (main.v):

module main
import db.pg
import orm
import time

fn main() {
    mut db := pg.connect(host: 'localhost', user: 'test', password: 'test', dbname: 'test')!

    // Create table — works fine (DDL uses correct types)
    sql db {
        create table Demo
    }!

    // Insert seed data — FAILS with type mismatch
    t := time.now()
    demo := Demo{
        id: '00000000-0000-0000-0000-000000000001'
        status: 0
        label: 'test'
        created_at: t
        updated_at: t
        del_flag: 0
        deleted_at: none
    }
    sql db {
        insert demo into Demo
    }!
    println('OK')
}

3. Error:

pg exec error:
ERROR:  column "status" is of type smallint but expression is of type "char"

Root Cause

/vlib/db/pg/orm.v:313u8 maps to Oid.t_char (PostgreSQL internal "char" type):

u8 {
    types << u32(Oid.t_char)  // ← should be Oid.t_int2
    vals << &char(&data)
    lens << int(sizeof(u8))
    formats << 1
}

All other unsigned integer types map correctly: u16→int2, u32→int4, u64→int8. Only u8 incorrectly maps to "char".

Expected Behavior

u8 should bind as Oid.t_int2 (smallint), consistent with u16→int2, u32→int4, u64→int8.

Workaround

Add default: 0 to every u8 field (ORM skips fields with default when value == default), but this only works if the seed value equals the default — any non-default value still triggers the bug.

Fix

 u8 {
-    types << u32(Oid.t_char)
-    vals << &char(&data)
-    lens << int(sizeof(u8))
+    types << u32(Oid.t_int2)
+    num := conv.hton16(u16(data))
+    vals << &char(&num)
+    lens << int(sizeof(u16))
     formats << 1
 }

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions