@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.
Files changed (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. 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(&params)?;
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(&params)?;
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, &params)) {
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
+ }