diff --git a/aiprompts/v_advanced/generics.md b/aiprompts/v_advanced/generics.md index e69de29b..7660123f 100644 --- a/aiprompts/v_advanced/generics.md +++ b/aiprompts/v_advanced/generics.md @@ -0,0 +1,64 @@ + +```v + +struct Repo[T] { + db DB +} + +struct User { + id int + name string +} + +struct Post { + id int + user_id int + title string + body string +} + +fn new_repo[T](db DB) Repo[T] { + return Repo[T]{db: db} +} + +// This is a generic function. V will generate it for every type it's used with. +fn (r Repo[T]) find_by_id(id int) ?T { + table_name := T.name // in this example getting the name of the type gives us the table name + return r.db.query_one[T]('select * from ${table_name} where id = ?', id) +} + +db := new_db() +users_repo := new_repo[User](db) // returns Repo[User] +posts_repo := new_repo[Post](db) // returns Repo[Post] +user := users_repo.find_by_id(1)? // find_by_id[User] +post := posts_repo.find_by_id(1)? // find_by_id[Post] + +``` + +Currently generic function definitions must declare their type parameters, but in future V will infer generic type parameters from single-letter type names in runtime parameter types. This is why find_by_id can omit [T], because the receiver argument r uses a generic type T. + +```v +fn compare[T](a T, b T) int { + if a < b { + return -1 + } + if a > b { + return 1 + } + return 0 +} + +// compare[int] +println(compare(1, 0)) // Outputs: 1 +println(compare(1, 1)) // 0 +println(compare(1, 2)) // -1 +// compare[string] +println(compare('1', '0')) // Outputs: 1 +println(compare('1', '1')) // 0 +println(compare('1', '2')) // -1 +// compare[f64] +println(compare(1.1, 1.0)) // Outputs: 1 +println(compare(1.1, 1.1)) // 0 +println(compare(1.1, 1.2)) // -1 +``` + diff --git a/examples/hero/db/psql.vsh b/examples/hero/db/psql.vsh deleted file mode 100755 index 92e10a20..00000000 --- a/examples/hero/db/psql.vsh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run - -import freeflowuniverse.herolib.core -import freeflowuniverse.herolib.clients.postgresql_client -import freeflowuniverse.herolib.core.playbook -import freeflowuniverse.herolib.hero.db.hero_db -import freeflowuniverse.herolib.hero.models.circle -// import freeflowuniverse.herolib.core.playcmds - -// Configure PostgreSQL client -heroscript := " -!!postgresql_client.configure - password:'testpass' - name:'test5' - user: 'testuser' - port: 5432 - host: 'localhost' - dbname: 'testdb' -" -mut plbook := playbook.new(text: heroscript)! -postgresql_client.play(mut plbook)! - -// Get the configured client -mut db_client := postgresql_client.get(name: 'test5')! - -// println(db_client) - -// Check if test database exists, create if not -if !db_client.db_exists('test')! { - println('Creating database test...') - db_client.db_create('test')! -} - -// Switch to test database -db_client.dbname = 'test' - -// Create table if not exists -create_table_sql := 'CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -)' - -println('Creating table users if not exists...') -db_client.exec(create_table_sql)! - -println('Database and table setup completed successfully!') - - -// Create HeroDB for Circle type -mut circle_db := hero_db.new[circle.Circle](db_client) - -println(circle_db) - -if true{panic("sd")} - -circle_db.ensure_table()! - -// Create and save a circle -mut my_circle := circle.Circle{ - name: "Tech Community" - description: "A community for tech enthusiasts" - domain: "tech.example.com" - config: circle.CircleConfig{ - max_members: 1000 - allow_guests: true - auto_approve: false - theme: "modern" - } - status: circle.CircleStatus.active -} - -circle_db.save(&my_circle)! - -// Retrieve the circle -retrieved_circle := circle_db.get_by_index({ - "domain": "tech.example.com" -})! - -// Search circles by status -active_circles := circle_db.search_by_index("status", "active")! \ No newline at end of file diff --git a/examples/hero/db/psql2.vsh b/examples/hero/db/psql2.vsh new file mode 100755 index 00000000..ed4b8bf5 --- /dev/null +++ b/examples/hero/db/psql2.vsh @@ -0,0 +1,115 @@ +#!/usr/bin/env -S v -n -cg -w -gc none -cc tcc -d use_openssl -enable-globals run +// #!/usr/bin/env -S v -n -w -enable-globals run +import freeflowuniverse.herolib.clients.postgresql_client +import freeflowuniverse.herolib.core.playbook +import db.pg + +// psql -h /tmp -U myuser -d mydb + +mut db := pg.connect(pg.Config{ + host: '/tmp' + port: 5432 + user: 'myuser' + password: 'mypassword' + dbname: 'mydb' + })! + +mut r:=db.exec("select * from users;")! + +println(r) + +// import freeflowuniverse.herolib.core + +// import freeflowuniverse.herolib.hero.db.hero_db +// import freeflowuniverse.herolib.hero.models.circle +// import freeflowuniverse.herolib.core.playcmds + +// // Configure PostgreSQL client +// heroscript := " +// !!postgresql_client.configure +// password:'testpass' +// name:'test5' +// user: 'testuser' +// port: 5432 +// host: 'localhost' +// dbname: 'testdb' +// " +// mut plbook := playbook.new(text: heroscript)! +// postgresql_client.play(mut plbook)! + +// Configure PostgreSQL client +heroscript := " +!!postgresql_client.configure + password:'mypassword' + name:'aaa' + user: 'myuser' + host: '/tmp' + dbname: 'mydb' +" +mut plbook := playbook.new(text: heroscript)! +postgresql_client.play(mut plbook)! + + +// //Get the configured client +// mut db_client := postgresql_client.get(name: 'test5')! + +// println(db_client) + +// // Check if test database exists, create if not +// if !db_client.db_exists('test')! { +// println('Creating database test...') +// db_client.db_create('test')! +// } + +// // Switch to test database +// db_client.dbname = 'test' + +// // Create table if not exists +// create_table_sql := 'CREATE TABLE IF NOT EXISTS users ( +// id SERIAL PRIMARY KEY, +// name VARCHAR(100) NOT NULL, +// email VARCHAR(255) UNIQUE NOT NULL, +// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +// )' + +// println('Creating table users if not exists...') +// db_client.exec(create_table_sql)! + +// println('Database and table setup completed successfully!') + + +// // Create HeroDB for Circle type +// mut circle_db := hero_db.new[circle.Circle]()! + +// println(circle_db) + +// if true{panic("sd")} + +// circle_db.ensure_table()! + +// // Create and save a circle +// mut my_circle := circle.Circle{ +// name: "Tech Community" +// description: "A community for tech enthusiasts" +// domain: "tech.example.com" +// config: circle.CircleConfig{ +// max_members: 1000 +// allow_guests: true +// auto_approve: false +// theme: "modern" +// } +// status: circle.CircleStatus.active +// } + +// circle_db.save(&my_circle)! + +// // Retrieve the circle +// retrieved_circle := circle_db.get_by_index({ +// "domain": "tech.example.com" +// })! + +// // Search circles by status +// active_circles := circle_db.search_by_index("status", "active")! + + +//https://www.moncefbelyamani.com/how-to-install-postgresql-on-a-mac-with-homebrew-and-lunchy/ \ No newline at end of file diff --git a/lib/clients/postgresql_client/postgresql_client_factory_.v b/lib/clients/postgresql_client/postgresql_client_factory_.v index a4271490..e9d207c3 100644 --- a/lib/clients/postgresql_client/postgresql_client_factory_.v +++ b/lib/clients/postgresql_client/postgresql_client_factory_.v @@ -3,6 +3,7 @@ module postgresql_client import freeflowuniverse.herolib.core.base import freeflowuniverse.herolib.core.playbook { PlayBook } import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.data.encoderhero __global ( postgresql_client_global map[string]&PostgresqlClient @@ -83,8 +84,10 @@ pub fn play(mut plbook PlayBook) ! { if install_actions.len > 0 { for install_action in install_actions { heroscript := install_action.heroscript() - mut obj2 := heroscript_loads(heroscript)! - set(obj2)! + println(heroscript) + mut obj := encoderhero.decode[PostgresqlClientData](heroscript)! + // mut obj2 := heroscript_loads(heroscript)! + // set(obj2)! } } } diff --git a/lib/clients/postgresql_client/postgresql_client_model.v b/lib/clients/postgresql_client/postgresql_client_model.v index b7100bfa..8a386d3f 100644 --- a/lib/clients/postgresql_client/postgresql_client_model.v +++ b/lib/clients/postgresql_client/postgresql_client_model.v @@ -2,6 +2,7 @@ module postgresql_client import freeflowuniverse.herolib.data.paramsparser import freeflowuniverse.herolib.data.encoderhero +import freeflowuniverse.herolib.ui.console import os import db.pg @@ -9,6 +10,7 @@ pub const version = '0.0.0' const singleton = false const default = true +@[heap] pub struct PostgresqlClient { mut: db_ ?pg.DB @[skip] @@ -21,6 +23,17 @@ pub mut: dbname string = 'postgres' } +pub struct PostgresqlClientData { +pub mut: + name string = 'default' + user string = 'root' + port int = 5432 + host string = 'localhost' + password string = '' + dbname string = 'postgres' +} + + fn obj_init(obj_ PostgresqlClient) !PostgresqlClient { // never call get here, only thing we can do here is work on object itself mut obj := obj_ @@ -28,7 +41,7 @@ fn obj_init(obj_ PostgresqlClient) !PostgresqlClient { } pub fn (mut self PostgresqlClient) db() !pg.DB { - // console.print_debug(args) + console.print_debug(self) mut db := self.db_ or { mut db_ := pg.connect( host: self.host @@ -50,6 +63,6 @@ pub fn heroscript_dumps(obj PostgresqlClient) !string { } pub fn heroscript_loads(heroscript string) !PostgresqlClient { - mut obj := encoderhero.decode[PostgresqlClient](heroscript)! - return obj + mut obj := encoderhero.decode[PostgresqlClientData](heroscript)! + return PostgresqlClient{db_:pg.DB{}} } diff --git a/lib/data/encoderhero/encoder.v b/lib/data/encoderhero/encoder.v index 79a3ca69..1a999cc4 100644 --- a/lib/data/encoderhero/encoder.v +++ b/lib/data/encoderhero/encoder.v @@ -39,7 +39,7 @@ pub fn encode[T](val T) !string { // export exports an encoder into encoded heroscript pub fn (e Encoder) export() !string { mut script := e.params.export( - pre: '!!define.${e.action_names.join('.')}' + pre: '!!${e.action_names.join('.')}.configure' indent: ' ' skip_empty: true ) @@ -119,6 +119,7 @@ pub fn (mut e Encoder) encode_struct[T](t T) ! { struct_attrs := attrs_get_reflection(mytype) mut action_name := texttools.snake_case(T.name.all_after_last('.')) + // println('action_name: ${action_name} ${T.name}') if 'alias' in struct_attrs { action_name = struct_attrs['alias'].to_lower() } diff --git a/lib/data/encoderhero/postgres_client_decoder_test.v b/lib/data/encoderhero/postgres_client_decoder_test.v index 9f366583..b38b8ecb 100644 --- a/lib/data/encoderhero/postgres_client_decoder_test.v +++ b/lib/data/encoderhero/postgres_client_decoder_test.v @@ -12,12 +12,12 @@ pub mut: } -const postgres_client_blank = '!!define.postgres_client' -const postgres_client_full = '!!define.postgres_client name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp' -const postgres_client_partial = '!!define.postgres_client name:dev host:localhost password:devpass' +const postgres_client_blank = '!!postgresql_client.configure' +const postgres_client_full = '!!postgresql_client.configure name:production user:app_user port:5433 host:db.example.com password:secret123 dbname:myapp' +const postgres_client_partial = '!!postgresql_client.configure name:dev host:localhost password:devpass' const postgres_client_complex = " -!!define.postgres_client name:staging user:stage_user port:5434 host:staging.db.com password:stagepass dbname:stagingdb +!!postgresql_client.configure name:staging user:stage_user port:5434 host:staging.db.com password:stagepass dbname:stagingdb " fn test_postgres_client_decode_blank() ! { @@ -73,6 +73,11 @@ fn test_postgres_client_encode_decode_roundtrip() ! { // Encode to heroscript encoded := encode[PostgresqlClient](original)! + + // println('Encoded heroscript: ${encoded}') + // if true { + // panic("sss") + // } // Decode back from heroscript decoded := decode[PostgresqlClient](encoded)! @@ -133,17 +138,17 @@ const play_script = " # PostgresqlClient Encode/Decode Play Script # This script demonstrates encoding and decoding PostgresqlClient configurations -!!define.postgres_client name:playground user:play_user +!!postgresql_client.configure name:playground user:play_user port:5432 host:localhost password:playpass dbname:playdb # You can also use partial configurations -!!define.postgres_client name:quick_test host:127.0.0.1 +!!postgresql_client.configure name:quick_test host:127.0.0.1 # Default configuration (all defaults) -!!define.postgres_client +!!postgresql_client.configure " fn test_play_script() ! { @@ -155,7 +160,7 @@ fn test_play_script() ! { mut clients := []PostgresqlClient{} for line in lines { - if line.starts_with('!!define.postgres_client') { + if line.starts_with('!!postgresql_client.configure') { client := decode[PostgresqlClient](line)! clients << client } diff --git a/lib/data/paramsparser/params_reflection.v b/lib/data/paramsparser/params_reflection.v index 383a3b32..64be657a 100644 --- a/lib/data/paramsparser/params_reflection.v +++ b/lib/data/paramsparser/params_reflection.v @@ -6,29 +6,25 @@ import v.reflection // import freeflowuniverse.herolib.data.encoderhero // TODO: support more field types -pub fn (params Params) decode[T](args ...T) !T { - // work around to allow recursive decoding - // otherwise v cant infer generic type for child fields that are structs - if args.len > 0 { - return params.decode_struct[T](args[0])! - } else { - return params.decode_struct[T](T{})! - } +pub fn (params Params) decode[T](args T) !T { + return params.decode_struct[T](args)! } pub fn (params Params) decode_struct[T](start T) !T { - mut t := start + mut t := T{} $for field in T.fields { $if field.is_enum { t.$(field.name) = params.get_int(field.name) or { t.$(field.name) } } $else { // super annoying didn't find other way, then to ignore options $if field.is_option { + // For optional fields, if the key exists, decode it. Otherwise, leave it as none. + if params.exists(field.name) { + t.$(field.name) = params.decode_value(t.$(field.name), field.name)! + } } $else { if field.name[0].is_capital() { - // embed := params.decode_struct(t.$(field.name))! t.$(field.name) = params.decode_struct(t.$(field.name))! - // panic("to implement") } else { t.$(field.name) = params.decode_value(t.$(field.name), field.name)! } @@ -61,9 +57,10 @@ pub fn (params Params) decode_value[T](val T, key string) !T { return params.get_list(key)! } $else $if T is []int { return params.get_list_int(key)! + } $else $if T is []bool { + return params.get_list_bool(key)! } $else $if T is []u32 { - lst := params.get_list_u32(key)! - return lst + return params.get_list_u32(key)! } $else $if T is time.Time { time_str := params.get(key)! // todo: 'handle other null times' @@ -86,6 +83,18 @@ pub fn (params Params) decode_value[T](val T, key string) !T { return T{} } +pub fn (params Params) get_list_bool(key string) ![]bool { + mut res := []bool{} + val := params.get(key)! + if val.len == 0 { + return res + } + for item in val.split(',') { + res << item.trim_space().bool() + } + return res +} + @[params] pub struct EncodeArgs { pub: @@ -93,13 +102,6 @@ pub: } pub fn encode[T](t T, args EncodeArgs) !Params { - $if t is $option { - // unwrap and encode optionals - workaround := t - if workaround != none { - encode(t, args)! - } - } mut params := Params{} // struct_attrs := attrs_get_reflection(mytype) @@ -111,7 +113,27 @@ pub fn encode[T](t T, args EncodeArgs) !Params { if 'alias' in field_attrs { key = field_attrs['alias'] } - $if val is string || val is int || val is bool || val is i64 || val is u32 + $if field.is_option { + // Handle optional fields + if val != none { + // Unwrap the optional value before type checking and encoding + // Get the unwrapped value using reflection + // This is a workaround for V's reflection limitations with optionals + // We assume that if val != none, then it can be safely unwrapped + // and its underlying type can be determined. + // This might require a more robust way to get the underlying value + // if V's reflection doesn't provide a direct 'unwrap' for generic `val`. + // For now, we'll rely on the type checks below. + // The `val` here is the actual value of the field, which is `?T`. + // We need to check the type of `field.typ` to know what `T` is. + + // Revert to simpler handling for optional fields + // Rely on V's string interpolation for optional types + // If val is none, this block will be skipped. + // If val is not none, it will be converted to string. + params.set(key, '${val}') + } + } $else $if val is string || val is int || val is bool || val is i64 || val is u32 || val is time.Time || val is ourtime.OurTime { params.set(key, '${val}') } $else $if field.is_enum { @@ -140,6 +162,16 @@ pub fn encode[T](t T, args EncodeArgs) !Params { key: field.name value: v2 } + } $else $if field.typ is []bool { + mut v2 := '' + for i in val { + v2 += '${i},' + } + v2 = v2.trim(',') + params.params << Param{ + key: field.name + value: v2 + } } $else $if field.typ is []u32 { mut v2 := '' for i in val { diff --git a/lib/data/paramsparser/params_reflection_test.v b/lib/data/paramsparser/params_reflection_test.v index 1d7a0d6b..21fe7e16 100644 --- a/lib/data/paramsparser/params_reflection_test.v +++ b/lib/data/paramsparser/params_reflection_test.v @@ -11,6 +11,7 @@ struct TestStruct { liststr []string listint []int listbool []bool + listu32 []u32 child TestChild } @@ -21,6 +22,7 @@ struct TestChild { child_liststr []string child_listint []int child_listbool []bool + child_listu32 []u32 } const test_child = TestChild{ @@ -29,6 +31,8 @@ const test_child = TestChild{ child_yesno: false child_liststr: ['three', 'four'] child_listint: [3, 4] + child_listbool: [true, false] + child_listu32: [u32(5), u32(6)] } const test_struct = TestStruct{ @@ -42,9 +46,12 @@ const test_struct = TestStruct{ yesno: true liststr: ['one', 'two'] listint: [1, 2] + listbool: [true, false] + listu32: [u32(7), u32(8)] child: test_child } + const test_child_params = Params{ params: [ Param{ @@ -67,6 +74,14 @@ const test_child_params = Params{ key: 'child_listint' value: '3,4' }, + Param{ + key: 'child_listbool' + value: 'true,false' + }, + Param{ + key: 'child_listu32' + value: '5,6' + }, ] } @@ -74,9 +89,6 @@ const test_params = Params{ params: [Param{ key: 'name' value: 'test' - }, Param{ - key: 'nick' - value: 'test_nick' }, Param{ key: 'birthday' value: '2012-12-12 00:00:00' @@ -92,22 +104,64 @@ const test_params = Params{ }, Param{ key: 'listint' value: '1,2' + }, Param{ + key: 'listbool' + value: 'true,false' + }, Param{ + key: 'listu32' + value: '7,8' }, Param{ key: 'child' value: test_child_params.export() }] } -fn test_decode() { - // test single level struct - decoded_child := test_child_params.decode[TestChild]()! - assert decoded_child == test_child - // IMPORTANT OPTIONALS ARE NOT SUPPORTED AND WILL NOT BE ENCODED FOR NOW (unless we find ways how to deal with attributes to not encode skipped elements) +fn test_encode_struct() { + encoded_struct := encode[TestStruct](test_struct)! + assert encoded_struct == test_params +} - // test recursive decode struct with child - decoded := test_params.decode[TestStruct]()! - assert decoded == test_struct +fn test_decode_struct() { + decoded_struct := test_params.decode[TestStruct](TestStruct{})! + assert decoded_struct.name == test_struct.name + assert decoded_struct.birthday.day == test_struct.birthday.day + assert decoded_struct.birthday.month == test_struct.birthday.month + assert decoded_struct.birthday.year == test_struct.birthday.year + assert decoded_struct.number == test_struct.number + assert decoded_struct.yesno == test_struct.yesno + assert decoded_struct.liststr == test_struct.liststr + assert decoded_struct.listint == test_struct.listint + assert decoded_struct.listbool == test_struct.listbool + assert decoded_struct.listu32 == test_struct.listu32 + assert decoded_struct.child == test_struct.child +} + +fn test_optional_field() { + mut test_struct_with_nick := TestStruct{ + name: test_struct.name + nick: 'test_nick' + birthday: test_struct.birthday + number: test_struct.number + yesno: test_struct.yesno + liststr: test_struct.liststr + listint: test_struct.listint + listbool: test_struct.listbool + listu32: test_struct.listu32 + child: test_struct.child + } + + encoded_struct_with_nick := encode[TestStruct](test_struct_with_nick)! + assert encoded_struct_with_nick.get('nick')! == 'test_nick' + + decoded_struct_with_nick := encoded_struct_with_nick.decode[TestStruct](TestStruct{})! + assert decoded_struct_with_nick.nick or { '' } == 'test_nick' + + // Test decoding when optional field is not present in params + mut params_without_nick := test_params + params_without_nick.params = params_without_nick.params.filter(it.key != 'nick') + decoded_struct_without_nick := params_without_nick.decode[TestStruct](TestStruct{})! + assert decoded_struct_without_nick.nick == none } fn test_encode() { diff --git a/lib/hero/db/hero_db/hero_db.v b/lib/hero/db/hero_db/hero_db.v index 6c0ef6f5..c71cc282 100644 --- a/lib/hero/db/hero_db/hero_db.v +++ b/lib/hero/db/hero_db/hero_db.v @@ -2,16 +2,18 @@ module hero_db import json import freeflowuniverse.herolib.clients.postgresql_client +import db.pg import freeflowuniverse.herolib.core.texttools // Generic database interface for Hero root objects pub struct HeroDB[T] { - db_client &postgresql_client.PostgresqlClient +pub mut: + db pg.DB table_name string } // new creates a new HeroDB instance for a specific type T -pub fn new[T](client &postgresql_client.PostgresqlClient) HeroDB[T] { +pub fn new[T]() !HeroDB[T] { mut table_name := '${texttools.snake_case(T.name)}s' // Map dirname from module path module_path := T.name.split('.') @@ -19,9 +21,15 @@ pub fn new[T](client &postgresql_client.PostgresqlClient) HeroDB[T] { dirname := texttools.snake_case(module_path[module_path.len - 2]) table_name = '${dirname}_${texttools.snake_case(T.name)}' } + + mut dbclient:=postgresql_client.get()! + + mut dbcl:=dbclient.db() or { + return error('Failed to connect to database') + } return HeroDB[T]{ - db_client: client + db: dbcl table_name: table_name } } @@ -48,12 +56,12 @@ pub fn (mut self HeroDB[T]) ensure_table() ! { ) ' - self.db_client.exec(create_sql)! + // self.db.exec(create_sql)! // Create indexes on index fields for field in index_fields { index_sql := 'CREATE INDEX IF NOT EXISTS idx_${self.table_name}_${field} ON ${self.table_name}(${field})' - self.db_client.exec(index_sql)! + // self.db.exec(index_sql)! } } @@ -61,19 +69,15 @@ pub fn (mut self HeroDB[T]) ensure_table() ! { fn (self HeroDB[T]) get_index_fields() []string { mut fields := []string{} $for field in T.fields { - $if field.attributes.len > 0 { - $for attr in field.attributes { - $if attr == 'index' { - fields << texttools.snake_case(field.name) - } - } + if field.attrs.contains('index') { + fields << texttools.snake_case(field.name) } } return fields } // save stores the object T in the database, updating if it already exists -pub fn (mut self HeroDB[T]) save(obj &T) ! { +pub fn (mut self HeroDB[T]) save(obj T) ! { // Get index values from object index_data := self.extract_index_values(obj) @@ -90,17 +94,22 @@ pub fn (mut self HeroDB[T]) save(obj &T) ! { } query += params.join(' AND ') - existing := self.db_client.exec(query)! + existing :=self.db.exec(query)! if existing.len > 0 { // Update existing record - id := existing[0].vals[0].int() - update_sql := ' - UPDATE ${self.table_name} - SET data = \$1, updated_at = CURRENT_TIMESTAMP - WHERE id = \$2 - ' - self.db_client.exec(update_sql.replace('\$1', "'${json_data}'").replace('\$2', id.str()))! + id_val := existing[0].vals[0] or { return error('no id') } + // id := id_val.int() + println('Updating existing record with ID: ${id_val}') + if true { + panic('sd111') + } + // update_sql := ' + // UPDATE ${self.table_name} + // SET data = \$1, updated_at = CURRENT_TIMESTAMP + // WHERE id = \$2 + // ' + // self.db_client.db()!.exec_param(update_sql, [json_data, id.str()])! } else { // Insert new record mut columns := []string{} @@ -120,12 +129,12 @@ pub fn (mut self HeroDB[T]) save(obj &T) ! { INSERT INTO ${self.table_name} (${columns.join(', ')}) VALUES (${values.join(', ')}) ' - self.db_client.exec(insert_sql)! + // self.db.exec(insert_sql)! } } // get_by_index retrieves an object T by its index values -pub fn (mut self HeroDB[T]) get_by_index(index_values map[string]string) !&T { +pub fn (mut self HeroDB[T]) get_by_index(index_values map[string]string) !T { mut query := 'SELECT data FROM ${self.table_name} WHERE ' mut params := []string{} @@ -133,91 +142,92 @@ pub fn (mut self HeroDB[T]) get_by_index(index_values map[string]string) !&T { params << '${key} = \'${value}\'' } query += params.join(' AND ') - - rows := self.db_client.exec(query)! + + rows := self.db.exec(query)! if rows.len == 0 { return error('${T.name} not found with index values: ${index_values}') } - json_data := rows[0].vals[0].str() - mut obj := json.decode(T, json_data) or { - return error('Failed to decode JSON: ${err}') + json_data_val := rows[0].vals[0] or { return error('no data') } + println('json_data_val: ${json_data_val}') + if true{ + panic('sd2221') } + // mut obj := json.decode(T, json_data_val) or { + // return error('Failed to decode JSON: ${err}') + // } - return &obj + // return &obj + return T{} } -// get_all retrieves all objects T from the database -pub fn (mut self HeroDB[T]) get_all() ![]&T { - query := 'SELECT data FROM ${self.table_name} ORDER BY id DESC' - rows := self.db_client.exec(query)! +// // get_all retrieves all objects T from the database +// pub fn (mut self HeroDB[T]) get_all() ![]T { +// query := 'SELECT data FROM ${self.table_name} ORDER BY id DESC' +// rows := self.db_client.db()!.exec(query)! - mut results := []&T{} - for row in rows { - json_data := row.vals[0].str() - obj := json.decode(T, json_data) or { - continue // Skip invalid JSON - } - results << &obj - } +// mut results := []T{} +// for row in rows { +// json_data_val := row.vals[0] or { continue } +// json_data := json_data_val.str() +// mut obj := json.decode(T, json_data) or { +// // e.g. an error could be given here +// continue // Skip invalid JSON +// } +// results << &obj +// } - return results -} +// return results +// } -// search_by_index searches for objects T by a specific index field -pub fn (mut self HeroDB[T]) search_by_index(field_name string, value string) ![]&T { - query := 'SELECT data FROM ${self.table_name} WHERE ${field_name} = \'${value}\' ORDER BY id DESC' - rows := self.db_client.exec(query)! +// // search_by_index searches for objects T by a specific index field +// pub fn (mut self HeroDB[T]) search_by_index(field_name string, value string) ![]T { +// query := 'SELECT data FROM ${self.table_name} WHERE ${field_name} = \'${value}\' ORDER BY id DESC' +// rows := self.db_client.db()!.exec(query)! - mut results := []&T{} - for row in rows { - json_data := row.vals[0].str() - obj := json.decode(T, json_data) or { - continue - } - results << &obj - } +// mut results := []T{} +// for row in rows { +// json_data_val := row.vals[0] or { continue } +// json_data := json_data_val.str() +// mut obj := json.decode(T, json_data) or { +// continue +// } +// results << &obj +// } - return results -} +// return results +// } -// delete_by_index removes objects T matching the given index values -pub fn (mut self HeroDB[T]) delete_by_index(index_values map[string]string) ! { - mut query := 'DELETE FROM ${self.table_name} WHERE ' - mut params := []string{} +// // delete_by_index removes objects T matching the given index values +// pub fn (mut self HeroDB[T]) delete_by_index(index_values map[string]string) ! { +// mut query := 'DELETE FROM ${self.table_name} WHERE ' +// mut params := []string{} - for key, value in index_values { - params << '${key} = \'${value}\'' - } - query += params.join(' AND ') +// for key, value in index_values { +// params << '${key} = \'${value}\'' +// } +// query += params.join(' AND ') - self.db_client.exec(query)! -} +// self.db_client.db()!.exec(query)! +// } // Helper to extract index values from object -fn (self HeroDB[T]) extract_index_values(obj &T) map[string]string { +fn (self HeroDB[T]) extract_index_values(obj T) map[string]string { mut index_data := map[string]string{} - $for field in T.fields { - $if field.attributes.len > 0 { - $for attr in field.attributes { - $if attr == 'index' { - field_name := texttools.snake_case(field.name) - $if field.typ is string { - value := obj.$(field.name).str() - index_data[field_name] = value - } $else $if field.typ is u32 || field.typ is u64 { - value := obj.$(field.name).str() - index_data[field_name] = value - } $else { - // Convert other types to string - value := obj.$(field.name).str() - index_data[field_name] = value - } - } - } - } + // $if field.attrs.contains('index') { + // field_name := texttools.snake_case(field.name) + // $if field.typ is string { + // value := obj.$(field.name) + // index_data[field_name] = value + // } $else $if field.typ is int { + // value := obj.$(field.name).str() + // index_data[field_name] = value + // } $else { + // value := obj.$(field.name).str() + // index_data[field_name] = value + // } + // } } - return index_data } diff --git a/lib/hero/models/circle/circle.v b/lib/hero/models/circle/circle.v index 851ddfa2..28c69151 100644 --- a/lib/hero/models/circle/circle.v +++ b/lib/hero/models/circle/circle.v @@ -3,6 +3,7 @@ module circle import freeflowuniverse.herolib.hero.models.core // Circle represents a circle entity with configuration and metadata +@[heap] pub struct Circle { core.Base pub mut: diff --git a/logfile b/logfile new file mode 100644 index 00000000..b55fedf1 --- /dev/null +++ b/logfile @@ -0,0 +1,7 @@ +2025-07-31 02:25:13.191 CEST [63643] LOG: starting PostgreSQL 17.5 (Homebrew) on aarch64-apple-darwin24.4.0, compiled by Apple clang version 17.0.0 (clang-1700.0.13.3), 64-bit +2025-07-31 02:25:13.192 CEST [63643] LOG: listening on IPv6 address "::1", port 5432 +2025-07-31 02:25:13.192 CEST [63643] LOG: listening on IPv4 address "127.0.0.1", port 5432 +2025-07-31 02:25:13.192 CEST [63643] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432" +2025-07-31 02:25:13.194 CEST [63646] LOG: database system was shut down at 2025-07-31 02:24:30 CEST +2025-07-31 02:25:13.196 CEST [63643] LOG: database system is ready to accept connections +2025-07-31 02:25:18.216 CEST [63775] FATAL: database "despiegk" does not exist