From 4f6ebfb33f2050a445748b72cb35db4f1db31ba8 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 24 Jan 2026 13:18:25 +0300 Subject: [PATCH] pg: exec_no_null(); gx: remove deprecated test; parser/tmpl: %raw translations; veb: time_to_render() --- cmd/tools/vwatch.v | 6 +- vlib/db/pg/pg.c.v | 41 +++++++++ vlib/gx/color_test.v | 102 ----------------------- vlib/v/checker/fn.v | 3 +- vlib/v/checker/tests/option_str_call.out | 2 +- vlib/v/parser/tmpl.v | 30 +++++-- vlib/veb/context.v | 9 ++ vlib/veb/veb.v | 11 +-- vlib/veb/veb_d_new_veb.v | 51 +++++++----- 9 files changed, 114 insertions(+), 141 deletions(-) delete mode 100644 vlib/gx/color_test.v diff --git a/cmd/tools/vwatch.v b/cmd/tools/vwatch.v index bc887ef86..7424e8b12 100644 --- a/cmd/tools/vwatch.v +++ b/cmd/tools/vwatch.v @@ -15,7 +15,7 @@ const scan_period_ms = 1000 / scan_frequency_hz const max_scan_cycles = scan_timeout_s * scan_frequency_hz -const default_vweb_suffixes = '*.v,*.html,*.css,*.js,*.md' +const default_veb_suffixes = '*.v,*.html,*.css,*.js,*.md,*.tr' fn get_scan_timeout_seconds() int { env_vw_timeout := os.getenv('VWATCH_TIMEOUT').int() @@ -171,14 +171,14 @@ fn (mut context Context) get_stats_for_affected_vfiles() []VFileStat { if is_vweb_found { if !os.args.any(it.starts_with('--only-watch')) { - context.only_watch = default_vweb_suffixes.split_any(',') + context.only_watch = default_veb_suffixes.split_any(',') // vweb is often used with SQLite .db or .sqlite3 files right next to the executable/source, // that are updated by the vweb app, causing a restart of the app, which in turn causes the // browser to reload the current page, that probably triggered the update in the first place. // Note that the problem is not specific to SQLite, any database that stores its files in the // current (project) folder, will also cause this. println('`v watch` detected that you are compiling a vweb project.') - println(' Because of that, the `--only-watch=${default_vweb_suffixes}` flag was also implied.') + println(' Because of that, the `--only-watch=${default_veb_suffixes}` flag was also implied.') println(' In result, `v watch` will ignore changes to other files.') println(' Add your own --only-watch filter, if you wish to override that choice.') println('') diff --git a/vlib/db/pg/pg.c.v b/vlib/db/pg/pg.c.v index f4324b5ad..b41c53e5f 100644 --- a/vlib/db/pg/pg.c.v +++ b/vlib/db/pg/pg.c.v @@ -69,6 +69,11 @@ pub mut: vals []?string } +pub struct RowNoNull { +pub mut: + vals []string +} + pub struct Result { pub: cols map[string]int @@ -259,6 +264,22 @@ fn res_to_rows(res voidptr) []Row { return rows } +fn res_to_rows_no_null(res voidptr) []RowNoNull { + nr_rows := C.PQntuples(res) + nr_cols := C.PQnfields(res) + mut rows := []RowNoNull{} + for i in 0 .. nr_rows { + mut row := RowNoNull{} + for j in 0 .. nr_cols { + val := C.PQgetvalue(res, i, j) + row.vals << unsafe { cstring_to_vstring(val) } + } + rows << row + } + C.PQclear(res) + return rows +} + // res_to_result creates a `Result` struct out of a `C.PGresult` pointer fn res_to_result(res voidptr) Result { nr_rows := C.PQntuples(res) @@ -338,6 +359,12 @@ pub fn (db &DB) exec(query string) ![]Row { return db.handle_error_or_rows(res, 'exec') } +// exec_no_null works like exec, but the fields can't be NULL, no optionals +pub fn (db &DB) exec_no_null(query string) ![]RowNoNull { + res := C.PQexec(db.conn, &char(query.str)) + return db.handle_error_or_rows_no_null(res, 'exec') +} + // exec_result submits a command to the database server and wait for the result, returning an error on failure and a `Result` set on success pub fn (db &DB) exec_result(query string) !Result { res := C.PQexec(db.conn, &char(query.str)) @@ -440,6 +467,7 @@ fn (db &DB) handle_error_or_rows(res voidptr, elabel string) ![]Row { e := unsafe { C.PQerrorMessage(db.conn).vstring() } if e != '' { C.PQclear(res) + // TODO make it default $if trace_pg_error ? { eprintln('pg error: ${e}') } @@ -448,6 +476,19 @@ fn (db &DB) handle_error_or_rows(res voidptr, elabel string) ![]Row { return res_to_rows(res) } +fn (db &DB) handle_error_or_rows_no_null(res voidptr, elabel string) ![]RowNoNull { + // TODO copypasta + e := unsafe { C.PQerrorMessage(db.conn).vstring() } + if e != '' { + C.PQclear(res) + $if trace_pg_error ? { + eprintln('pg error: ${e}') + } + return error('pg ${elabel} error:\n${e}') + } + return res_to_rows_no_null(res) +} + // hande_error_or_result is an internal function similar to handle_error_or_rows that returns `Result` instead of `[]Row` fn (db &DB) handle_error_or_result(res voidptr, elabel string) !Result { e := unsafe { C.PQerrorMessage(db.conn).vstring() } diff --git a/vlib/gx/color_test.v b/vlib/gx/color_test.v deleted file mode 100644 index 1d7edc2d7..000000000 --- a/vlib/gx/color_test.v +++ /dev/null @@ -1,102 +0,0 @@ -// vtest build: !musl? // gx now transitively imports sokol, and that needs GL/gl.h, which is not installed on the musl CIs -import gx - -fn test_hex() { - // valid colors - // a := gx.hex(0x6c5ce7ff) - // b := gx.rgba(108, 92, 231, 255) - assert gx.hex(0xff6600) == gx.rgb(255, 102, 0) // orange - // doesn't give right value with short hex value - short := gx.hex(0xfff) - assert short != gx.white - // assert short == gx.Color{0, 0, 15, 255} -} - -fn test_add() { - a := gx.rgba(100, 100, 100, 100) - b := gx.rgba(100, 100, 100, 100) - r := gx.rgba(200, 200, 200, 200) - assert (a + b) == r - - assert gx.red + gx.green == gx.yellow - assert gx.red + gx.blue == gx.magenta - assert gx.green + gx.blue == gx.cyan -} - -fn test_sub() { - a := gx.rgba(100, 100, 100, 50) - b := gx.rgba(100, 100, 100, 100) - r := gx.rgba(0, 0, 0, 100) - assert (a - b) == r - - assert gx.white - gx.green == gx.magenta - assert gx.white - gx.blue == gx.yellow - assert gx.white - gx.red == gx.cyan -} - -fn test_mult() { - a := gx.rgba(10, 10, 10, 10) - b := gx.rgba(10, 10, 10, 10) - r := gx.rgba(100, 100, 100, 100) - assert (a * b) == r -} - -fn test_div() { - a := gx.rgba(100, 100, 100, 100) - b := gx.rgba(10, 10, 10, 10) - r := gx.rgba(10, 10, 10, 10) - assert (a / b) == r -} - -fn test_rgba8() { - assert gx.white.rgba8() == -1 - assert gx.black.rgba8() == 255 - assert gx.red.rgba8() == -16776961 - assert gx.green.rgba8() == 16711935 - assert gx.blue.rgba8() == 65535 -} - -fn test_bgra8() { - assert gx.white.bgra8() == -1 - assert gx.black.bgra8() == 255 - assert gx.red.bgra8() == 65535 - assert gx.green.bgra8() == 16711935 - assert gx.blue.bgra8() == -16776961 -} - -fn test_abgr8() { - assert gx.white.abgr8() == -1 - assert gx.black.abgr8() == -16777216 - assert gx.red.abgr8() == -16776961 - assert gx.green.abgr8() == -16711936 - assert gx.blue.abgr8() == -65536 -} - -fn test_over() { - // shorten names: - r := gx.red - g := gx.green - b := gx.blue - y := gx.yellow - semi_r := gx.Color{255, 0, 0, 128} - semi_g := gx.Color{0, 255, 0, 128} - semi_b := gx.Color{0, 0, 255, 128} - // fully opaque colors, should be preserved when laid *over* any others: - assert b.over(g) == b - assert r.over(g) == r - assert y.over(r) == y - assert g.over(r) == g - - // half transparent pure colors, *over* pure colors, should preserve them correspondingly: - assert semi_r.over(r) == gx.Color{255, 0, 0, 255} // preserve pure red - assert semi_r.over(g) == gx.Color{128, 126, 0, 255} - assert semi_r.over(b) == gx.Color{128, 0, 126, 255} - - assert semi_g.over(r) == gx.Color{126, 128, 0, 255} - assert semi_g.over(g) == gx.Color{0, 255, 0, 255} // preserve pure green - assert semi_g.over(b) == gx.Color{0, 128, 126, 255} - - assert semi_b.over(r) == gx.Color{126, 0, 128, 255} - assert semi_b.over(g) == gx.Color{0, 126, 128, 255} - assert semi_b.over(b) == gx.Color{0, 0, 255, 255} // preserve pure blue -} diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 55728e888..2da62c894 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -2176,7 +2176,8 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) method_name := node.name if left_type.has_flag(.option) { - c.error('Option type cannot be called directly, you should unwrap it first', node.left.pos()) + c.error('Option type `${left_sym.name}` cannot be called directly, you should unwrap it first', + node.left.pos()) return ast.void_type } else if left_type.has_flag(.result) { c.error('Result type cannot be called directly', node.left.pos()) diff --git a/vlib/v/checker/tests/option_str_call.out b/vlib/v/checker/tests/option_str_call.out index 0e03a150e..d8a908a91 100644 --- a/vlib/v/checker/tests/option_str_call.out +++ b/vlib/v/checker/tests/option_str_call.out @@ -1,4 +1,4 @@ -vlib/v/checker/tests/option_str_call.vv:7:10: error: Option type cannot be called directly, you should unwrap it first +vlib/v/checker/tests/option_str_call.vv:7:10: error: Option type `net.html.Tag` cannot be called directly, you should unwrap it first 5 | footer := doc.get_tags_by_class_name('Box-footer')[0] 6 | invalid := footer.get_tag_by_class_name('invalid') 7 | println(invalid.str()) diff --git a/vlib/v/parser/tmpl.v b/vlib/v/parser/tmpl.v index 7dfad5166..64140c908 100644 --- a/vlib/v/parser/tmpl.v +++ b/vlib/v/parser/tmpl.v @@ -429,17 +429,33 @@ fn veb_tmpl_${fn_name}() string { if pos := line.index('%') { // %translation_key => ${tr('translation_key')} + is_raw := line.contains('%raw ') mut line_ := line - if pos + 1 < line.len && line[pos + 1].is_letter() { //|| line[pos + 1] == '_' { - mut end := pos + 1 + if is_raw { + // Start reading the key after "raw " (pos + 1 + 4) + mut end := pos + 5 + // valid variable characters for end < line.len && (line[end].is_letter() || line[end] == `_`) { end++ } - key := line[pos + 1..end] - // println('GOT tr key line="${line}" key="${key}"') - // source.writeln('\${tr("${key}")}') - line_ = line.replace('%${key}', '\${veb.tr(ctx.lang.str(), "${key}")}') - // i += key.len + // Extract the key + key := line[pos + 5..end] + // Replace '%raw key' with just '${key}' + // We escape it as \${key} so the generated V code performs the interpolation + // line_ = line.replace('%raw ${key}', '\${${key}}') + line_ = line.replace('%raw ${key}', '\${veb.raw(veb.tr(ctx.lang.str(), "${key}"))}') + } else { + if pos + 1 < line.len && line[pos + 1].is_letter() { //|| line[pos + 1] == '_' { + mut end := pos + 1 + for end < line.len && (line[end].is_letter() || line[end] == `_`) { + end++ + } + key := line[pos + 1..end] + // println('GOT tr key line="${line}" key="${key}"') + // source.writeln('\${tr("${key}")}') + line_ = line.replace('%${key}', '\${veb.tr(ctx.lang.str(), "${key}")}') + // i += key.len + } } // println(source.str()) source.writeln(insert_template_code(fn_name, tmpl_str_start, line_)) diff --git a/vlib/veb/context.v b/vlib/veb/context.v index 15c0d12fb..24ca2d775 100644 --- a/vlib/veb/context.v +++ b/vlib/veb/context.v @@ -6,6 +6,7 @@ import json import net import net.http import os +import time enum ContextReturnType { normal @@ -443,3 +444,11 @@ pub fn (ctx &Context) ip() string { } return ip } + +// time_to_render returns the time in milliseconds that it took to render the page +pub fn (ctx &Context) time_to_render() i64 { + if ctx.page_gen_start == 0 { + return 0 + } + return time.ticks() - ctx.page_gen_start +} diff --git a/vlib/veb/veb.v b/vlib/veb/veb.v index a1c95c9fa..f1ecb4c57 100644 --- a/vlib/veb/veb.v +++ b/vlib/veb/veb.v @@ -72,11 +72,12 @@ pub fn run[A, X](mut global_app A, port int) { pub struct RunParams { pub: // use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1 - family net.AddrFamily = .ip6 - host string - port int = default_port - show_startup_message bool = true - timeout_in_seconds int = 30 + family net.AddrFamily = .ip6 + host string + port int = default_port + show_startup_message bool = true + timeout_in_seconds int = 30 + benchmark_page_generation bool // for the "page rendered in X ms" } struct FileResponse { diff --git a/vlib/veb/veb_d_new_veb.v b/vlib/veb/veb_d_new_veb.v index 37825b830..06c7772ef 100644 --- a/vlib/veb/veb_d_new_veb.v +++ b/vlib/veb/veb_d_new_veb.v @@ -9,21 +9,22 @@ import time import net.urllib struct RequestParams { - global_app voidptr - controllers_sorted []&ControllerPath - routes &map[string]Route + global_app voidptr + controllers_sorted []&ControllerPath + routes &map[string]Route + benchmark_page_generation bool } const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() pub fn run_at[A, X](mut global_app A, params RunParams) ! { - run_new[A, X](mut global_app, params.port)! + run_new[A, X](mut global_app, params)! } // run_new - start a new veb server using the parallel fasthttp backend. -pub fn run_new[A, X](mut global_app A, port int) ! { - if port <= 0 || port > 65535 { - return error('invalid port number `${port}`, it should be between 1 and 65535') +pub fn run_new[A, X](mut global_app A, params RunParams) ! { + if params.port <= 0 || params.port > 65535 { + return error('invalid port number `${params.port}`, it should be between 1 and 65535') } // Generate routes and controllers just like the original run() function. @@ -31,22 +32,23 @@ pub fn run_new[A, X](mut global_app A, port int) ! { controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)! // Allocate params on the heap to keep it valid for the server lifetime - params := &RequestParams{ - global_app: global_app - controllers_sorted: controllers_sorted - routes: &routes + request_params := &RequestParams{ + global_app: global_app + controllers_sorted: controllers_sorted + routes: &routes + benchmark_page_generation: params.benchmark_page_generation } // Configure and run the fasthttp server mut server := fasthttp.new_server(fasthttp.ServerConfig{ - port: port + port: params.port handler: parallel_request_handler[A, X] - user_data: voidptr(params) + user_data: voidptr(request_params) }) or { eprintln('Failed to create server: ${err}') return } - println('[veb] Running multi-threaded app on http://localhost:${port}/') + println('[veb] Running multi-threaded app on http://localhost:${params.port}/') flush_stdout() server.run() or { panic(err) } } @@ -67,7 +69,8 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpRespon } // Create and populate the `veb.Context`. completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd, - params.routes, params.controllers_sorted) + params) + // params.routes, params.controllers_sorted) // Serialize the final `http.Response` into a byte array. if completed_context.takeover { eprintln('[veb] WARNING: ctx.takeover_conn() was called, but this is not supported by this server backend. The connection will be closed after this response.') @@ -87,7 +90,7 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpRespon } // handle_request_and_route is a unified function that creates the context, // runs middleware, and finds the correct route for a request. -fn handle_request_and_route[A, X](mut app A, req http.Request, client_fd int, routes &map[string]Route, controllers []&ControllerPath) &Context { +fn handle_request_and_route[A, X](mut app A, req http.Request, client_fd int, params RequestParams) &Context { // Create and populate the `veb.Context` from the request. mut url := urllib.parse(req.url) or { // This should be rare if http.parse_request succeeded. @@ -107,12 +110,14 @@ fn handle_request_and_route[A, X](mut app A, req http.Request, client_fd int, ro } host_with_port := req.header.get(.host) or { '' } host, _ := urllib.split_host_port(host_with_port) + page_gen_start := if params.benchmark_page_generation { time.ticks() } else { 0 } mut ctx := &Context{ req: req - page_gen_start: time.ticks() - query: query - form: form - files: files + page_gen_start: page_gen_start + // page_gen_start: time.ticks() + query: query + form: form + files: files } if connection_header := req.header.get(.connection) { if connection_header.to_lower() == 'close' { @@ -124,13 +129,15 @@ fn handle_request_and_route[A, X](mut app A, req http.Request, client_fd int, ro } // Match controller paths first $if A is ControllerInterface { - if completed_context := handle_controllers[X](controllers, ctx, mut url, host) { + if completed_context := handle_controllers[X](params.controllers_sorted, ctx, mut + url, host) + { return completed_context } } // Create a new user context and pass veb's context mut user_context := X{} user_context.Context = ctx - handle_route[A, X](mut app, mut user_context, url, host, routes) + handle_route[A, X](mut app, mut user_context, url, host, params.routes) return &user_context.Context } -- 2.39.5