tep 0.11.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/lib/tep/tep_pg.c
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
/* tep_pg.c - thin libpq wrapper for spinel-AOT'd tep apps.
|
|
2
|
+
*
|
|
3
|
+
* Mirrors tep_sqlite.c's pattern: spinel can't load CRuby C
|
|
4
|
+
* extensions (the `pg` gem ships native code against MRI's ABI),
|
|
5
|
+
* so we expose a stable str/int-typed FFI surface that the Ruby
|
|
6
|
+
* side wraps in PG::Connection / PG::Result / PG::Error.
|
|
7
|
+
*
|
|
8
|
+
* Surface scope is documented in docs/PG-BATTERY.md. This file
|
|
9
|
+
* implements Phase 0 + Phase 1: connect / status / exec /
|
|
10
|
+
* exec_params / result reading / escape. Phase 1.5 (prepared
|
|
11
|
+
* statements) and Phase 2 (async) layer on top.
|
|
12
|
+
*
|
|
13
|
+
* Integer-handle slot tables avoid putting PGconn* / PGresult* into
|
|
14
|
+
* spinel's poly value type. Strings come back via a rotating
|
|
15
|
+
* static-buffer pool so callers can hold a few results live without
|
|
16
|
+
* the lifetime-of-PQclear pointer alias biting them.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
#include <stdlib.h>
|
|
20
|
+
#include <string.h>
|
|
21
|
+
#include <stdio.h>
|
|
22
|
+
#include <libpq-fe.h>
|
|
23
|
+
|
|
24
|
+
#define TEP_PG_MAX_CONNS 8
|
|
25
|
+
#define TEP_PG_MAX_RESULTS 64
|
|
26
|
+
#define TEP_PG_MAX_PARAMS 32
|
|
27
|
+
#define TEP_PG_PARAM_BUFSIZE 262144 /* 256 KiB; AR migrations push big DDL params */
|
|
28
|
+
#define TEP_PG_STR_BUFSIZE 65536
|
|
29
|
+
#define TEP_PG_STR_BUF_SLOTS 128
|
|
30
|
+
|
|
31
|
+
static PGconn *tep_pg_conns[TEP_PG_MAX_CONNS] = {0};
|
|
32
|
+
static PGresult *tep_pg_results[TEP_PG_MAX_RESULTS] = {0};
|
|
33
|
+
static int tep_pg_result_conn[TEP_PG_MAX_RESULTS]; /* conn slot owning each result */
|
|
34
|
+
|
|
35
|
+
/* Per-process "no live conn" error stash. PQconnectdb returns
|
|
36
|
+
* non-NULL even on failure; we keep its error message here long
|
|
37
|
+
* enough for the Ruby side to read it, then PQfinish the conn.
|
|
38
|
+
* Slot 0 is intentionally reserved for "no conn" -- tep_pg_error_message(0)
|
|
39
|
+
* reads this stash, while tep_pg_error_message(h>=1) reads
|
|
40
|
+
* PQerrorMessage on the corresponding live conn. */
|
|
41
|
+
#define TEP_PG_LAST_CONNECT_ERR_SIZE 1024
|
|
42
|
+
static char tep_pg_last_connect_err[TEP_PG_LAST_CONNECT_ERR_SIZE] = {0};
|
|
43
|
+
|
|
44
|
+
/* Parameter accumulator for exec_params. */
|
|
45
|
+
static const char *tep_pg_param_ptrs[TEP_PG_MAX_PARAMS];
|
|
46
|
+
static int tep_pg_param_is_null[TEP_PG_MAX_PARAMS];
|
|
47
|
+
static char tep_pg_param_buf[TEP_PG_PARAM_BUFSIZE];
|
|
48
|
+
static int tep_pg_param_buf_used = 0;
|
|
49
|
+
static int tep_pg_param_count = 0;
|
|
50
|
+
|
|
51
|
+
/* Rotating return-string buffer. */
|
|
52
|
+
static char tep_pg_str_buf[TEP_PG_STR_BUF_SLOTS][TEP_PG_STR_BUFSIZE];
|
|
53
|
+
static int tep_pg_str_slot = 0;
|
|
54
|
+
|
|
55
|
+
static char *tep_pg_next_str_buf(void) {
|
|
56
|
+
char *buf = tep_pg_str_buf[tep_pg_str_slot];
|
|
57
|
+
tep_pg_str_slot = (tep_pg_str_slot + 1) % TEP_PG_STR_BUF_SLOTS;
|
|
58
|
+
return buf;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static char *tep_pg_return_str(const char *src) {
|
|
62
|
+
char *buf = tep_pg_next_str_buf();
|
|
63
|
+
if (src == NULL) { buf[0] = '\0'; return buf; }
|
|
64
|
+
size_t n = strlen(src);
|
|
65
|
+
if (n >= TEP_PG_STR_BUFSIZE) n = TEP_PG_STR_BUFSIZE - 1;
|
|
66
|
+
memcpy(buf, src, n);
|
|
67
|
+
buf[n] = '\0';
|
|
68
|
+
return buf;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Find a free conn slot (1-indexed return). 0 = none free. */
|
|
72
|
+
static int tep_pg_alloc_conn_slot(void) {
|
|
73
|
+
for (int i = 0; i < TEP_PG_MAX_CONNS; i++) {
|
|
74
|
+
if (tep_pg_conns[i] == NULL) return i + 1;
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Find a free result slot (1-indexed return). 0 = none free. */
|
|
80
|
+
static int tep_pg_alloc_result_slot(void) {
|
|
81
|
+
for (int i = 0; i < TEP_PG_MAX_RESULTS; i++) {
|
|
82
|
+
if (tep_pg_results[i] == NULL) return i + 1;
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static PGconn *tep_pg_conn_for(int h) {
|
|
88
|
+
if (h < 1 || h > TEP_PG_MAX_CONNS) return NULL;
|
|
89
|
+
return tep_pg_conns[h - 1];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static PGresult *tep_pg_result_for(int rh) {
|
|
93
|
+
if (rh < 1 || rh > TEP_PG_MAX_RESULTS) return NULL;
|
|
94
|
+
return tep_pg_results[rh - 1];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* --- Connection lifecycle --- */
|
|
98
|
+
|
|
99
|
+
int tep_pg_connect(const char *conninfo) {
|
|
100
|
+
int slot = tep_pg_alloc_conn_slot();
|
|
101
|
+
if (slot == 0) {
|
|
102
|
+
snprintf(tep_pg_last_connect_err, TEP_PG_LAST_CONNECT_ERR_SIZE,
|
|
103
|
+
"tep_pg_connect: no free connection slot (max %d)",
|
|
104
|
+
TEP_PG_MAX_CONNS);
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
PGconn *c = PQconnectdb(conninfo ? conninfo : "");
|
|
108
|
+
if (PQstatus(c) != CONNECTION_OK) {
|
|
109
|
+
const char *m = PQerrorMessage(c);
|
|
110
|
+
size_t n = m ? strlen(m) : 0;
|
|
111
|
+
if (n >= TEP_PG_LAST_CONNECT_ERR_SIZE) n = TEP_PG_LAST_CONNECT_ERR_SIZE - 1;
|
|
112
|
+
if (m) memcpy(tep_pg_last_connect_err, m, n);
|
|
113
|
+
tep_pg_last_connect_err[n] = '\0';
|
|
114
|
+
PQfinish(c);
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
/* Force UTF8 once; lets Ruby-side strings round-trip cleanly. */
|
|
118
|
+
PQsetClientEncoding(c, "UTF8");
|
|
119
|
+
tep_pg_conns[slot - 1] = c;
|
|
120
|
+
return slot;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Hash-form connect. `keys` and `vals` are \0-delimited string
|
|
124
|
+
* buffers of length `count` entries each; we walk them to build
|
|
125
|
+
* the parallel `const char **` arrays PQconnectdbParams expects. */
|
|
126
|
+
int tep_pg_connect_kv(const char *keys, const char *vals, int count) {
|
|
127
|
+
if (count < 0 || count > 32) {
|
|
128
|
+
snprintf(tep_pg_last_connect_err, TEP_PG_LAST_CONNECT_ERR_SIZE,
|
|
129
|
+
"tep_pg_connect_kv: bad count %d (max 32)", count);
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
int slot = tep_pg_alloc_conn_slot();
|
|
133
|
+
if (slot == 0) {
|
|
134
|
+
snprintf(tep_pg_last_connect_err, TEP_PG_LAST_CONNECT_ERR_SIZE,
|
|
135
|
+
"tep_pg_connect_kv: no free connection slot");
|
|
136
|
+
return -1;
|
|
137
|
+
}
|
|
138
|
+
/* Build parallel C arrays. PQconnectdbParams wants
|
|
139
|
+
* keywords[i] and values[i] as NUL-terminated, with a final
|
|
140
|
+
* NULL entry. count+1 to leave the terminator. */
|
|
141
|
+
const char *kw[33];
|
|
142
|
+
const char *vw[33];
|
|
143
|
+
const char *kp = keys;
|
|
144
|
+
const char *vp = vals;
|
|
145
|
+
for (int i = 0; i < count; i++) {
|
|
146
|
+
kw[i] = kp;
|
|
147
|
+
vw[i] = vp;
|
|
148
|
+
kp += strlen(kp) + 1;
|
|
149
|
+
vp += strlen(vp) + 1;
|
|
150
|
+
}
|
|
151
|
+
kw[count] = NULL;
|
|
152
|
+
vw[count] = NULL;
|
|
153
|
+
PGconn *c = PQconnectdbParams(kw, vw, 0);
|
|
154
|
+
if (PQstatus(c) != CONNECTION_OK) {
|
|
155
|
+
const char *m = PQerrorMessage(c);
|
|
156
|
+
size_t n = m ? strlen(m) : 0;
|
|
157
|
+
if (n >= TEP_PG_LAST_CONNECT_ERR_SIZE) n = TEP_PG_LAST_CONNECT_ERR_SIZE - 1;
|
|
158
|
+
if (m) memcpy(tep_pg_last_connect_err, m, n);
|
|
159
|
+
tep_pg_last_connect_err[n] = '\0';
|
|
160
|
+
PQfinish(c);
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
PQsetClientEncoding(c, "UTF8");
|
|
164
|
+
tep_pg_conns[slot - 1] = c;
|
|
165
|
+
return slot;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
int tep_pg_finish(int h) {
|
|
169
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
170
|
+
if (c == NULL) return -1;
|
|
171
|
+
/* Clear any results belonging to this conn so libpq's "you
|
|
172
|
+
* freed the conn but a result still references its OIDs" UB
|
|
173
|
+
* doesn't surface. */
|
|
174
|
+
for (int i = 0; i < TEP_PG_MAX_RESULTS; i++) {
|
|
175
|
+
if (tep_pg_results[i] != NULL && tep_pg_result_conn[i] == h) {
|
|
176
|
+
PQclear(tep_pg_results[i]);
|
|
177
|
+
tep_pg_results[i] = NULL;
|
|
178
|
+
tep_pg_result_conn[i] = 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
PQfinish(c);
|
|
182
|
+
tep_pg_conns[h - 1] = NULL;
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
int tep_pg_reset(int h) {
|
|
187
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
188
|
+
if (c == NULL) return -1;
|
|
189
|
+
PQreset(c);
|
|
190
|
+
return PQstatus(c) == CONNECTION_OK ? 0 : -1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
int tep_pg_status(int h) {
|
|
194
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
195
|
+
if (c == NULL) return 1; /* CONNECTION_BAD shape for "no slot" */
|
|
196
|
+
return PQstatus(c) == CONNECTION_OK ? 0 : 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
int tep_pg_transaction_status(int h) {
|
|
200
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
201
|
+
if (c == NULL) return 4; /* PQTRANS_UNKNOWN */
|
|
202
|
+
return (int)PQtransactionStatus(c);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const char *tep_pg_error_message(int h) {
|
|
206
|
+
if (h == 0) {
|
|
207
|
+
/* "no live conn" stash -- used for connect failures. */
|
|
208
|
+
return tep_pg_return_str(tep_pg_last_connect_err);
|
|
209
|
+
}
|
|
210
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
211
|
+
if (c == NULL) return tep_pg_return_str("");
|
|
212
|
+
return tep_pg_return_str(PQerrorMessage(c));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
int tep_pg_server_version(int h) {
|
|
216
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
217
|
+
if (c == NULL) return 0;
|
|
218
|
+
return PQserverVersion(c);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
int tep_pg_set_client_encoding(int h, const char *enc) {
|
|
222
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
223
|
+
if (c == NULL) return -1;
|
|
224
|
+
return PQsetClientEncoding(c, enc);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* --- Sync exec --- */
|
|
228
|
+
|
|
229
|
+
/* Stash the result in a slot, recording its owning conn. Returns
|
|
230
|
+
* 1-indexed result handle, or -1 on slot exhaustion (PQclears the
|
|
231
|
+
* orphan result first). */
|
|
232
|
+
static int tep_pg_stash_result(PGresult *r, int conn_handle) {
|
|
233
|
+
if (r == NULL) return -1;
|
|
234
|
+
int rs = tep_pg_alloc_result_slot();
|
|
235
|
+
if (rs == 0) {
|
|
236
|
+
PQclear(r);
|
|
237
|
+
return -1;
|
|
238
|
+
}
|
|
239
|
+
tep_pg_results[rs - 1] = r;
|
|
240
|
+
tep_pg_result_conn[rs - 1] = conn_handle;
|
|
241
|
+
return rs;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
int tep_pg_exec(int h, const char *sql) {
|
|
245
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
246
|
+
if (c == NULL) return -1;
|
|
247
|
+
return tep_pg_stash_result(PQexec(c, sql), h);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
int tep_pg_param_clear(void) {
|
|
251
|
+
tep_pg_param_count = 0;
|
|
252
|
+
tep_pg_param_buf_used = 0;
|
|
253
|
+
for (int i = 0; i < TEP_PG_MAX_PARAMS; i++) {
|
|
254
|
+
tep_pg_param_ptrs[i] = NULL;
|
|
255
|
+
tep_pg_param_is_null[i] = 0;
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
int tep_pg_param_push_str(const char *s) {
|
|
261
|
+
if (tep_pg_param_count >= TEP_PG_MAX_PARAMS) return -1;
|
|
262
|
+
if (s == NULL) s = "";
|
|
263
|
+
size_t n = strlen(s) + 1; /* include NUL terminator */
|
|
264
|
+
if ((size_t)tep_pg_param_buf_used + n > TEP_PG_PARAM_BUFSIZE) return -1;
|
|
265
|
+
char *dst = tep_pg_param_buf + tep_pg_param_buf_used;
|
|
266
|
+
memcpy(dst, s, n);
|
|
267
|
+
tep_pg_param_buf_used += (int)n;
|
|
268
|
+
tep_pg_param_ptrs[tep_pg_param_count] = dst;
|
|
269
|
+
tep_pg_param_is_null[tep_pg_param_count] = 0;
|
|
270
|
+
tep_pg_param_count++;
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
int tep_pg_param_push_null(void) {
|
|
275
|
+
if (tep_pg_param_count >= TEP_PG_MAX_PARAMS) return -1;
|
|
276
|
+
tep_pg_param_ptrs[tep_pg_param_count] = NULL;
|
|
277
|
+
tep_pg_param_is_null[tep_pg_param_count] = 1;
|
|
278
|
+
tep_pg_param_count++;
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
int tep_pg_exec_params(int h, const char *sql) {
|
|
283
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
284
|
+
if (c == NULL) return -1;
|
|
285
|
+
/* PQexecParams: null paramTypes -> let libpq infer from the
|
|
286
|
+
* query's $1::T cast or the destination column's type. null
|
|
287
|
+
* paramLengths -> use strlen on each text-format param. null
|
|
288
|
+
* paramFormats -> all text. resultFormat=0 -> text result. */
|
|
289
|
+
PGresult *r = PQexecParams(
|
|
290
|
+
c, sql,
|
|
291
|
+
tep_pg_param_count,
|
|
292
|
+
NULL,
|
|
293
|
+
tep_pg_param_ptrs,
|
|
294
|
+
NULL,
|
|
295
|
+
NULL,
|
|
296
|
+
0
|
|
297
|
+
);
|
|
298
|
+
return tep_pg_stash_result(r, h);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
int tep_pg_clear(int rh) {
|
|
302
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
303
|
+
if (r == NULL) return -1;
|
|
304
|
+
PQclear(r);
|
|
305
|
+
tep_pg_results[rh - 1] = NULL;
|
|
306
|
+
tep_pg_result_conn[rh - 1] = 0;
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* --- Result inspection --- */
|
|
311
|
+
|
|
312
|
+
int tep_pg_result_status(int rh) {
|
|
313
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
314
|
+
if (r == NULL) return 3; /* RES_ERROR */
|
|
315
|
+
switch (PQresultStatus(r)) {
|
|
316
|
+
case PGRES_TUPLES_OK: return 0; /* RES_TUPLES */
|
|
317
|
+
case PGRES_COMMAND_OK: return 1; /* RES_COMMAND */
|
|
318
|
+
case PGRES_EMPTY_QUERY: return 2; /* RES_EMPTY */
|
|
319
|
+
default: return 3; /* RES_ERROR */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const char *tep_pg_result_error_message(int rh) {
|
|
324
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
325
|
+
if (r == NULL) return tep_pg_return_str("");
|
|
326
|
+
return tep_pg_return_str(PQresultErrorMessage(r));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const char *tep_pg_result_error_field(int rh, int code) {
|
|
330
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
331
|
+
if (r == NULL) return tep_pg_return_str("");
|
|
332
|
+
return tep_pg_return_str(PQresultErrorField(r, code));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const char *tep_pg_cmd_status(int rh) {
|
|
336
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
337
|
+
if (r == NULL) return tep_pg_return_str("");
|
|
338
|
+
return tep_pg_return_str(PQcmdStatus(r));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
int tep_pg_cmd_tuples(int rh) {
|
|
342
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
343
|
+
if (r == NULL) return 0;
|
|
344
|
+
const char *s = PQcmdTuples(r);
|
|
345
|
+
if (s == NULL || s[0] == '\0') return 0;
|
|
346
|
+
return atoi(s);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
int tep_pg_ntuples(int rh) {
|
|
350
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
351
|
+
return r ? PQntuples(r) : 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
int tep_pg_nfields(int rh) {
|
|
355
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
356
|
+
return r ? PQnfields(r) : 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const char *tep_pg_fname(int rh, int col) {
|
|
360
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
361
|
+
if (r == NULL) return tep_pg_return_str("");
|
|
362
|
+
return tep_pg_return_str(PQfname(r, col));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
int tep_pg_fnumber(int rh, const char *name) {
|
|
366
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
367
|
+
if (r == NULL) return -1;
|
|
368
|
+
return PQfnumber(r, name);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
int tep_pg_ftype(int rh, int col) {
|
|
372
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
373
|
+
return r ? (int)PQftype(r, col) : 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
int tep_pg_fformat(int rh, int col) {
|
|
377
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
378
|
+
return r ? PQfformat(r, col) : 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
int tep_pg_fmod(int rh, int col) {
|
|
382
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
383
|
+
return r ? PQfmod(r, col) : -1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const char *tep_pg_getvalue(int rh, int row, int col) {
|
|
387
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
388
|
+
if (r == NULL) return tep_pg_return_str("");
|
|
389
|
+
return tep_pg_return_str(PQgetvalue(r, row, col));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
int tep_pg_getisnull(int rh, int row, int col) {
|
|
393
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
394
|
+
return r ? PQgetisnull(r, row, col) : 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
int tep_pg_getlength(int rh, int row, int col) {
|
|
398
|
+
PGresult *r = tep_pg_result_for(rh);
|
|
399
|
+
return r ? PQgetlength(r, row, col) : 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* --- Escape --- */
|
|
403
|
+
|
|
404
|
+
const char *tep_pg_escape_string(int h, const char *s) {
|
|
405
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
406
|
+
char *buf = tep_pg_next_str_buf();
|
|
407
|
+
if (s == NULL) { buf[0] = '\0'; return buf; }
|
|
408
|
+
size_t slen = strlen(s);
|
|
409
|
+
if (slen * 2 + 1 >= TEP_PG_STR_BUFSIZE) slen = (TEP_PG_STR_BUFSIZE - 1) / 2;
|
|
410
|
+
int err = 0;
|
|
411
|
+
if (c != NULL) {
|
|
412
|
+
PQescapeStringConn(c, buf, s, slen, &err);
|
|
413
|
+
} else {
|
|
414
|
+
/* No live conn: fall back to the deprecated standalone form.
|
|
415
|
+
* AR rarely hits this path (it always has a conn), but the
|
|
416
|
+
* class-method PG::Connection.escape_string(s) needs it. */
|
|
417
|
+
PQescapeString(buf, s, slen);
|
|
418
|
+
}
|
|
419
|
+
return err == 0 ? buf : tep_pg_return_str("");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const char *tep_pg_escape_literal(int h, const char *s) {
|
|
423
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
424
|
+
if (c == NULL || s == NULL) return tep_pg_return_str("");
|
|
425
|
+
char *q = PQescapeLiteral(c, s, strlen(s));
|
|
426
|
+
if (q == NULL) return tep_pg_return_str("");
|
|
427
|
+
const char *out = tep_pg_return_str(q);
|
|
428
|
+
PQfreemem(q);
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const char *tep_pg_escape_identifier(int h, const char *s) {
|
|
433
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
434
|
+
if (c == NULL || s == NULL) return tep_pg_return_str("");
|
|
435
|
+
char *q = PQescapeIdentifier(c, s, strlen(s));
|
|
436
|
+
if (q == NULL) return tep_pg_return_str("");
|
|
437
|
+
const char *out = tep_pg_return_str(q);
|
|
438
|
+
PQfreemem(q);
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* --- Async exec (libpq non-blocking surface) --- */
|
|
443
|
+
/*
|
|
444
|
+
* The async primitives mirror libpq's PQsend* / PQconsumeInput /
|
|
445
|
+
* PQisBusy / PQgetResult family. Tep::PG::Connection#async_exec
|
|
446
|
+
* on the Ruby side drives this loop:
|
|
447
|
+
*
|
|
448
|
+
* PQsetnonblocking(c, 1)
|
|
449
|
+
* PQsendQuery(c, sql) // queue the request
|
|
450
|
+
* loop { PQflush(c); io_wait(WRITE) until done } // drain send buf
|
|
451
|
+
* loop { // wait for response
|
|
452
|
+
* PQconsumeInput(c)
|
|
453
|
+
* break unless PQisBusy(c)
|
|
454
|
+
* io_wait(fd, READ)
|
|
455
|
+
* }
|
|
456
|
+
* r = PQgetResult(c) // first result is the data
|
|
457
|
+
* while PQgetResult(c) != NULL { } // drain any trailing results
|
|
458
|
+
*
|
|
459
|
+
* The fd to park on comes from PQsocket(c); io_wait yields the
|
|
460
|
+
* fiber under Tep::Server::Scheduled, blocks-for-fd-ready under
|
|
461
|
+
* the prefork server (both end up correct, just different
|
|
462
|
+
* concurrency profile).
|
|
463
|
+
*
|
|
464
|
+
* Result handling reuses the existing slot table -- get_result
|
|
465
|
+
* stashes the returned PGresult into a slot and returns the
|
|
466
|
+
* 1-indexed handle, same as sync exec. -1 means "no result"
|
|
467
|
+
* (NULL from PQgetResult, the terminator that says "done"). 0
|
|
468
|
+
* is reserved for "slot table full" type errors.
|
|
469
|
+
*/
|
|
470
|
+
|
|
471
|
+
/* Async connect.
|
|
472
|
+
*
|
|
473
|
+
* tep_pg_connect_start mirrors PQconnectStart: returns a conn slot
|
|
474
|
+
* whose connection is mid-handshake (CONNECTION_STARTED). The
|
|
475
|
+
* caller drives the state machine with tep_pg_connect_poll, parking
|
|
476
|
+
* the fiber on Tep::Scheduler.io_wait between calls. After
|
|
477
|
+
* tep_pg_connect_poll returns 0 (PGRES_POLLING_OK), the caller
|
|
478
|
+
* should PQsetClientEncoding(c, "UTF8") -- same step the sync path
|
|
479
|
+
* does. On PGRES_POLLING_FAILED (3) the caller PQfinishes the
|
|
480
|
+
* conn.
|
|
481
|
+
*
|
|
482
|
+
* The poll loop accepts these return values:
|
|
483
|
+
*
|
|
484
|
+
* 0 = PGRES_POLLING_OK connected; stop polling
|
|
485
|
+
* 1 = PGRES_POLLING_READING park on fd READ
|
|
486
|
+
* 2 = PGRES_POLLING_WRITING park on fd WRITE
|
|
487
|
+
* 3 = PGRES_POLLING_FAILED connect failed; PQfinish
|
|
488
|
+
*
|
|
489
|
+
* The libpq enum has these specific values so the int casts are
|
|
490
|
+
* stable.
|
|
491
|
+
*/
|
|
492
|
+
|
|
493
|
+
int tep_pg_connect_start(const char *conninfo) {
|
|
494
|
+
int slot = tep_pg_alloc_conn_slot();
|
|
495
|
+
if (slot == 0) {
|
|
496
|
+
snprintf(tep_pg_last_connect_err, TEP_PG_LAST_CONNECT_ERR_SIZE,
|
|
497
|
+
"tep_pg_connect_start: no free connection slot (max %d)",
|
|
498
|
+
TEP_PG_MAX_CONNS);
|
|
499
|
+
return -1;
|
|
500
|
+
}
|
|
501
|
+
PGconn *c = PQconnectStart(conninfo ? conninfo : "");
|
|
502
|
+
if (c == NULL) {
|
|
503
|
+
snprintf(tep_pg_last_connect_err, TEP_PG_LAST_CONNECT_ERR_SIZE,
|
|
504
|
+
"tep_pg_connect_start: PQconnectStart returned NULL (OOM)");
|
|
505
|
+
return -1;
|
|
506
|
+
}
|
|
507
|
+
/* PQconnectStart can return non-NULL but CONNECTION_BAD on an
|
|
508
|
+
* unparseable conninfo string. Surface the error message and
|
|
509
|
+
* return -1 in that case so the Ruby side knows not to bother
|
|
510
|
+
* polling. */
|
|
511
|
+
if (PQstatus(c) == CONNECTION_BAD) {
|
|
512
|
+
const char *m = PQerrorMessage(c);
|
|
513
|
+
size_t n = m ? strlen(m) : 0;
|
|
514
|
+
if (n >= TEP_PG_LAST_CONNECT_ERR_SIZE) n = TEP_PG_LAST_CONNECT_ERR_SIZE - 1;
|
|
515
|
+
if (m) memcpy(tep_pg_last_connect_err, m, n);
|
|
516
|
+
tep_pg_last_connect_err[n] = '\0';
|
|
517
|
+
PQfinish(c);
|
|
518
|
+
return -1;
|
|
519
|
+
}
|
|
520
|
+
tep_pg_conns[slot - 1] = c;
|
|
521
|
+
return slot;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
int tep_pg_connect_poll(int h) {
|
|
525
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
526
|
+
if (c == NULL) return 0; /* PGRES_POLLING_FAILED for missing slot */
|
|
527
|
+
int state = (int)PQconnectPoll(c);
|
|
528
|
+
if (state == 0) {
|
|
529
|
+
/* PGRES_POLLING_FAILED -- stash error before caller PQfinishes. */
|
|
530
|
+
const char *m = PQerrorMessage(c);
|
|
531
|
+
size_t n = m ? strlen(m) : 0;
|
|
532
|
+
if (n >= TEP_PG_LAST_CONNECT_ERR_SIZE) n = TEP_PG_LAST_CONNECT_ERR_SIZE - 1;
|
|
533
|
+
if (m) memcpy(tep_pg_last_connect_err, m, n);
|
|
534
|
+
tep_pg_last_connect_err[n] = '\0';
|
|
535
|
+
}
|
|
536
|
+
return state;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
int tep_pg_socket(int h) {
|
|
540
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
541
|
+
if (c == NULL) return -1;
|
|
542
|
+
return PQsocket(c);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
int tep_pg_set_nonblocking(int h, int arg) {
|
|
546
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
547
|
+
if (c == NULL) return -1;
|
|
548
|
+
return PQsetnonblocking(c, arg);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
int tep_pg_send_query(int h, const char *sql) {
|
|
552
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
553
|
+
if (c == NULL) return 0;
|
|
554
|
+
return PQsendQuery(c, sql); /* 1 = ok, 0 = error */
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
int tep_pg_send_query_params(int h, const char *sql) {
|
|
558
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
559
|
+
if (c == NULL) return 0;
|
|
560
|
+
return PQsendQueryParams(
|
|
561
|
+
c, sql,
|
|
562
|
+
tep_pg_param_count,
|
|
563
|
+
NULL,
|
|
564
|
+
tep_pg_param_ptrs,
|
|
565
|
+
NULL,
|
|
566
|
+
NULL,
|
|
567
|
+
0
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* PQflush: 0 = done, 1 = more, -1 = error. */
|
|
572
|
+
int tep_pg_flush(int h) {
|
|
573
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
574
|
+
if (c == NULL) return -1;
|
|
575
|
+
return PQflush(c);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* PQconsumeInput: 1 = ok, 0 = error. Non-blocking by contract. */
|
|
579
|
+
int tep_pg_consume_input(int h) {
|
|
580
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
581
|
+
if (c == NULL) return 0;
|
|
582
|
+
return PQconsumeInput(c);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/* PQisBusy: 1 = need more input, 0 = ready for PQgetResult. */
|
|
586
|
+
int tep_pg_is_busy(int h) {
|
|
587
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
588
|
+
if (c == NULL) return 0;
|
|
589
|
+
return PQisBusy(c);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/* PQgetResult. Returns 1-indexed result slot, or -1 if NULL
|
|
593
|
+
* (libpq's "no more results" terminator). The caller's
|
|
594
|
+
* async_exec loop calls this once for the data result and again
|
|
595
|
+
* to read the NULL terminator -- doing so leaves the conn in a
|
|
596
|
+
* state where the next async_exec can start cleanly. */
|
|
597
|
+
int tep_pg_get_result(int h) {
|
|
598
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
599
|
+
if (c == NULL) return -1;
|
|
600
|
+
PGresult *r = PQgetResult(c);
|
|
601
|
+
if (r == NULL) return -1;
|
|
602
|
+
return tep_pg_stash_result(r, h);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/* --- LISTEN / NOTIFY ---
|
|
606
|
+
*
|
|
607
|
+
* Connection-level async-notification surface. Used by
|
|
608
|
+
* Tep::Broadcast's PG backend (Battery 2 chunk 2.2) to cross-worker
|
|
609
|
+
* pub/sub: worker A's publish runs NOTIFY on a shared channel;
|
|
610
|
+
* worker B's poll_notification picks up the delivery and dispatches
|
|
611
|
+
* to local subscribers. The C side just exposes PQexec-shaped
|
|
612
|
+
* LISTEN / NOTIFY and a poll loop around PQconsumeInput + PQnotifies.
|
|
613
|
+
*
|
|
614
|
+
* Channel names are SQL identifiers, NOT escaped here -- the
|
|
615
|
+
* caller is responsible for passing a safe identifier (typically
|
|
616
|
+
* a hard-coded constant like "tep_broadcast"). Payloads ARE
|
|
617
|
+
* escaped via PQescapeLiteral so arbitrary bytes (with quotes,
|
|
618
|
+
* backslashes, NULs up to PG's payload-size limit) round-trip
|
|
619
|
+
* cleanly. */
|
|
620
|
+
|
|
621
|
+
int tep_pg_listen(int h, const char *channel) {
|
|
622
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
623
|
+
if (c == NULL) return -1;
|
|
624
|
+
/* LISTEN <identifier> -- channel must be a safe SQL identifier. */
|
|
625
|
+
char buf[256];
|
|
626
|
+
snprintf(buf, sizeof(buf), "LISTEN %s", channel);
|
|
627
|
+
PGresult *r = PQexec(c, buf);
|
|
628
|
+
int ok = (PQresultStatus(r) == PGRES_COMMAND_OK) ? 0 : -1;
|
|
629
|
+
PQclear(r);
|
|
630
|
+
return ok;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
int tep_pg_unlisten(int h, const char *channel) {
|
|
634
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
635
|
+
if (c == NULL) return -1;
|
|
636
|
+
char buf[256];
|
|
637
|
+
snprintf(buf, sizeof(buf), "UNLISTEN %s", channel);
|
|
638
|
+
PGresult *r = PQexec(c, buf);
|
|
639
|
+
int ok = (PQresultStatus(r) == PGRES_COMMAND_OK) ? 0 : -1;
|
|
640
|
+
PQclear(r);
|
|
641
|
+
return ok;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
int tep_pg_notify(int h, const char *channel, const char *payload) {
|
|
645
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
646
|
+
if (c == NULL) return -1;
|
|
647
|
+
/* PQescapeLiteral wraps payload in single quotes + escapes
|
|
648
|
+
* embedded quotes/backslashes. Returns a malloc'd string the
|
|
649
|
+
* caller must PQfreemem. */
|
|
650
|
+
char *esc = PQescapeLiteral(c, payload, strlen(payload));
|
|
651
|
+
if (esc == NULL) return -1;
|
|
652
|
+
/* "NOTIFY <channel>, <escaped_payload>" -- max payload size is
|
|
653
|
+
* 8000 bytes per PG (configurable, but the default cap is
|
|
654
|
+
* load-bearing). Larger payloads get rejected at PQexec time. */
|
|
655
|
+
size_t need = strlen(channel) + strlen(esc) + 32;
|
|
656
|
+
char *buf = (char *)malloc(need);
|
|
657
|
+
if (buf == NULL) { PQfreemem(esc); return -1; }
|
|
658
|
+
snprintf(buf, need, "NOTIFY %s, %s", channel, esc);
|
|
659
|
+
PGresult *r = PQexec(c, buf);
|
|
660
|
+
int ok = (PQresultStatus(r) == PGRES_COMMAND_OK) ? 0 : -1;
|
|
661
|
+
PQclear(r);
|
|
662
|
+
PQfreemem(esc);
|
|
663
|
+
free(buf);
|
|
664
|
+
return ok;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Stash for the most recently consumed notification. */
|
|
668
|
+
static char tep_pg_notify_channel_buf[256];
|
|
669
|
+
static char tep_pg_notify_payload_buf[16384];
|
|
670
|
+
|
|
671
|
+
/* Block up to `timeout_ms` waiting for a notification on `h`.
|
|
672
|
+
* Returns 1 if one was received (channel + payload available via
|
|
673
|
+
* tep_pg_notify_channel / tep_pg_notify_payload), 0 on timeout, -1
|
|
674
|
+
* on connection error.
|
|
675
|
+
*
|
|
676
|
+
* Uses select() on PQsocket(conn) to wait, then PQconsumeInput +
|
|
677
|
+
* PQnotifies to read pending notifications. The connection MUST
|
|
678
|
+
* already be in LISTEN mode (via tep_pg_listen) for the channel
|
|
679
|
+
* the caller cares about.
|
|
680
|
+
*
|
|
681
|
+
* Single-notification per call by design -- the caller drives the
|
|
682
|
+
* loop, calling repeatedly to drain any accumulated notifications.
|
|
683
|
+
* Returns 1 + sets a fresh stash on every notification received. */
|
|
684
|
+
#include <sys/select.h>
|
|
685
|
+
#include <sys/time.h>
|
|
686
|
+
|
|
687
|
+
int tep_pg_poll_notification(int h, int timeout_ms) {
|
|
688
|
+
PGconn *c = tep_pg_conn_for(h);
|
|
689
|
+
if (c == NULL) return -1;
|
|
690
|
+
|
|
691
|
+
/* Fast path: check for already-pending notification before
|
|
692
|
+
* doing any I/O. PQconsumeInput drains the kernel buffer if
|
|
693
|
+
* anything is sitting there. */
|
|
694
|
+
if (PQconsumeInput(c) == 0) return -1;
|
|
695
|
+
PGnotify *n = PQnotifies(c);
|
|
696
|
+
|
|
697
|
+
/* If nothing pending, wait on the socket for up to timeout_ms. */
|
|
698
|
+
if (n == NULL) {
|
|
699
|
+
int fd = PQsocket(c);
|
|
700
|
+
if (fd < 0) return -1;
|
|
701
|
+
fd_set rs;
|
|
702
|
+
FD_ZERO(&rs);
|
|
703
|
+
FD_SET(fd, &rs);
|
|
704
|
+
struct timeval tv;
|
|
705
|
+
tv.tv_sec = timeout_ms / 1000;
|
|
706
|
+
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
|
707
|
+
int sel = select(fd + 1, &rs, NULL, NULL, &tv);
|
|
708
|
+
if (sel < 0) return -1;
|
|
709
|
+
if (sel == 0) return 0;
|
|
710
|
+
if (PQconsumeInput(c) == 0) return -1;
|
|
711
|
+
n = PQnotifies(c);
|
|
712
|
+
if (n == NULL) return 0;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/* Copy out into static buffers + free the libpq struct. */
|
|
716
|
+
if (n->relname) {
|
|
717
|
+
size_t nlen = strlen(n->relname);
|
|
718
|
+
if (nlen >= sizeof(tep_pg_notify_channel_buf)) {
|
|
719
|
+
nlen = sizeof(tep_pg_notify_channel_buf) - 1;
|
|
720
|
+
}
|
|
721
|
+
memcpy(tep_pg_notify_channel_buf, n->relname, nlen);
|
|
722
|
+
tep_pg_notify_channel_buf[nlen] = '\0';
|
|
723
|
+
} else {
|
|
724
|
+
tep_pg_notify_channel_buf[0] = '\0';
|
|
725
|
+
}
|
|
726
|
+
if (n->extra) {
|
|
727
|
+
size_t plen = strlen(n->extra);
|
|
728
|
+
if (plen >= sizeof(tep_pg_notify_payload_buf)) {
|
|
729
|
+
plen = sizeof(tep_pg_notify_payload_buf) - 1;
|
|
730
|
+
}
|
|
731
|
+
memcpy(tep_pg_notify_payload_buf, n->extra, plen);
|
|
732
|
+
tep_pg_notify_payload_buf[plen] = '\0';
|
|
733
|
+
} else {
|
|
734
|
+
tep_pg_notify_payload_buf[0] = '\0';
|
|
735
|
+
}
|
|
736
|
+
PQfreemem(n);
|
|
737
|
+
return 1;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const char *tep_pg_notify_channel(void) {
|
|
741
|
+
return tep_pg_notify_channel_buf;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const char *tep_pg_notify_payload(void) {
|
|
745
|
+
return tep_pg_notify_payload_buf;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* --- Version --- */
|
|
749
|
+
|
|
750
|
+
const char *tep_pg_libpq_version(void) {
|
|
751
|
+
int v = PQlibVersion();
|
|
752
|
+
/* PQlibVersion returns NNxxYYzz where NN=major, xx=minor, YY=patch
|
|
753
|
+
* for pre-10; for 10+ it's MMMMxxYY (major thousands). Render
|
|
754
|
+
* generically as "major.minor.patch" (PG-10+: minor/patch from
|
|
755
|
+
* the lower digits). */
|
|
756
|
+
int major, minor, patch;
|
|
757
|
+
if (v >= 100000) {
|
|
758
|
+
major = v / 10000;
|
|
759
|
+
minor = (v / 100) % 100;
|
|
760
|
+
patch = v % 100;
|
|
761
|
+
} else {
|
|
762
|
+
major = v / 10000;
|
|
763
|
+
minor = (v / 100) % 100;
|
|
764
|
+
patch = v % 100;
|
|
765
|
+
}
|
|
766
|
+
char *buf = tep_pg_next_str_buf();
|
|
767
|
+
snprintf(buf, TEP_PG_STR_BUFSIZE, "%d.%d.%d", major, minor, patch);
|
|
768
|
+
return buf;
|
|
769
|
+
}
|