@tishlang/tish-format 1.0.12 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
//! **tishlang_pg** — PostgreSQL for the **Tish native runtime** only.
|
|
2
|
+
//!
|
|
3
|
+
//! - **No Node.js**, **no N-API**, **no JavaScript** in this crate or its distribution.
|
|
4
|
+
//! - Uses **Rust** [`tokio-postgres`](https://docs.rs/tokio-postgres) + [`deadpool-postgres`](https://docs.rs/deadpool-postgres) for protocol and pooling.
|
|
5
|
+
//! - The **Tish compiler / runtime** (`tishlang/tish`) links this `rlib` and exposes a `pg` (or `tish_pg`) module to **`.tish` application code** — same *call shape* as node-postgres where practical (`Pool`, `query`, `rows`, `rowCount`).
|
|
6
|
+
//!
|
|
7
|
+
//! Application authors write **only `.tish`**; they never touch this Rust API directly once bindings exist upstream.
|
|
8
|
+
use tishlang_runtime::VmRef;
|
|
9
|
+
|
|
10
|
+
mod error;
|
|
11
|
+
pub use error::{format_pg_error, format_tish_pg_error, Result, TishPgError};
|
|
12
|
+
|
|
13
|
+
use deadpool_postgres::{Manager, Pool, Runtime};
|
|
14
|
+
use serde::{Deserialize, Serialize};
|
|
15
|
+
use serde_json::{json, Map, Value as JsonValue};
|
|
16
|
+
use tokio_postgres::{types::ToSql, NoTls, Row};
|
|
17
|
+
|
|
18
|
+
/// Configuration mirroring the common `pg` / `node-postgres` `Pool` constructor shape.
|
|
19
|
+
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
20
|
+
pub struct PoolConfig {
|
|
21
|
+
pub connection_string: String,
|
|
22
|
+
#[serde(default)]
|
|
23
|
+
pub max: Option<u32>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[derive(Debug, Clone, Serialize)]
|
|
27
|
+
pub struct FieldInfo {
|
|
28
|
+
pub name: String,
|
|
29
|
+
pub data_type_id: i32,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone, Serialize)]
|
|
33
|
+
pub struct QueryResult {
|
|
34
|
+
pub rows: Vec<JsonValue>,
|
|
35
|
+
pub row_count: u32,
|
|
36
|
+
pub fields: Vec<FieldInfo>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Rust-side pool; Tish maps this to `Pool` in user code.
|
|
40
|
+
#[derive(Clone)]
|
|
41
|
+
pub struct PgPool {
|
|
42
|
+
inner: Pool,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn parse_connection_string(cs: &str) -> std::result::Result<tokio_postgres::Config, String> {
|
|
46
|
+
let u = url::Url::parse(cs).map_err(|e| e.to_string())?;
|
|
47
|
+
if u.scheme() != "postgres" && u.scheme() != "postgresql" {
|
|
48
|
+
return Err("URL must use postgres:// or postgresql://".into());
|
|
49
|
+
}
|
|
50
|
+
let mut cfg = tokio_postgres::Config::new();
|
|
51
|
+
if let Some(host) = u.host_str() {
|
|
52
|
+
cfg.host(host);
|
|
53
|
+
}
|
|
54
|
+
if let Some(p) = u.port() {
|
|
55
|
+
cfg.port(p);
|
|
56
|
+
}
|
|
57
|
+
let path = u.path().trim_start_matches('/');
|
|
58
|
+
if !path.is_empty() {
|
|
59
|
+
cfg.dbname(path);
|
|
60
|
+
}
|
|
61
|
+
if !u.username().is_empty() {
|
|
62
|
+
cfg.user(u.username());
|
|
63
|
+
}
|
|
64
|
+
if let Some(pw) = u.password() {
|
|
65
|
+
cfg.password(pw);
|
|
66
|
+
}
|
|
67
|
+
Ok(cfg)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn params_to_sql(values: &[JsonValue]) -> Result<Vec<Box<dyn ToSql + Sync + Send>>> {
|
|
71
|
+
let mut out: Vec<Box<dyn ToSql + Sync + Send>> = Vec::with_capacity(values.len());
|
|
72
|
+
for v in values {
|
|
73
|
+
let b: Box<dyn ToSql + Sync + Send> = match v {
|
|
74
|
+
JsonValue::Null => Box::new(Option::<String>::None),
|
|
75
|
+
JsonValue::Bool(b) => Box::new(*b),
|
|
76
|
+
JsonValue::Number(n) => {
|
|
77
|
+
// tokio-postgres type-checks against the prepared statement's
|
|
78
|
+
// column OIDs. An i64 param against an INT4 column or an f64
|
|
79
|
+
// param against INT4 both error with WrongType. Tish stores
|
|
80
|
+
// all numbers as f64, so we need to detect "whole number in
|
|
81
|
+
// i32 range" and downgrade to i32 so TFB's world.id / world.randomnumber
|
|
82
|
+
// (both INT4) bind correctly. Larger ints fall through to i64
|
|
83
|
+
// (matches INT8). Fractional values stay as f64 (matches FLOAT8).
|
|
84
|
+
if let Some(i) = n.as_i64() {
|
|
85
|
+
if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
|
|
86
|
+
Box::new(i as i32)
|
|
87
|
+
} else {
|
|
88
|
+
Box::new(i)
|
|
89
|
+
}
|
|
90
|
+
} else if let Some(u) = n.as_u64() {
|
|
91
|
+
Box::new(u as i64)
|
|
92
|
+
} else if let Some(f) = n.as_f64() {
|
|
93
|
+
if f.fract() == 0.0 && f >= i32::MIN as f64 && f <= i32::MAX as f64 {
|
|
94
|
+
Box::new(f as i32)
|
|
95
|
+
} else if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
|
|
96
|
+
Box::new(f as i64)
|
|
97
|
+
} else {
|
|
98
|
+
Box::new(f)
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
return Err(TishPgError::BadParam(
|
|
102
|
+
"invalid JSON number for SQL param".into(),
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
JsonValue::String(s) => Box::new(s.clone()),
|
|
107
|
+
JsonValue::Array(_) | JsonValue::Object(_) => {
|
|
108
|
+
Box::new(tokio_postgres::types::Json(v.clone()))
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
out.push(b);
|
|
112
|
+
}
|
|
113
|
+
Ok(out)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Convert a Postgres `Row` directly into a Tish `Value::Object`,
|
|
117
|
+
/// skipping the `JsonValue` intermediate that `row_to_object` produces.
|
|
118
|
+
///
|
|
119
|
+
/// Two wins on the hot path:
|
|
120
|
+
///
|
|
121
|
+
/// 1. **One pass instead of two.** The blanket `Row → JsonValue → Value`
|
|
122
|
+
/// path allocated a `serde_json::Map<String, JsonValue>` and then
|
|
123
|
+
/// re-walked it into an `ObjectMap<Arc<str>, Value>`. This produces
|
|
124
|
+
/// the `ObjectMap` in one shot.
|
|
125
|
+
/// 2. **Interned column names.** TFB hits the same `id`/`randomnumber`/
|
|
126
|
+
/// `message` keys ~140k times per second; the per-row `Arc::from(name)`
|
|
127
|
+
/// became a measurable allocator load. We cache one `Arc<str>` per
|
|
128
|
+
/// `(stmt-prepared-once)` column name in a thread-local, so subsequent
|
|
129
|
+
/// rows reuse the same `Arc` (one atomic ref-count bump, no allocation).
|
|
130
|
+
///
|
|
131
|
+
/// Used by [`PerWorkerClient::query_prepared_to_value`] and
|
|
132
|
+
/// [`PerWorkerClient::query_batch_to_values`] (added below).
|
|
133
|
+
fn row_to_value_direct(row: &Row) -> tishlang_runtime::Value {
|
|
134
|
+
use std::cell::RefCell;
|
|
135
|
+
use std::collections::HashMap;
|
|
136
|
+
use std::sync::Arc as StdArcInner;
|
|
137
|
+
use tishlang_runtime::ObjectMap;
|
|
138
|
+
use tishlang_runtime::Value as RtValue;
|
|
139
|
+
use tokio_postgres::types::Type;
|
|
140
|
+
|
|
141
|
+
thread_local! {
|
|
142
|
+
// Per-worker thread cache of column-name interned `Arc<str>`s.
|
|
143
|
+
// Bounded by total distinct PG column names the app prepares
|
|
144
|
+
// statements for, i.e. tiny — so unconditional retention is fine.
|
|
145
|
+
static KEY_CACHE: RefCell<HashMap<String, StdArcInner<str>>> =
|
|
146
|
+
RefCell::new(HashMap::new());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn intern_key(name: &str) -> StdArcInner<str> {
|
|
150
|
+
KEY_CACHE.with(|cache| {
|
|
151
|
+
let mut cache = cache.borrow_mut();
|
|
152
|
+
if let Some(k) = cache.get(name) {
|
|
153
|
+
return StdArcInner::clone(k);
|
|
154
|
+
}
|
|
155
|
+
let k: StdArcInner<str> = StdArcInner::from(name);
|
|
156
|
+
cache.insert(name.to_string(), StdArcInner::clone(&k));
|
|
157
|
+
k
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let cols = row.columns();
|
|
162
|
+
let mut om = ObjectMap::with_capacity(cols.len());
|
|
163
|
+
for (i, col) in cols.iter().enumerate() {
|
|
164
|
+
let key = intern_key(col.name());
|
|
165
|
+
let v: RtValue = match *col.type_() {
|
|
166
|
+
Type::INT2 => row
|
|
167
|
+
.try_get::<_, Option<i16>>(i)
|
|
168
|
+
.ok()
|
|
169
|
+
.flatten()
|
|
170
|
+
.map(|n| RtValue::Number(n as f64))
|
|
171
|
+
.unwrap_or(RtValue::Null),
|
|
172
|
+
Type::INT4 | Type::OID => row
|
|
173
|
+
.try_get::<_, Option<i32>>(i)
|
|
174
|
+
.ok()
|
|
175
|
+
.flatten()
|
|
176
|
+
.map(|n| RtValue::Number(n as f64))
|
|
177
|
+
.unwrap_or(RtValue::Null),
|
|
178
|
+
Type::INT8 => row
|
|
179
|
+
.try_get::<_, Option<i64>>(i)
|
|
180
|
+
.ok()
|
|
181
|
+
.flatten()
|
|
182
|
+
.map(|n| RtValue::Number(n as f64))
|
|
183
|
+
.unwrap_or(RtValue::Null),
|
|
184
|
+
Type::FLOAT4 => row
|
|
185
|
+
.try_get::<_, Option<f32>>(i)
|
|
186
|
+
.ok()
|
|
187
|
+
.flatten()
|
|
188
|
+
.map(|n| RtValue::Number(n as f64))
|
|
189
|
+
.unwrap_or(RtValue::Null),
|
|
190
|
+
Type::FLOAT8 => row
|
|
191
|
+
.try_get::<_, Option<f64>>(i)
|
|
192
|
+
.ok()
|
|
193
|
+
.flatten()
|
|
194
|
+
.map(RtValue::Number)
|
|
195
|
+
.unwrap_or(RtValue::Null),
|
|
196
|
+
Type::BOOL => row
|
|
197
|
+
.try_get::<_, Option<bool>>(i)
|
|
198
|
+
.ok()
|
|
199
|
+
.flatten()
|
|
200
|
+
.map(RtValue::Bool)
|
|
201
|
+
.unwrap_or(RtValue::Null),
|
|
202
|
+
Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME => row
|
|
203
|
+
.try_get::<_, Option<&str>>(i)
|
|
204
|
+
.ok()
|
|
205
|
+
.flatten()
|
|
206
|
+
.map(|s| RtValue::String(tishlang_runtime::ArcStr::from(s)))
|
|
207
|
+
.unwrap_or(RtValue::Null),
|
|
208
|
+
_ => {
|
|
209
|
+
// Anything else goes through the JSON path for backwards
|
|
210
|
+
// compat. Hot TFB rows never hit this branch.
|
|
211
|
+
if let Ok(s) = row.try_get::<_, Option<String>>(i) {
|
|
212
|
+
s.map(|s| RtValue::String(tishlang_runtime::ArcStr::from(s.as_str())))
|
|
213
|
+
.unwrap_or(RtValue::Null)
|
|
214
|
+
} else {
|
|
215
|
+
RtValue::Null
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
om.insert(key, v);
|
|
220
|
+
}
|
|
221
|
+
RtValue::object(om)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn row_to_object(row: &Row) -> Result<JsonValue> {
|
|
225
|
+
use tokio_postgres::types::Type;
|
|
226
|
+
let mut map = Map::with_capacity(row.columns().len());
|
|
227
|
+
for (i, col) in row.columns().iter().enumerate() {
|
|
228
|
+
let name = col.name().to_string();
|
|
229
|
+
// Phase-2 item 10: type-directed decode for the common Postgres wire
|
|
230
|
+
// types so we skip the text->parse->int detour that the blind
|
|
231
|
+
// try_get cascade triggered. Hot TFB columns are INT4 (world.id,
|
|
232
|
+
// world.randomnumber) and TEXT/VARCHAR (fortune.message).
|
|
233
|
+
let val: JsonValue = match *col.type_() {
|
|
234
|
+
Type::INT2 => row
|
|
235
|
+
.try_get::<_, Option<i16>>(i)
|
|
236
|
+
.map(|v| json!(v))
|
|
237
|
+
.unwrap_or(JsonValue::Null),
|
|
238
|
+
Type::INT4 | Type::OID => row
|
|
239
|
+
.try_get::<_, Option<i32>>(i)
|
|
240
|
+
.map(|v| json!(v))
|
|
241
|
+
.unwrap_or(JsonValue::Null),
|
|
242
|
+
Type::INT8 => row
|
|
243
|
+
.try_get::<_, Option<i64>>(i)
|
|
244
|
+
.map(|v| json!(v))
|
|
245
|
+
.unwrap_or(JsonValue::Null),
|
|
246
|
+
Type::FLOAT4 => row
|
|
247
|
+
.try_get::<_, Option<f32>>(i)
|
|
248
|
+
.map(|v| json!(v.map(|f| f as f64)))
|
|
249
|
+
.unwrap_or(JsonValue::Null),
|
|
250
|
+
Type::FLOAT8 => row
|
|
251
|
+
.try_get::<_, Option<f64>>(i)
|
|
252
|
+
.map(|v| json!(v))
|
|
253
|
+
.unwrap_or(JsonValue::Null),
|
|
254
|
+
Type::BOOL => row
|
|
255
|
+
.try_get::<_, Option<bool>>(i)
|
|
256
|
+
.map(|v| json!(v))
|
|
257
|
+
.unwrap_or(JsonValue::Null),
|
|
258
|
+
Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME => row
|
|
259
|
+
.try_get::<_, Option<String>>(i)
|
|
260
|
+
.map(|v| json!(v))
|
|
261
|
+
.unwrap_or(JsonValue::Null),
|
|
262
|
+
Type::JSON | Type::JSONB => row
|
|
263
|
+
.try_get::<_, Option<JsonValue>>(i)
|
|
264
|
+
.unwrap_or(None)
|
|
265
|
+
.unwrap_or(JsonValue::Null),
|
|
266
|
+
Type::BYTEA => row
|
|
267
|
+
.try_get::<_, Option<Vec<u8>>>(i)
|
|
268
|
+
.map(|v| json!(v))
|
|
269
|
+
.unwrap_or(JsonValue::Null),
|
|
270
|
+
_ => {
|
|
271
|
+
// Fall back to the old try-cascade for anything we haven't
|
|
272
|
+
// listed explicitly yet.
|
|
273
|
+
if let Ok(v) = row.try_get::<_, Option<String>>(i) {
|
|
274
|
+
json!(v)
|
|
275
|
+
} else if let Ok(v) = row.try_get::<_, Option<i64>>(i) {
|
|
276
|
+
json!(v)
|
|
277
|
+
} else if let Ok(v) = row.try_get::<_, Option<f64>>(i) {
|
|
278
|
+
json!(v)
|
|
279
|
+
} else if let Ok(v) = row.try_get::<_, Option<bool>>(i) {
|
|
280
|
+
json!(v)
|
|
281
|
+
} else if let Ok(v) = row.try_get::<_, Option<JsonValue>>(i) {
|
|
282
|
+
v.unwrap_or(JsonValue::Null)
|
|
283
|
+
} else if let Ok(v) = row.try_get::<_, Option<Vec<u8>>>(i) {
|
|
284
|
+
json!(v)
|
|
285
|
+
} else {
|
|
286
|
+
JsonValue::String(format!("<decode:{}>", col.type_().name()))
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
map.insert(name, val);
|
|
291
|
+
}
|
|
292
|
+
Ok(JsonValue::Object(map))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
impl PgPool {
|
|
296
|
+
pub async fn connect(cfg: PoolConfig) -> Result<Self> {
|
|
297
|
+
let pg_cfg = parse_connection_string(&cfg.connection_string)
|
|
298
|
+
.map_err(TishPgError::BadConnectionString)?;
|
|
299
|
+
let mgr = Manager::new(pg_cfg, NoTls);
|
|
300
|
+
let mut b = Pool::builder(mgr).runtime(Runtime::Tokio1);
|
|
301
|
+
if let Some(m) = cfg.max {
|
|
302
|
+
b = b.max_size(m as usize);
|
|
303
|
+
}
|
|
304
|
+
let inner = b.build()?;
|
|
305
|
+
Ok(Self { inner })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Same logical contract as `pool.query(text, params)` in node-postgres.
|
|
309
|
+
pub async fn query(&self, text: &str, params: &[JsonValue]) -> Result<QueryResult> {
|
|
310
|
+
let sql_values = params_to_sql(params)?;
|
|
311
|
+
let refs: Vec<&(dyn ToSql + Sync)> = sql_values
|
|
312
|
+
.iter()
|
|
313
|
+
.map(|b| b.as_ref() as &(dyn ToSql + Sync))
|
|
314
|
+
.collect();
|
|
315
|
+
|
|
316
|
+
let client = self.inner.get().await?;
|
|
317
|
+
let rows = client.query(text, &refs[..]).await?;
|
|
318
|
+
|
|
319
|
+
let mut fields = Vec::new();
|
|
320
|
+
if let Some(first) = rows.first() {
|
|
321
|
+
for col in first.columns().iter() {
|
|
322
|
+
fields.push(FieldInfo {
|
|
323
|
+
name: col.name().to_string(),
|
|
324
|
+
data_type_id: col.type_().oid() as i32,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let mut out_rows = Vec::with_capacity(rows.len());
|
|
330
|
+
for r in &rows {
|
|
331
|
+
out_rows.push(row_to_object(r)?);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
Ok(QueryResult {
|
|
335
|
+
row_count: rows.len() as u32,
|
|
336
|
+
rows: out_rows,
|
|
337
|
+
fields,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Close the pool (mirrors `pool.end()`).
|
|
342
|
+
pub fn close(&self) {
|
|
343
|
+
self.inner.close();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// PerWorkerClient + prepared-statement surface (Phase-1 item 3)
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
//
|
|
351
|
+
// The Tish bench needs a dedicated Postgres connection per HTTP worker so the
|
|
352
|
+
// hot path is:
|
|
353
|
+
//
|
|
354
|
+
// worker 0 -> client 0 (1 TCP socket) -> pipelined Parse/Bind/Execute/Sync
|
|
355
|
+
// worker N -> client N
|
|
356
|
+
//
|
|
357
|
+
// `tokio-postgres` pipelines automatically when multiple futures on the same
|
|
358
|
+
// `Client` are polled concurrently, so we expose a batch-query primitive that
|
|
359
|
+
// creates N futures under the hood and `try_join_all`s them.
|
|
360
|
+
|
|
361
|
+
use std::sync::Arc as StdArc;
|
|
362
|
+
use tokio::task::JoinHandle;
|
|
363
|
+
use tokio_postgres::{Client, Statement};
|
|
364
|
+
|
|
365
|
+
/// Dedicated tokio-postgres client. Cheap to clone (internal Arc).
|
|
366
|
+
#[derive(Clone)]
|
|
367
|
+
pub struct PerWorkerClient {
|
|
368
|
+
inner: StdArc<Client>,
|
|
369
|
+
// Background task that drives the connection; we keep it alive for the
|
|
370
|
+
// lifetime of the client.
|
|
371
|
+
_driver: StdArc<JoinHandle<()>>,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
impl PerWorkerClient {
|
|
375
|
+
/// Open a single direct connection (no pool).
|
|
376
|
+
pub async fn connect(connection_string: &str) -> Result<Self> {
|
|
377
|
+
let cfg =
|
|
378
|
+
parse_connection_string(connection_string).map_err(TishPgError::BadConnectionString)?;
|
|
379
|
+
let (client, connection) = cfg.connect(NoTls).await?;
|
|
380
|
+
let driver = tokio::spawn(async move {
|
|
381
|
+
if let Err(e) = connection.await {
|
|
382
|
+
eprintln!("[tish_pg] connection driver exited: {}", e);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
Ok(Self {
|
|
386
|
+
inner: StdArc::new(client),
|
|
387
|
+
_driver: StdArc::new(driver),
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
pub fn client(&self) -> &Client {
|
|
392
|
+
&self.inner
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Prepare a named statement. Handle is cheap to clone.
|
|
396
|
+
pub async fn prepare(&self, text: &str) -> Result<Statement> {
|
|
397
|
+
Ok(self.inner.prepare(text).await?)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// Run one prepared query -> rows as JSON.
|
|
401
|
+
pub async fn query_prepared(
|
|
402
|
+
&self,
|
|
403
|
+
stmt: &Statement,
|
|
404
|
+
params: &[JsonValue],
|
|
405
|
+
) -> Result<QueryResult> {
|
|
406
|
+
let sql_values = params_to_sql(params)?;
|
|
407
|
+
let refs: Vec<&(dyn ToSql + Sync)> = sql_values
|
|
408
|
+
.iter()
|
|
409
|
+
.map(|b| b.as_ref() as &(dyn ToSql + Sync))
|
|
410
|
+
.collect();
|
|
411
|
+
let rows = self.inner.query(stmt, &refs[..]).await?;
|
|
412
|
+
let mut fields = Vec::new();
|
|
413
|
+
if let Some(first) = rows.first() {
|
|
414
|
+
for col in first.columns().iter() {
|
|
415
|
+
fields.push(FieldInfo {
|
|
416
|
+
name: col.name().to_string(),
|
|
417
|
+
data_type_id: col.type_().oid() as i32,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
let mut out_rows = Vec::with_capacity(rows.len());
|
|
422
|
+
for r in &rows {
|
|
423
|
+
out_rows.push(row_to_object(r)?);
|
|
424
|
+
}
|
|
425
|
+
Ok(QueryResult {
|
|
426
|
+
row_count: rows.len() as u32,
|
|
427
|
+
rows: out_rows,
|
|
428
|
+
fields,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Fire N prepared queries against this client concurrently and await them
|
|
433
|
+
/// together. Triggers `tokio-postgres`'s automatic pipelining: all Parse/
|
|
434
|
+
/// Bind/Execute/Sync messages are written back-to-back in one TCP batch
|
|
435
|
+
/// and the server executes them sequentially while the client never waits
|
|
436
|
+
/// for per-query round-trips.
|
|
437
|
+
pub async fn query_batch(
|
|
438
|
+
&self,
|
|
439
|
+
specs: Vec<(Statement, Vec<JsonValue>)>,
|
|
440
|
+
) -> Result<Vec<QueryResult>> {
|
|
441
|
+
use futures::future::try_join_all;
|
|
442
|
+
let futs: Vec<_> = specs
|
|
443
|
+
.into_iter()
|
|
444
|
+
.map(|(stmt, params)| {
|
|
445
|
+
let client = StdArc::clone(&self.inner);
|
|
446
|
+
async move {
|
|
447
|
+
let sql_values = params_to_sql(¶ms)?;
|
|
448
|
+
let refs: Vec<&(dyn ToSql + Sync)> = sql_values
|
|
449
|
+
.iter()
|
|
450
|
+
.map(|b| b.as_ref() as &(dyn ToSql + Sync))
|
|
451
|
+
.collect();
|
|
452
|
+
let rows = client.query(&stmt, &refs[..]).await?;
|
|
453
|
+
let mut out_rows = Vec::with_capacity(rows.len());
|
|
454
|
+
for r in &rows {
|
|
455
|
+
out_rows.push(row_to_object(r)?);
|
|
456
|
+
}
|
|
457
|
+
Ok::<_, TishPgError>(QueryResult {
|
|
458
|
+
row_count: rows.len() as u32,
|
|
459
|
+
rows: out_rows,
|
|
460
|
+
fields: Vec::new(),
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
.collect();
|
|
465
|
+
try_join_all(futs).await
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// Like [`query_prepared`] but emits `Vec<tishlang_runtime::Value>`
|
|
469
|
+
/// directly using the [`row_to_value_direct`] fast path — no
|
|
470
|
+
/// `serde_json::Value` intermediate, no per-row column-name `Arc`
|
|
471
|
+
/// allocations. The hot TFB call site for `/db` and `/queries`.
|
|
472
|
+
pub async fn query_prepared_to_values(
|
|
473
|
+
&self,
|
|
474
|
+
stmt: &Statement,
|
|
475
|
+
params: &[JsonValue],
|
|
476
|
+
) -> Result<Vec<tishlang_runtime::Value>> {
|
|
477
|
+
let sql_values = params_to_sql(params)?;
|
|
478
|
+
let refs: Vec<&(dyn ToSql + Sync)> = sql_values
|
|
479
|
+
.iter()
|
|
480
|
+
.map(|b| b.as_ref() as &(dyn ToSql + Sync))
|
|
481
|
+
.collect();
|
|
482
|
+
let rows = self.inner.query(stmt, &refs[..]).await?;
|
|
483
|
+
let mut out = Vec::with_capacity(rows.len());
|
|
484
|
+
for r in &rows {
|
|
485
|
+
out.push(row_to_value_direct(r));
|
|
486
|
+
}
|
|
487
|
+
Ok(out)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// Pipelined batch of typed queries — the equivalent of
|
|
491
|
+
/// [`query_batch`] for the typed (no-JSON) path.
|
|
492
|
+
pub async fn query_batch_to_values(
|
|
493
|
+
&self,
|
|
494
|
+
specs: Vec<(Statement, Vec<JsonValue>)>,
|
|
495
|
+
) -> Result<Vec<Vec<tishlang_runtime::Value>>> {
|
|
496
|
+
use futures::future::try_join_all;
|
|
497
|
+
let futs: Vec<_> = specs
|
|
498
|
+
.into_iter()
|
|
499
|
+
.map(|(stmt, params)| {
|
|
500
|
+
let client = StdArc::clone(&self.inner);
|
|
501
|
+
async move {
|
|
502
|
+
let sql_values = params_to_sql(¶ms)?;
|
|
503
|
+
let refs: Vec<&(dyn ToSql + Sync)> = sql_values
|
|
504
|
+
.iter()
|
|
505
|
+
.map(|b| b.as_ref() as &(dyn ToSql + Sync))
|
|
506
|
+
.collect();
|
|
507
|
+
let rows = client.query(&stmt, &refs[..]).await?;
|
|
508
|
+
let mut out = Vec::with_capacity(rows.len());
|
|
509
|
+
for r in &rows {
|
|
510
|
+
out.push(row_to_value_direct(r));
|
|
511
|
+
}
|
|
512
|
+
Ok::<_, TishPgError>(out)
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
.collect();
|
|
516
|
+
try_join_all(futs).await
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// Sync facade for the `cargo:tish_pg` Tish import (feature = tish-bindings)
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
#[cfg(feature = "tish-bindings")]
|
|
525
|
+
mod tish_sync {
|
|
526
|
+
use super::*;
|
|
527
|
+
use once_cell::sync::Lazy;
|
|
528
|
+
use slab::Slab;
|
|
529
|
+
use tishlang_runtime::Value as TishValue;
|
|
530
|
+
use tokio::runtime::Runtime as TokioRuntime;
|
|
531
|
+
|
|
532
|
+
static RT: Lazy<TokioRuntime> =
|
|
533
|
+
Lazy::new(|| TokioRuntime::new().expect("tish_pg: failed to build tokio runtime"));
|
|
534
|
+
// `RwLock` (not `Mutex`) on the registries: hot path is read-only —
|
|
535
|
+
// every query does `get_client(id)` + `get_statement(id)`. Inserts
|
|
536
|
+
// happen once per `connect`/`prepare` at startup. With multiple HTTP
|
|
537
|
+
// worker threads each doing thousands of QPS, the prior `Mutex<Slab>`
|
|
538
|
+
// serialised every query through one global lock; `RwLock` lets all
|
|
539
|
+
// concurrent reads run lock-free against each other.
|
|
540
|
+
use std::sync::RwLock;
|
|
541
|
+
static CLIENTS: Lazy<RwLock<Slab<PerWorkerClient>>> = Lazy::new(|| RwLock::new(Slab::new()));
|
|
542
|
+
static STATEMENTS: Lazy<RwLock<Slab<(usize, Statement)>>> =
|
|
543
|
+
Lazy::new(|| RwLock::new(Slab::new()));
|
|
544
|
+
|
|
545
|
+
/// Drive a future on our tokio runtime without panicking when called from
|
|
546
|
+
/// inside another runtime's worker thread.
|
|
547
|
+
///
|
|
548
|
+
/// Fast path: the Tish HTTP handler runs on the VM dispatcher thread,
|
|
549
|
+
/// which is NOT inside a tokio runtime — so we can enter `RT.block_on`
|
|
550
|
+
/// directly, no thread spawn. Measured cost: ~50-100μs per call saved
|
|
551
|
+
/// (and a thread creation per request is what was capping /db RPS).
|
|
552
|
+
///
|
|
553
|
+
/// Slow path: if we detect an ambient tokio runtime (e.g. someone is
|
|
554
|
+
/// calling us from inside an async context like Tish's top-level
|
|
555
|
+
/// `await`), we fall back to a scoped thread to avoid the
|
|
556
|
+
/// "Cannot start a runtime from within a runtime" panic.
|
|
557
|
+
fn block_on<F>(fut: F) -> F::Output
|
|
558
|
+
where
|
|
559
|
+
F: std::future::Future + Send,
|
|
560
|
+
F::Output: Send,
|
|
561
|
+
{
|
|
562
|
+
// Whether we're inside a tokio runtime never changes for a given
|
|
563
|
+
// OS thread — Tish runs handlers on the VM dispatcher thread,
|
|
564
|
+
// which is a plain `std::thread` (so the answer is "no, no
|
|
565
|
+
// ambient runtime"). Cache the result per-thread so the
|
|
566
|
+
// `try_current()` syscall-ish call doesn't repeat thousands of
|
|
567
|
+
// times per second per worker.
|
|
568
|
+
use std::cell::Cell;
|
|
569
|
+
thread_local! {
|
|
570
|
+
static AMBIENT_RT: Cell<Option<bool>> = const { Cell::new(None) };
|
|
571
|
+
}
|
|
572
|
+
let in_ambient = AMBIENT_RT.with(|c| {
|
|
573
|
+
if let Some(v) = c.get() {
|
|
574
|
+
return v;
|
|
575
|
+
}
|
|
576
|
+
let v = tokio::runtime::Handle::try_current().is_ok();
|
|
577
|
+
c.set(Some(v));
|
|
578
|
+
v
|
|
579
|
+
});
|
|
580
|
+
if in_ambient {
|
|
581
|
+
// Ambient runtime present — spawn a thread so RT.block_on does
|
|
582
|
+
// not nest.
|
|
583
|
+
return std::thread::scope(|s| {
|
|
584
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
585
|
+
s.spawn(move || {
|
|
586
|
+
let out = RT.block_on(fut);
|
|
587
|
+
let _ = tx.send(out);
|
|
588
|
+
});
|
|
589
|
+
rx.recv().expect("tish_pg::block_on thread panicked")
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
// Hot path: we're on a plain OS thread (the Tish VM dispatcher),
|
|
593
|
+
// enter tokio directly.
|
|
594
|
+
RT.block_on(fut)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
fn tish_to_json(v: &TishValue) -> JsonValue {
|
|
598
|
+
match v {
|
|
599
|
+
TishValue::Null => JsonValue::Null,
|
|
600
|
+
TishValue::Bool(b) => JsonValue::Bool(*b),
|
|
601
|
+
TishValue::Number(n) => serde_json::Number::from_f64(*n)
|
|
602
|
+
.map(JsonValue::Number)
|
|
603
|
+
.unwrap_or(JsonValue::Null),
|
|
604
|
+
TishValue::String(s) => JsonValue::String(s.to_string()),
|
|
605
|
+
TishValue::Array(a) => JsonValue::Array(a.borrow().iter().map(tish_to_json).collect()),
|
|
606
|
+
TishValue::Object(o) => {
|
|
607
|
+
let mut m = serde_json::Map::new();
|
|
608
|
+
for (k, v) in o.borrow().strings.iter() {
|
|
609
|
+
m.insert(k.to_string(), tish_to_json(v));
|
|
610
|
+
}
|
|
611
|
+
JsonValue::Object(m)
|
|
612
|
+
}
|
|
613
|
+
_ => JsonValue::Null,
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// fn json_to_tish(v: JsonValue) -> TishValue {
|
|
618
|
+
// use std::cell::RefCell;
|
|
619
|
+
// use std::rc::Rc;
|
|
620
|
+
// use std::sync::Arc;
|
|
621
|
+
// use tishlang_runtime::ObjectMap;
|
|
622
|
+
// match v {
|
|
623
|
+
// JsonValue::Null => TishValue::Null,
|
|
624
|
+
// JsonValue::Bool(b) => TishValue::Bool(b),
|
|
625
|
+
// JsonValue::Number(n) => TishValue::Number(n.as_f64().unwrap_or(0.0)),
|
|
626
|
+
// JsonValue::String(s) => TishValue::String(s.into()),
|
|
627
|
+
// JsonValue::Array(a) => {
|
|
628
|
+
// let mut out = Vec::with_capacity(a.len());
|
|
629
|
+
// for item in a {
|
|
630
|
+
// out.push(json_to_tish(item));
|
|
631
|
+
// }
|
|
632
|
+
// TishValue::Array(VmRef::new(out))
|
|
633
|
+
// }
|
|
634
|
+
// JsonValue::Object(m) => {
|
|
635
|
+
// // Pre-allocate ObjectMap capacity so HashMap doesn't rehash
|
|
636
|
+
// // on every insert. Common TFB rows are 2 columns (id,
|
|
637
|
+
// // randomnumber or id, message).
|
|
638
|
+
// let mut om = ObjectMap::with_capacity(m.len());
|
|
639
|
+
// for (k, v) in m {
|
|
640
|
+
// om.insert(Arc::from(k), json_to_tish(v));
|
|
641
|
+
// }
|
|
642
|
+
// TishValue::object(om)
|
|
643
|
+
// }
|
|
644
|
+
// }
|
|
645
|
+
// }
|
|
646
|
+
|
|
647
|
+
fn tish_err(msg: impl Into<String>) -> TishValue {
|
|
648
|
+
use std::sync::Arc;
|
|
649
|
+
use tishlang_runtime::ObjectMap;
|
|
650
|
+
let mut om = ObjectMap::with_capacity(2);
|
|
651
|
+
om.insert(Arc::from("error"), TishValue::String(msg.into().into()));
|
|
652
|
+
om.insert(Arc::from("ok"), TishValue::Bool(false));
|
|
653
|
+
TishValue::object(om)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// fn rows_to_value(res: QueryResult) -> TishValue {
|
|
657
|
+
// use std::cell::RefCell;
|
|
658
|
+
// use std::rc::Rc;
|
|
659
|
+
// TishValue::Array(VmRef::new(res.rows.into_iter().map(json_to_tish).collect()))
|
|
660
|
+
// }
|
|
661
|
+
|
|
662
|
+
/// `perWorkerClient(connection_string) -> client_handle` (blocking).
|
|
663
|
+
pub fn per_worker_client(args: &[TishValue]) -> TishValue {
|
|
664
|
+
let cs = match args.first() {
|
|
665
|
+
Some(TishValue::String(s)) => s.to_string(),
|
|
666
|
+
_ => return tish_err("perWorkerClient: expected connection string"),
|
|
667
|
+
};
|
|
668
|
+
match block_on(PerWorkerClient::connect(&cs)) {
|
|
669
|
+
Ok(c) => {
|
|
670
|
+
let mut g = CLIENTS.write().unwrap();
|
|
671
|
+
let id = g.insert(c);
|
|
672
|
+
TishValue::Number(id as f64)
|
|
673
|
+
}
|
|
674
|
+
Err(e) => tish_err(format!(
|
|
675
|
+
"perWorkerClient: {}",
|
|
676
|
+
crate::format_tish_pg_error(&e)
|
|
677
|
+
)),
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/// `connect(options) -> client_handle`.
|
|
682
|
+
/// Aliased to per_worker_client for now; Pool-based variant is still
|
|
683
|
+
/// reachable via the async Rust API.
|
|
684
|
+
pub fn connect(args: &[TishValue]) -> TishValue {
|
|
685
|
+
// Accept either a plain connection string or `{ connectionString }`.
|
|
686
|
+
let cs = match args.first() {
|
|
687
|
+
Some(TishValue::String(s)) => s.to_string(),
|
|
688
|
+
Some(TishValue::Object(obj)) => {
|
|
689
|
+
let b = obj.borrow();
|
|
690
|
+
match b.strings.get("connectionString") {
|
|
691
|
+
Some(TishValue::String(s)) => s.to_string(),
|
|
692
|
+
_ => return tish_err("connect: options.connectionString missing"),
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
_ => return tish_err("connect: expected connection string or options"),
|
|
696
|
+
};
|
|
697
|
+
match block_on(PerWorkerClient::connect(&cs)) {
|
|
698
|
+
Ok(c) => {
|
|
699
|
+
let mut g = CLIENTS.write().unwrap();
|
|
700
|
+
let id = g.insert(c);
|
|
701
|
+
TishValue::Number(id as f64)
|
|
702
|
+
}
|
|
703
|
+
Err(e) => tish_err(format!("connect: {}", crate::format_tish_pg_error(&e))),
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn get_client(id: f64) -> Option<PerWorkerClient> {
|
|
708
|
+
let g = CLIENTS.read().unwrap();
|
|
709
|
+
g.get(id as usize).cloned()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
fn get_statement(id: f64) -> Option<(usize, Statement)> {
|
|
713
|
+
let g = STATEMENTS.read().unwrap();
|
|
714
|
+
g.get(id as usize).cloned()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/// `prepare(client_handle, sql) -> statement_handle`.
|
|
718
|
+
pub fn prepare(args: &[TishValue]) -> TishValue {
|
|
719
|
+
let Some(TishValue::Number(client_id)) = args.first() else {
|
|
720
|
+
return tish_err("prepare: expected (client, sql)");
|
|
721
|
+
};
|
|
722
|
+
let Some(TishValue::String(sql)) = args.get(1) else {
|
|
723
|
+
return tish_err("prepare: expected (client, sql)");
|
|
724
|
+
};
|
|
725
|
+
let Some(client) = get_client(*client_id) else {
|
|
726
|
+
return tish_err("prepare: unknown client handle");
|
|
727
|
+
};
|
|
728
|
+
let sql = sql.to_string();
|
|
729
|
+
match block_on(client.prepare(&sql)) {
|
|
730
|
+
Ok(stmt) => {
|
|
731
|
+
let mut g = STATEMENTS.write().unwrap();
|
|
732
|
+
let id = g.insert((*client_id as usize, stmt));
|
|
733
|
+
TishValue::Number(id as f64)
|
|
734
|
+
}
|
|
735
|
+
Err(e) => tish_err(format!("prepare: {}", crate::format_tish_pg_error(&e))),
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/// `queryPrepared(client, stmt, params) -> rows`.
|
|
740
|
+
pub fn query_prepared(args: &[TishValue]) -> TishValue {
|
|
741
|
+
let Some(TishValue::Number(client_id)) = args.first() else {
|
|
742
|
+
return tish_err("queryPrepared: expected (client, stmt, params)");
|
|
743
|
+
};
|
|
744
|
+
let Some(TishValue::Number(stmt_id)) = args.get(1) else {
|
|
745
|
+
return tish_err("queryPrepared: expected (client, stmt, params)");
|
|
746
|
+
};
|
|
747
|
+
let params = match args.get(2) {
|
|
748
|
+
Some(TishValue::Array(a)) => a.borrow().iter().map(tish_to_json).collect::<Vec<_>>(),
|
|
749
|
+
Some(TishValue::Null) | None => Vec::new(),
|
|
750
|
+
Some(v) => vec![tish_to_json(v)],
|
|
751
|
+
};
|
|
752
|
+
let Some(client) = get_client(*client_id) else {
|
|
753
|
+
return tish_err("queryPrepared: unknown client");
|
|
754
|
+
};
|
|
755
|
+
let Some((_cid, stmt)) = get_statement(*stmt_id) else {
|
|
756
|
+
return tish_err("queryPrepared: unknown statement");
|
|
757
|
+
};
|
|
758
|
+
// Fast path: build `Value::Object` rows directly from `Row` so we
|
|
759
|
+
// skip the `serde_json::Value` intermediate that `query_prepared`
|
|
760
|
+
// produces. Fewer allocations + interned column-name `Arc`s.
|
|
761
|
+
match block_on(client.query_prepared_to_values(&stmt, ¶ms)) {
|
|
762
|
+
Ok(rows) => TishValue::Array(VmRef::new(rows)),
|
|
763
|
+
Err(e) => tish_err(format!(
|
|
764
|
+
"queryPrepared: {}",
|
|
765
|
+
crate::format_tish_pg_error(&e)
|
|
766
|
+
)),
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/// `queryAll(client, specs) -> array_of_row_arrays`.
|
|
771
|
+
/// `specs` is an Array of `[stmt_handle, params_array]` pairs. All are
|
|
772
|
+
/// polled concurrently on the same client to trigger tokio-postgres
|
|
773
|
+
/// automatic pipelining.
|
|
774
|
+
pub fn query_all(args: &[TishValue]) -> TishValue {
|
|
775
|
+
let Some(TishValue::Number(client_id)) = args.first() else {
|
|
776
|
+
return tish_err("queryAll: expected (client, specs[])");
|
|
777
|
+
};
|
|
778
|
+
let Some(TishValue::Array(specs)) = args.get(1) else {
|
|
779
|
+
return tish_err("queryAll: expected (client, specs[])");
|
|
780
|
+
};
|
|
781
|
+
let Some(client) = get_client(*client_id) else {
|
|
782
|
+
return tish_err("queryAll: unknown client");
|
|
783
|
+
};
|
|
784
|
+
let specs_vec: Vec<(Statement, Vec<JsonValue>)> = {
|
|
785
|
+
let borrow = specs.borrow();
|
|
786
|
+
let mut out = Vec::with_capacity(borrow.len());
|
|
787
|
+
for item in borrow.iter() {
|
|
788
|
+
let TishValue::Array(pair) = item else {
|
|
789
|
+
return tish_err("queryAll: each spec must be [stmt, params]");
|
|
790
|
+
};
|
|
791
|
+
let pair_b = pair.borrow();
|
|
792
|
+
let Some(TishValue::Number(stmt_id)) = pair_b.first() else {
|
|
793
|
+
return tish_err("queryAll: spec[0] must be a statement handle");
|
|
794
|
+
};
|
|
795
|
+
let Some((_cid, stmt)) = get_statement(*stmt_id) else {
|
|
796
|
+
return tish_err("queryAll: unknown statement");
|
|
797
|
+
};
|
|
798
|
+
let params = match pair_b.get(1) {
|
|
799
|
+
Some(TishValue::Array(a)) => {
|
|
800
|
+
a.borrow().iter().map(tish_to_json).collect::<Vec<_>>()
|
|
801
|
+
}
|
|
802
|
+
Some(TishValue::Null) | None => Vec::new(),
|
|
803
|
+
Some(v) => vec![tish_to_json(v)],
|
|
804
|
+
};
|
|
805
|
+
out.push((stmt, params));
|
|
806
|
+
}
|
|
807
|
+
out
|
|
808
|
+
};
|
|
809
|
+
// Fast path: same direct `Row -> Value::Object` mapping as
|
|
810
|
+
// `query_prepared` above, but pipelined across all specs.
|
|
811
|
+
match block_on(client.query_batch_to_values(specs_vec)) {
|
|
812
|
+
Ok(results) => {
|
|
813
|
+
let outer: Vec<TishValue> = results
|
|
814
|
+
.into_iter()
|
|
815
|
+
.map(|rows| TishValue::Array(VmRef::new(rows)))
|
|
816
|
+
.collect();
|
|
817
|
+
TishValue::Array(VmRef::new(outer))
|
|
818
|
+
}
|
|
819
|
+
Err(e) => tish_err(format!("queryAll: {}", crate::format_tish_pg_error(&e))),
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/// `close(client_handle) -> null`.
|
|
824
|
+
pub fn close(args: &[TishValue]) -> TishValue {
|
|
825
|
+
if let Some(TishValue::Number(id)) = args.first() {
|
|
826
|
+
let mut g = CLIENTS.write().unwrap();
|
|
827
|
+
if g.contains(*id as usize) {
|
|
828
|
+
g.remove(*id as usize);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
TishValue::Null
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/// `migrate(client_handle, dir) -> { ok, applied: [name, …], error? }`.
|
|
835
|
+
///
|
|
836
|
+
/// Reads `dir` for files matching `^\d+_.+\.sql$`, sorted lexically
|
|
837
|
+
/// (so `001_init.sql` < `002_users.sql` < …), creates a
|
|
838
|
+
/// `_tish_pg_migrations(name TEXT PRIMARY KEY, applied_at TIMESTAMPTZ
|
|
839
|
+
/// NOT NULL DEFAULT NOW())` ledger, and applies each new file in a
|
|
840
|
+
/// single transaction per file. Idempotent — files already recorded
|
|
841
|
+
/// in the ledger are skipped.
|
|
842
|
+
pub fn migrate(args: &[TishValue]) -> TishValue {
|
|
843
|
+
use std::sync::Arc;
|
|
844
|
+
use tishlang_runtime::ObjectMap;
|
|
845
|
+
|
|
846
|
+
let Some(TishValue::Number(client_id)) = args.first() else {
|
|
847
|
+
return tish_err("migrate: expected (client_handle, dir)");
|
|
848
|
+
};
|
|
849
|
+
let Some(TishValue::String(dir)) = args.get(1) else {
|
|
850
|
+
return tish_err("migrate: expected (client_handle, dir)");
|
|
851
|
+
};
|
|
852
|
+
let Some(client) = get_client(*client_id) else {
|
|
853
|
+
return tish_err("migrate: unknown client handle");
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
let dir = std::path::PathBuf::from(dir.as_str());
|
|
857
|
+
let entries = match std::fs::read_dir(&dir) {
|
|
858
|
+
Ok(e) => e,
|
|
859
|
+
Err(e) => return tish_err(format!("migrate: read_dir({:?}): {e}", dir)),
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
let mut files: Vec<(String, std::path::PathBuf)> = Vec::new();
|
|
863
|
+
for ent in entries.flatten() {
|
|
864
|
+
let name = ent.file_name().to_string_lossy().to_string();
|
|
865
|
+
if !name.ends_with(".sql") {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
// accept any file ending in .sql; sort lexically.
|
|
869
|
+
files.push((name, ent.path()));
|
|
870
|
+
}
|
|
871
|
+
files.sort_by(|a, b| a.0.cmp(&b.0));
|
|
872
|
+
|
|
873
|
+
let raw_client = client.client();
|
|
874
|
+
let applied: Vec<TishValue> = match block_on(async {
|
|
875
|
+
// Create ledger
|
|
876
|
+
raw_client
|
|
877
|
+
.batch_execute(
|
|
878
|
+
"CREATE TABLE IF NOT EXISTS _tish_pg_migrations (\
|
|
879
|
+
name TEXT PRIMARY KEY, \
|
|
880
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())",
|
|
881
|
+
)
|
|
882
|
+
.await?;
|
|
883
|
+
|
|
884
|
+
// Read already-applied
|
|
885
|
+
let rows = raw_client
|
|
886
|
+
.query("SELECT name FROM _tish_pg_migrations", &[])
|
|
887
|
+
.await?;
|
|
888
|
+
let already: std::collections::HashSet<String> =
|
|
889
|
+
rows.iter().map(|r| r.get::<_, String>(0)).collect();
|
|
890
|
+
|
|
891
|
+
let mut applied_now: Vec<String> = Vec::new();
|
|
892
|
+
for (name, path) in &files {
|
|
893
|
+
if already.contains(name) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
let sql = match std::fs::read_to_string(path) {
|
|
897
|
+
Ok(s) => s,
|
|
898
|
+
Err(e) => {
|
|
899
|
+
return Err::<_, TishPgError>(TishPgError::BadParam(format!(
|
|
900
|
+
"migrate: read {name}: {e}"
|
|
901
|
+
)))
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
// Each migration runs in its own transaction.
|
|
905
|
+
raw_client.batch_execute("BEGIN").await?;
|
|
906
|
+
let res = raw_client.batch_execute(&sql).await;
|
|
907
|
+
match res {
|
|
908
|
+
Ok(()) => {
|
|
909
|
+
raw_client
|
|
910
|
+
.execute(
|
|
911
|
+
"INSERT INTO _tish_pg_migrations(name) VALUES ($1)",
|
|
912
|
+
&[&name],
|
|
913
|
+
)
|
|
914
|
+
.await?;
|
|
915
|
+
raw_client.batch_execute("COMMIT").await?;
|
|
916
|
+
applied_now.push(name.clone());
|
|
917
|
+
}
|
|
918
|
+
Err(e) => {
|
|
919
|
+
let _ = raw_client.batch_execute("ROLLBACK").await;
|
|
920
|
+
return Err(TishPgError::Postgres(e));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
Ok::<_, TishPgError>(applied_now)
|
|
925
|
+
}) {
|
|
926
|
+
Ok(v) => v.into_iter().map(|s| TishValue::String(s.into())).collect(),
|
|
927
|
+
Err(e) => return tish_err(format!("migrate: {}", crate::format_tish_pg_error(&e))),
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
let mut om = ObjectMap::with_capacity(2);
|
|
931
|
+
om.insert(Arc::from("ok"), TishValue::Bool(true));
|
|
932
|
+
om.insert(Arc::from("applied"), TishValue::Array(VmRef::new(applied)));
|
|
933
|
+
TishValue::object(om)
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Re-export the sync facade at crate root (pub fn(args: &[Value]) -> Value
|
|
938
|
+
// shape that `tishlang-cargo-bindgen` picks up automatically). The public
|
|
939
|
+
// names are snake_case because Tish's codegen snake-cases .tish imports on
|
|
940
|
+
// the Rust side (camelCase `queryAll` in .tish -> snake `query_all` here).
|
|
941
|
+
#[cfg(feature = "tish-bindings")]
|
|
942
|
+
pub use tish_sync::{
|
|
943
|
+
close, connect, migrate, per_worker_client, prepare, query_all, query_prepared,
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
#[cfg(test)]
|
|
947
|
+
mod tests {
|
|
948
|
+
use super::*;
|
|
949
|
+
|
|
950
|
+
#[tokio::test]
|
|
951
|
+
async fn parses_url() {
|
|
952
|
+
let cfg = parse_connection_string("postgres://u:p@h:5432/db").unwrap();
|
|
953
|
+
assert_eq!(cfg.get_user(), Some("u"));
|
|
954
|
+
}
|
|
955
|
+
}
|