Skip to content

Commit 39e7a64

Browse files
committed
gnd: Support auto-increment IDs for Int8 and Bytes in codegen
For entity types with `id: Int8!` or `id: Bytes!`, the generated constructor now has an optional id parameter. When the id is not provided, the save() method uses "auto" as the store key to trigger auto-incrementing behavior in graph-node. This is strictly an enhancement of the functionality in latest graph-cli (version 0.98.1) Over time, there has been quite a bit of confusion about the values that get passed to these constructors. In older versions, the constructors accepted strings instead of Int8 or Bytes values. - Int8 uses `i64.MIN_VALUE` as sentinel (primitive can't be nullable) - Bytes uses `Bytes | null = null` (reference type) - String IDs remain unchanged (required parameter, no auto-increment)
1 parent 1e0ea76 commit 39e7a64

File tree

3 files changed

+309
-21
lines changed

3 files changed

+309
-21
lines changed

gnd/src/codegen/schema.rs

Lines changed: 258 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,38 @@ impl IdFieldKind {
140140
_ => IdFieldKind::String,
141141
}
142142
}
143+
144+
/// Returns true if this ID type supports auto-increment (optional constructor arg).
145+
pub fn supports_auto(&self) -> bool {
146+
matches!(self, IdFieldKind::Int8 | IdFieldKind::Bytes)
147+
}
148+
149+
/// Get the constructor parameter type (with nullability for auto-increment types).
150+
pub fn constructor_param_type(&self) -> &'static str {
151+
match self {
152+
IdFieldKind::String => "string",
153+
IdFieldKind::Bytes => "Bytes | null",
154+
IdFieldKind::Int8 => "i64", // primitive, can't be nullable
155+
}
156+
}
157+
158+
/// Get the default value for constructor parameter (for auto-increment types).
159+
pub fn constructor_default(&self) -> Option<&'static str> {
160+
match self {
161+
IdFieldKind::String => None,
162+
IdFieldKind::Bytes => Some("null"),
163+
IdFieldKind::Int8 => Some("i64.MIN_VALUE"),
164+
}
165+
}
166+
167+
/// Get the condition to check if id was provided (not auto-increment).
168+
pub fn auto_check_condition(&self) -> &'static str {
169+
match self {
170+
IdFieldKind::String => "", // N/A
171+
IdFieldKind::Bytes => "id !== null",
172+
IdFieldKind::Int8 => "id != i64.MIN_VALUE",
173+
}
174+
}
143175
}
144176

145177
/// Get the base type name from a GraphQL type (stripping NonNull and List wrappers).
@@ -365,23 +397,76 @@ impl SchemaCodeGenerator {
365397
}
366398

367399
fn generate_constructor(&self, id_kind: &IdFieldKind) -> Method {
368-
Method::new(
369-
"constructor",
370-
vec![Param::new("id", NamedType::new(id_kind.type_name()))],
371-
None,
372-
format!(
373-
r#"
400+
if id_kind.supports_auto() {
401+
// For Int8 and Bytes, make id optional to support auto-increment
402+
let param_type = id_kind.constructor_param_type();
403+
let default_value = id_kind.constructor_default().unwrap();
404+
let check_condition = id_kind.auto_check_condition();
405+
406+
Method::new(
407+
"constructor",
408+
vec![Param::with_default(
409+
"id",
410+
TypeExpr::Raw(param_type.to_string()),
411+
default_value,
412+
)],
413+
None,
414+
format!(
415+
r#"
416+
super()
417+
if ({}) {{
418+
this.set('id', {})
419+
}}"#,
420+
check_condition,
421+
id_kind.value_from()
422+
),
423+
)
424+
.with_doc("Leaving out the id argument uses an autoincrementing id.")
425+
} else {
426+
// For String IDs, keep the existing behavior (required parameter)
427+
Method::new(
428+
"constructor",
429+
vec![Param::new("id", NamedType::new(id_kind.type_name()))],
430+
None,
431+
format!(
432+
r#"
374433
super()
375434
this.set('id', {})"#,
376-
id_kind.value_from()
377-
),
378-
)
435+
id_kind.value_from()
436+
),
437+
)
438+
}
379439
}
380440

381441
fn generate_store_methods(&self, entity_name: &str, id_kind: &IdFieldKind) -> Vec<StoreMethod> {
382-
vec![
383-
// save() method
384-
StoreMethod::Regular(Method::new(
442+
// Generate save() method - different for auto-increment vs string IDs
443+
let save_method = if id_kind.supports_auto() {
444+
// For Int8 and Bytes, check if id is null/unset and use "auto" as the key
445+
Method::new(
446+
"save",
447+
vec![],
448+
Some(NamedType::new("void").into()),
449+
format!(
450+
r#"
451+
let id = this.get('id')
452+
if (id == null || id.kind == ValueKind.NULL) {{
453+
store.set('{}', 'auto', this)
454+
}} else {{
455+
assert(id.kind == {},
456+
`Entities of type {} must have an ID of type {} but the id '${{id.displayData()}}' is of type ${{id.displayKind()}}`)
457+
store.set('{}', {}, this)
458+
}}"#,
459+
entity_name,
460+
id_kind.value_kind(),
461+
entity_name,
462+
id_kind.gql_type_name(),
463+
entity_name,
464+
id_kind.value_to_string()
465+
),
466+
)
467+
} else {
468+
// For String IDs, keep the existing behavior (require ID)
469+
Method::new(
385470
"save",
386471
vec![],
387472
Some(NamedType::new("void").into()),
@@ -402,7 +487,11 @@ impl SchemaCodeGenerator {
402487
entity_name,
403488
id_kind.value_to_string()
404489
),
405-
)),
490+
)
491+
};
492+
493+
vec![
494+
StoreMethod::Regular(save_method),
406495
// loadInBlock() static method
407496
StoreMethod::Static(StaticMethod::new(
408497
"loadInBlock",
@@ -710,6 +799,162 @@ mod tests {
710799
assert_eq!(IdFieldKind::Int8.type_name(), "i64");
711800
}
712801

802+
#[test]
803+
fn test_auto_increment_support() {
804+
// String IDs don't support auto-increment
805+
assert!(!IdFieldKind::String.supports_auto());
806+
assert!(IdFieldKind::String.constructor_default().is_none());
807+
808+
// Int8 IDs support auto-increment
809+
assert!(IdFieldKind::Int8.supports_auto());
810+
assert_eq!(IdFieldKind::Int8.constructor_param_type(), "i64");
811+
assert_eq!(
812+
IdFieldKind::Int8.constructor_default(),
813+
Some("i64.MIN_VALUE")
814+
);
815+
assert_eq!(
816+
IdFieldKind::Int8.auto_check_condition(),
817+
"id != i64.MIN_VALUE"
818+
);
819+
820+
// Bytes IDs support auto-increment
821+
assert!(IdFieldKind::Bytes.supports_auto());
822+
assert_eq!(IdFieldKind::Bytes.constructor_param_type(), "Bytes | null");
823+
assert_eq!(IdFieldKind::Bytes.constructor_default(), Some("null"));
824+
assert_eq!(IdFieldKind::Bytes.auto_check_condition(), "id !== null");
825+
}
826+
827+
#[test]
828+
fn test_int8_id_auto_increment_codegen() {
829+
let schema = r#"
830+
type Counter @entity {
831+
id: Int8!
832+
value: BigInt!
833+
}
834+
"#;
835+
let doc = parse_schema::<String>(schema).unwrap();
836+
let gen = SchemaCodeGenerator::new(&doc).unwrap();
837+
838+
let classes = gen.generate_types(true);
839+
assert_eq!(classes.len(), 1);
840+
841+
let counter = &classes[0];
842+
let output = counter.to_string();
843+
844+
// Constructor should have optional id with sentinel default
845+
assert!(
846+
output.contains("constructor(id: i64 = i64.MIN_VALUE)"),
847+
"Int8 ID constructor should have default sentinel value, got: {}",
848+
output
849+
);
850+
851+
// Constructor should have doc comment
852+
assert!(
853+
output.contains("Leaving out the id argument uses an autoincrementing id"),
854+
"Constructor should have auto-increment doc comment"
855+
);
856+
857+
// Constructor body should conditionally set id
858+
assert!(
859+
output.contains("if (id != i64.MIN_VALUE)"),
860+
"Constructor should check for sentinel value"
861+
);
862+
863+
// save() method should use "auto" when id is null
864+
assert!(
865+
output.contains("store.set('Counter', 'auto', this)"),
866+
"save() should use 'auto' key for auto-increment, got: {}",
867+
output
868+
);
869+
}
870+
871+
#[test]
872+
fn test_bytes_id_auto_increment_codegen() {
873+
let schema = r#"
874+
type Event @entity {
875+
id: Bytes!
876+
data: String!
877+
}
878+
"#;
879+
let doc = parse_schema::<String>(schema).unwrap();
880+
let gen = SchemaCodeGenerator::new(&doc).unwrap();
881+
882+
let classes = gen.generate_types(true);
883+
assert_eq!(classes.len(), 1);
884+
885+
let event = &classes[0];
886+
let output = event.to_string();
887+
888+
// Constructor should have nullable id with null default
889+
assert!(
890+
output.contains("constructor(id: Bytes | null = null)"),
891+
"Bytes ID constructor should have null default, got: {}",
892+
output
893+
);
894+
895+
// Constructor should have doc comment
896+
assert!(
897+
output.contains("Leaving out the id argument uses an autoincrementing id"),
898+
"Constructor should have auto-increment doc comment"
899+
);
900+
901+
// Constructor body should conditionally set id
902+
assert!(
903+
output.contains("if (id !== null)"),
904+
"Constructor should check for null"
905+
);
906+
907+
// save() method should use "auto" when id is null
908+
assert!(
909+
output.contains("store.set('Event', 'auto', this)"),
910+
"save() should use 'auto' key for auto-increment, got: {}",
911+
output
912+
);
913+
}
914+
915+
#[test]
916+
fn test_string_id_no_auto_increment() {
917+
let schema = r#"
918+
type User @entity {
919+
id: ID!
920+
name: String!
921+
}
922+
"#;
923+
let doc = parse_schema::<String>(schema).unwrap();
924+
let gen = SchemaCodeGenerator::new(&doc).unwrap();
925+
926+
let classes = gen.generate_types(true);
927+
assert_eq!(classes.len(), 1);
928+
929+
let user = &classes[0];
930+
let output = user.to_string();
931+
932+
// Constructor should have required id (no default)
933+
assert!(
934+
output.contains("constructor(id: string)"),
935+
"String ID constructor should have required parameter, got: {}",
936+
output
937+
);
938+
939+
// Constructor should NOT have auto-increment doc comment
940+
assert!(
941+
!output.contains("autoincrementing"),
942+
"String ID should not mention auto-increment"
943+
);
944+
945+
// save() method should NOT use "auto"
946+
assert!(
947+
!output.contains("'auto'"),
948+
"String ID save() should not use 'auto' key"
949+
);
950+
951+
// save() should require ID
952+
assert!(
953+
output.contains("Cannot save User entity without an ID"),
954+
"String ID save() should require ID"
955+
);
956+
}
957+
713958
#[test]
714959
fn test_entity_reference() {
715960
let schema = r#"

0 commit comments

Comments
 (0)