@@ -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