...
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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")!
|
||||
115
examples/hero/db/psql2.vsh
Executable file
115
examples/hero/db/psql2.vsh
Executable file
@@ -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/
|
||||
@@ -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)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{}}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() ! {
|
||||
@@ -74,6 +74,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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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('.')
|
||||
@@ -20,8 +22,14 @@ pub fn new[T](client &postgresql_client.PostgresqlClient) HeroDB[T] {
|
||||
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' {
|
||||
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{}
|
||||
|
||||
@@ -134,90 +143,91 @@ pub fn (mut self HeroDB[T]) get_by_index(index_values map[string]string) !&T {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
7
logfile
Normal file
7
logfile
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user