From 25d5dfffe4a5d3cc799fbcfdd02343187d2eef60 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 22:56:14 +0300 Subject: [PATCH] orm: fix sqlite with pragma foreign_keys=on error (fixes #11181) --- vlib/orm/README.md | 3 ++- vlib/orm/orm.v | 1 + vlib/orm/orm_references_test.v | 47 ++++++++++++++++++++++++++++++++++ vlib/v/checker/orm.v | 9 +++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/vlib/orm/README.md b/vlib/orm/README.md index 141b9d0de..aae7e27ff 100644 --- a/vlib/orm/README.md +++ b/vlib/orm/README.md @@ -6,7 +6,8 @@ regardless of the DB driver you decide to use. ## Nullable For a nullable column, use an option field. If the field is non-option, the column will be defined -with `NOT NULL` at table creation. +with `NOT NULL` at table creation, except scalar foreign keys declared with `@[references]`, which +default to nullable so omitted relations can be stored as `NULL`. ```v ignore struct Foo { diff --git a/vlib/orm/orm.v b/vlib/orm/orm.v index 694790682..1f8a72141 100644 --- a/vlib/orm/orm.v +++ b/vlib/orm/orm.v @@ -928,6 +928,7 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo } } 'references' { + nullable = true if attr.arg == '' { if field.name.ends_with('_id') { references_table = field.name.trim_right('_id') diff --git a/vlib/orm/orm_references_test.v b/vlib/orm/orm_references_test.v index 6472c2048..38d44eca6 100644 --- a/vlib/orm/orm_references_test.v +++ b/vlib/orm/orm_references_test.v @@ -32,3 +32,50 @@ fn test_references_constraint() { assert pragma_result.len == 3 } + +struct Member { + id int @[primary; sql: serial] + name string +} + +struct Team { + id int @[primary; sql: serial] + name string + member_id int @[references: 'Member(id)'] +} + +fn test_omitted_references_field_inserts_null() { + mut db := sqlite.connect(':memory:')! + defer { + db.close() or {} + } + db.exec('PRAGMA foreign_keys=ON')! + + sql db { + create table Member + create table Team + }! + + team := Team{ + name: 'unassigned' + } + + sql db { + insert team into Team + }! + + rows := db.exec('select member_id from Team where id = 1')! + assert rows.len == 1 + assert rows[0].vals[0] == '' + + invalid_team := Team{ + name: 'invalid' + member_id: 0 + } + + mut failed := false + sql db { + insert invalid_team into Team + } or { failed = true } + assert failed +} diff --git a/vlib/v/checker/orm.v b/vlib/v/checker/orm.v index 43e831363..dc70c86b1 100644 --- a/vlib/v/checker/orm.v +++ b/vlib/v/checker/orm.v @@ -340,9 +340,18 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type { info := table_sym.info as ast.Struct mut fields := c.fetch_and_check_orm_fields(info, node.table_expr.pos, table_sym.name) + mut insert_fields := []ast.StructField{cap: fields.len} for field in fields { c.check_orm_struct_field_attrs(node, field) + // Preserve SQL NULL/default handling for omitted reference fields instead of + // inserting the V zero value and violating foreign key constraints. + if field.attrs.contains('references') + && c.check_field_of_inserting_struct_is_uninitialized(node, field.name) { + continue + } + insert_fields << field } + fields = insert_fields.clone() mut sub_structs := map[int]ast.SqlStmtLine{} non_primitive_fields := c.get_orm_non_primitive_fields(fields) -- 2.39.5