fresco 0.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.
- checksums.yaml +7 -0
- data/exe/fresco +3 -0
- data/lib/fresco/application.rb +12 -0
- data/lib/fresco/cli/build.rb +682 -0
- data/lib/fresco/cli/dev.rb +17 -0
- data/lib/fresco/cli/dev_loop.rb +815 -0
- data/lib/fresco/cli/new.rb +120 -0
- data/lib/fresco/cli/release.rb +76 -0
- data/lib/fresco/cli.rb +56 -0
- data/lib/fresco/database_config.rb +34 -0
- data/lib/fresco/generators/app/Gemfile.tt +18 -0
- data/lib/fresco/generators/app/README.md.tt +32 -0
- data/lib/fresco/generators/app/app/action.rb.tt +20 -0
- data/lib/fresco/generators/app/app/actions/root_path.rb.tt +5 -0
- data/lib/fresco/generators/app/app/views/layouts/application.html.erb +29 -0
- data/lib/fresco/generators/app/app/views/root_path.html.erb +8 -0
- data/lib/fresco/generators/app/app.rb.tt +15 -0
- data/lib/fresco/generators/app/bin/build +2 -0
- data/lib/fresco/generators/app/bin/dev +2 -0
- data/lib/fresco/generators/app/bin/release +2 -0
- data/lib/fresco/generators/app/config/app.rb.tt +26 -0
- data/lib/fresco/generators/app/config/database.rb +17 -0
- data/lib/fresco/generators/app/config/routes.rb +11 -0
- data/lib/fresco/generators/app/db/schema.rb +14 -0
- data/lib/fresco/generators/app/public/404.html +87 -0
- data/lib/fresco/generators/app/public/500.html +84 -0
- data/lib/fresco/migration_builder.rb +55 -0
- data/lib/fresco/model_builder.rb +54 -0
- data/lib/fresco/paths.rb +20 -0
- data/lib/fresco/router.rb +67 -0
- data/lib/fresco/runtime/boot.rb +34 -0
- data/lib/fresco/runtime/db_postgres.rb +403 -0
- data/lib/fresco/runtime/db_sqlite.rb +495 -0
- data/lib/fresco/runtime/http.c +456 -0
- data/lib/fresco/runtime/postgres.c +339 -0
- data/lib/fresco/runtime/runtime.rb +1810 -0
- data/lib/fresco/runtime/sqlite.c +220 -0
- data/lib/fresco/runtime/welcome.rb +152 -0
- data/lib/fresco/schema_builder.rb +71 -0
- data/lib/fresco/templates/dispatch.rb.erb +32 -0
- data/lib/fresco/templates/layout_dispatch.rb.erb +16 -0
- data/lib/fresco/templates/manifest.rb.erb +5 -0
- data/lib/fresco/templates/migrations.rb.erb +152 -0
- data/lib/fresco/templates/model.rb.erb +223 -0
- data/lib/fresco/templates/view.rb.erb +5 -0
- data/lib/fresco/version.rb +3 -0
- data/lib/fresco.rb +61 -0
- metadata +115 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Fresco Postgres shim. Wraps libpq behind the same str/int FFI
|
|
3
|
+
* surface as lib/sqlite.c so the Ruby wrapper has a single shape
|
|
4
|
+
* regardless of which adapter is linked. The Spinel codegen (M3+)
|
|
5
|
+
* only has to know the wrapper's method names, not the underlying
|
|
6
|
+
* client library.
|
|
7
|
+
*
|
|
8
|
+
* Shape divergences from sqlite.c — same Ruby-side method names but
|
|
9
|
+
* the C semantics differ because libpq isn't a cursor library:
|
|
10
|
+
*
|
|
11
|
+
* - libpq fetches a whole PGresult per query. We model "cursor" as
|
|
12
|
+
* a slot holding {sql, accumulated bind params, PGresult, current
|
|
13
|
+
* row index}. `fresco_pg_prepare` only stores the SQL + sets the
|
|
14
|
+
* slot up; nothing actually hits Postgres until the first `step`.
|
|
15
|
+
* `bind_str`/`bind_int` accumulate values into the slot's param
|
|
16
|
+
* array. The first `step` executes PQexecParams with the
|
|
17
|
+
* accumulated values and sets row = 0; subsequent steps advance
|
|
18
|
+
* row until ntuples is reached.
|
|
19
|
+
*
|
|
20
|
+
* - `fresco_pg_last_insert_rowid` runs `SELECT lastval()`. Only
|
|
21
|
+
* valid when the most recent statement triggered a sequence
|
|
22
|
+
* (SERIAL / IDENTITY column). For tables without a sequence
|
|
23
|
+
* callers should use `INSERT ... RETURNING id` + `col_int(0)`.
|
|
24
|
+
*
|
|
25
|
+
* - Connection strings, not file paths. `fresco_pg_open(url)` is
|
|
26
|
+
* a thin wrapper over PQconnectdb. URL goes in, 1-indexed handle
|
|
27
|
+
* comes out.
|
|
28
|
+
*
|
|
29
|
+
* Errors
|
|
30
|
+
* ------
|
|
31
|
+
* Most functions return 0 on success and -1 on error. Detailed
|
|
32
|
+
* errmsg surfacing isn't wired through (same reasoning as sqlite.c:
|
|
33
|
+
* Spinel's :str return lifetime would have to be threaded through
|
|
34
|
+
* another rotating buffer). Callers branch on `< 0`.
|
|
35
|
+
*
|
|
36
|
+
* Build dependency
|
|
37
|
+
* ----------------
|
|
38
|
+
* Requires libpq's headers (`libpq-fe.h`) and library (`-lpq`) at
|
|
39
|
+
* compile/link time. On macOS via Homebrew:
|
|
40
|
+
*
|
|
41
|
+
* export CPATH=/opt/homebrew/opt/libpq/include
|
|
42
|
+
* export LIBRARY_PATH=/opt/homebrew/opt/libpq/lib
|
|
43
|
+
* ./bin/release
|
|
44
|
+
*
|
|
45
|
+
* On Linux: `apt-get install libpq-dev` puts headers and lib in
|
|
46
|
+
* cc's default search paths.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
#include <stdlib.h>
|
|
50
|
+
#include <string.h>
|
|
51
|
+
#include <libpq-fe.h>
|
|
52
|
+
|
|
53
|
+
#define FRESCO_PG_MAX_HANDLES 16
|
|
54
|
+
#define FRESCO_PG_MAX_STMTS 64
|
|
55
|
+
#define FRESCO_PG_MAX_PARAMS 32
|
|
56
|
+
#define FRESCO_PG_COL_BUFSIZE 65536
|
|
57
|
+
#define FRESCO_PG_COL_BUF_SLOTS 16
|
|
58
|
+
|
|
59
|
+
typedef struct {
|
|
60
|
+
PGconn *conn; /* which connection this cursor belongs to */
|
|
61
|
+
char *sql; /* malloc'd, owned by the slot */
|
|
62
|
+
char *params[FRESCO_PG_MAX_PARAMS]; /* text-format binds, malloc'd */
|
|
63
|
+
int n_params; /* highest 1-indexed param bound so far */
|
|
64
|
+
PGresult *result; /* NULL until first step fires PQexecParams */
|
|
65
|
+
int row; /* current row index; -1 before first step */
|
|
66
|
+
int ntuples; /* PQntuples(result) once executed */
|
|
67
|
+
int nfields; /* PQnfields(result) once executed */
|
|
68
|
+
} fresco_pg_cursor;
|
|
69
|
+
|
|
70
|
+
static PGconn *fresco_pg_handles[FRESCO_PG_MAX_HANDLES] = {0};
|
|
71
|
+
static fresco_pg_cursor *fresco_pg_stmts[FRESCO_PG_MAX_STMTS] = {0};
|
|
72
|
+
|
|
73
|
+
/* Rotating return buffers for col_str. Same rationale as sqlite.c:
|
|
74
|
+
* callers stash several col_str results into different variables /
|
|
75
|
+
* containers before the buffer would otherwise rotate, and a single
|
|
76
|
+
* static buf would alias them all to the last call. */
|
|
77
|
+
static char fresco_pg_col_buf[FRESCO_PG_COL_BUF_SLOTS][FRESCO_PG_COL_BUFSIZE];
|
|
78
|
+
static int fresco_pg_col_slot = 0;
|
|
79
|
+
|
|
80
|
+
static void fresco_pg_cursor_free_params(fresco_pg_cursor *c) {
|
|
81
|
+
int i;
|
|
82
|
+
for (i = 0; i < FRESCO_PG_MAX_PARAMS; i++) {
|
|
83
|
+
if (c->params[i]) {
|
|
84
|
+
free(c->params[i]);
|
|
85
|
+
c->params[i] = NULL;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
c->n_params = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static void fresco_pg_cursor_free(fresco_pg_cursor *c) {
|
|
92
|
+
if (!c) return;
|
|
93
|
+
if (c->result) {
|
|
94
|
+
PQclear(c->result);
|
|
95
|
+
c->result = NULL;
|
|
96
|
+
}
|
|
97
|
+
fresco_pg_cursor_free_params(c);
|
|
98
|
+
if (c->sql) {
|
|
99
|
+
free(c->sql);
|
|
100
|
+
c->sql = NULL;
|
|
101
|
+
}
|
|
102
|
+
free(c);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Open a connection. Returns a 1-indexed handle on success (0 reserved
|
|
106
|
+
* for "uninitialised"); -1 if the slot table is full or PQconnectdb
|
|
107
|
+
* fails. URL accepts any libpq-conninfo string: postgres://user@host/db
|
|
108
|
+
* or `host=... user=... dbname=...`. Closing frees the slot for re-use.
|
|
109
|
+
*/
|
|
110
|
+
int fresco_pg_open(const char *url) {
|
|
111
|
+
int i;
|
|
112
|
+
for (i = 0; i < FRESCO_PG_MAX_HANDLES; i++) {
|
|
113
|
+
if (fresco_pg_handles[i] == NULL) {
|
|
114
|
+
PGconn *conn = PQconnectdb(url);
|
|
115
|
+
if (PQstatus(conn) != CONNECTION_OK) {
|
|
116
|
+
PQfinish(conn);
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
fresco_pg_handles[i] = conn;
|
|
120
|
+
return i + 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
int fresco_pg_close(int h) {
|
|
127
|
+
int i;
|
|
128
|
+
if (h < 1 || h > FRESCO_PG_MAX_HANDLES) return -1;
|
|
129
|
+
PGconn *conn = fresco_pg_handles[h - 1];
|
|
130
|
+
if (conn == NULL) return -1;
|
|
131
|
+
/* Free any cursors still attached so dangling slots don't outlive
|
|
132
|
+
* the connection. */
|
|
133
|
+
for (i = 0; i < FRESCO_PG_MAX_STMTS; i++) {
|
|
134
|
+
if (fresco_pg_stmts[i] != NULL && fresco_pg_stmts[i]->conn == conn) {
|
|
135
|
+
fresco_pg_cursor_free(fresco_pg_stmts[i]);
|
|
136
|
+
fresco_pg_stmts[i] = NULL;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
PQfinish(conn);
|
|
140
|
+
fresco_pg_handles[h - 1] = NULL;
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* No-bind statement runner. Equivalent to sqlite_exec — fires the SQL,
|
|
145
|
+
* discards the result, returns 0 on success and -1 on error. Use for
|
|
146
|
+
* DDL, BEGIN/COMMIT, and queries whose values are constants. For
|
|
147
|
+
* anything user-supplied always use prepare + bind + step + finalize. */
|
|
148
|
+
int fresco_pg_exec(int h, const char *sql) {
|
|
149
|
+
if (h < 1 || h > FRESCO_PG_MAX_HANDLES) return -1;
|
|
150
|
+
PGconn *conn = fresco_pg_handles[h - 1];
|
|
151
|
+
if (conn == NULL) return -1;
|
|
152
|
+
PGresult *res = PQexec(conn, sql);
|
|
153
|
+
ExecStatusType s = PQresultStatus(res);
|
|
154
|
+
int ok = (s == PGRES_COMMAND_OK || s == PGRES_TUPLES_OK);
|
|
155
|
+
PQclear(res);
|
|
156
|
+
return ok ? 0 : -1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Set up a cursor over a parameterised statement. Returns a 1-indexed
|
|
160
|
+
* cursor id (>= 1) on success, -1 on failure. The actual execution is
|
|
161
|
+
* deferred to the first `step` call so accumulated bind_str/bind_int
|
|
162
|
+
* values land in the PQexecParams call. Placeholder syntax is $1, $2,
|
|
163
|
+
* ... (libpq native); SQLite's `?` won't work. */
|
|
164
|
+
int fresco_pg_prepare(int h, const char *sql) {
|
|
165
|
+
int i;
|
|
166
|
+
if (h < 1 || h > FRESCO_PG_MAX_HANDLES) return -1;
|
|
167
|
+
PGconn *conn = fresco_pg_handles[h - 1];
|
|
168
|
+
if (conn == NULL) return -1;
|
|
169
|
+
for (i = 0; i < FRESCO_PG_MAX_STMTS; i++) {
|
|
170
|
+
if (fresco_pg_stmts[i] == NULL) {
|
|
171
|
+
fresco_pg_cursor *c = calloc(1, sizeof(fresco_pg_cursor));
|
|
172
|
+
if (!c) return -1;
|
|
173
|
+
c->conn = conn;
|
|
174
|
+
c->sql = strdup(sql);
|
|
175
|
+
if (!c->sql) {
|
|
176
|
+
free(c);
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
c->row = -1;
|
|
180
|
+
c->ntuples = 0;
|
|
181
|
+
c->nfields = 0;
|
|
182
|
+
fresco_pg_stmts[i] = c;
|
|
183
|
+
return i + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return -1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Stash a text param at 1-indexed position. Overwrites any prior bind
|
|
190
|
+
* at the same index. Strings are duplicated — caller's pointer doesn't
|
|
191
|
+
* need to outlive the bind. */
|
|
192
|
+
int fresco_pg_bind_str(int cid, int idx, const char *value) {
|
|
193
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return -1;
|
|
194
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
195
|
+
if (c == NULL) return -1;
|
|
196
|
+
if (idx < 1 || idx > FRESCO_PG_MAX_PARAMS) return -1;
|
|
197
|
+
if (c->params[idx - 1]) {
|
|
198
|
+
free(c->params[idx - 1]);
|
|
199
|
+
c->params[idx - 1] = NULL;
|
|
200
|
+
}
|
|
201
|
+
char *copy = strdup(value);
|
|
202
|
+
if (!copy) return -1;
|
|
203
|
+
c->params[idx - 1] = copy;
|
|
204
|
+
if (idx > c->n_params) c->n_params = idx;
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Int binds are stringified into the param slot — libpq's text-format
|
|
209
|
+
* coercion casts them back to the column type. Fixed-size decimal buf
|
|
210
|
+
* (32 bytes) covers every int range comfortably. */
|
|
211
|
+
int fresco_pg_bind_int(int cid, int idx, int value) {
|
|
212
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return -1;
|
|
213
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
214
|
+
if (c == NULL) return -1;
|
|
215
|
+
if (idx < 1 || idx > FRESCO_PG_MAX_PARAMS) return -1;
|
|
216
|
+
char buf[32];
|
|
217
|
+
snprintf(buf, sizeof(buf), "%d", value);
|
|
218
|
+
if (c->params[idx - 1]) {
|
|
219
|
+
free(c->params[idx - 1]);
|
|
220
|
+
c->params[idx - 1] = NULL;
|
|
221
|
+
}
|
|
222
|
+
char *copy = strdup(buf);
|
|
223
|
+
if (!copy) return -1;
|
|
224
|
+
c->params[idx - 1] = copy;
|
|
225
|
+
if (idx > c->n_params) c->n_params = idx;
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* 1 -> row available, 0 -> done (no more rows), -1 -> error or invalid
|
|
230
|
+
* cursor. First call fires PQexecParams; subsequent calls advance the
|
|
231
|
+
* row index. Same iteration pattern as sqlite_step. */
|
|
232
|
+
int fresco_pg_step(int cid) {
|
|
233
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return -1;
|
|
234
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
235
|
+
if (c == NULL) return -1;
|
|
236
|
+
if (c->result == NULL) {
|
|
237
|
+
/* First step: fire the query with accumulated binds. */
|
|
238
|
+
const char *param_values[FRESCO_PG_MAX_PARAMS];
|
|
239
|
+
int i;
|
|
240
|
+
for (i = 0; i < c->n_params; i++) {
|
|
241
|
+
/* libpq treats NULL as SQL NULL; we treat un-bound slots
|
|
242
|
+
* as empty strings to match sqlite's "bind silently makes
|
|
243
|
+
* the value visible" ergonomics. */
|
|
244
|
+
param_values[i] = c->params[i] ? c->params[i] : "";
|
|
245
|
+
}
|
|
246
|
+
c->result = PQexecParams(c->conn, c->sql, c->n_params,
|
|
247
|
+
NULL, param_values, NULL, NULL, 0);
|
|
248
|
+
ExecStatusType s = PQresultStatus(c->result);
|
|
249
|
+
if (s != PGRES_TUPLES_OK && s != PGRES_COMMAND_OK) {
|
|
250
|
+
PQclear(c->result);
|
|
251
|
+
c->result = NULL;
|
|
252
|
+
return -1;
|
|
253
|
+
}
|
|
254
|
+
c->ntuples = PQntuples(c->result);
|
|
255
|
+
c->nfields = PQnfields(c->result);
|
|
256
|
+
c->row = -1;
|
|
257
|
+
}
|
|
258
|
+
c->row++;
|
|
259
|
+
return c->row < c->ntuples ? 1 : 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const char *fresco_pg_col_str(int cid, int idx) {
|
|
263
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return "";
|
|
264
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
265
|
+
if (c == NULL || c->result == NULL || c->row < 0 || c->row >= c->ntuples) return "";
|
|
266
|
+
if (PQgetisnull(c->result, c->row, idx)) return "";
|
|
267
|
+
const char *v = PQgetvalue(c->result, c->row, idx);
|
|
268
|
+
if (!v) return "";
|
|
269
|
+
size_t n = strlen(v);
|
|
270
|
+
if (n >= FRESCO_PG_COL_BUFSIZE) n = FRESCO_PG_COL_BUFSIZE - 1;
|
|
271
|
+
char *buf = fresco_pg_col_buf[fresco_pg_col_slot];
|
|
272
|
+
fresco_pg_col_slot = (fresco_pg_col_slot + 1) % FRESCO_PG_COL_BUF_SLOTS;
|
|
273
|
+
memcpy(buf, v, n);
|
|
274
|
+
buf[n] = '\0';
|
|
275
|
+
return buf;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
int fresco_pg_col_int(int cid, int idx) {
|
|
279
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return 0;
|
|
280
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
281
|
+
if (c == NULL || c->result == NULL || c->row < 0 || c->row >= c->ntuples) return 0;
|
|
282
|
+
if (PQgetisnull(c->result, c->row, idx)) return 0;
|
|
283
|
+
const char *v = PQgetvalue(c->result, c->row, idx);
|
|
284
|
+
return v ? atoi(v) : 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
int fresco_pg_col_count(int cid) {
|
|
288
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return 0;
|
|
289
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
290
|
+
if (c == NULL) return 0;
|
|
291
|
+
return c->nfields;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
int fresco_pg_finalize(int cid) {
|
|
295
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return 0;
|
|
296
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
297
|
+
if (c == NULL) return 0;
|
|
298
|
+
fresco_pg_cursor_free(c);
|
|
299
|
+
fresco_pg_stmts[cid - 1] = NULL;
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Reset the cursor so it can be re-stepped (e.g. inside a loop where
|
|
304
|
+
* bound params change between iterations). Drops the prior PGresult
|
|
305
|
+
* but keeps bound params and the SQL string. The next step will
|
|
306
|
+
* re-execute. */
|
|
307
|
+
int fresco_pg_reset(int cid) {
|
|
308
|
+
if (cid < 1 || cid > FRESCO_PG_MAX_STMTS) return -1;
|
|
309
|
+
fresco_pg_cursor *c = fresco_pg_stmts[cid - 1];
|
|
310
|
+
if (c == NULL) return -1;
|
|
311
|
+
if (c->result) {
|
|
312
|
+
PQclear(c->result);
|
|
313
|
+
c->result = NULL;
|
|
314
|
+
}
|
|
315
|
+
c->row = -1;
|
|
316
|
+
c->ntuples = 0;
|
|
317
|
+
c->nfields = 0;
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Postgres doesn't have a per-connection "last rowid" — it has
|
|
322
|
+
* sequence currvals. `SELECT lastval()` returns the most recent value
|
|
323
|
+
* returned by any sequence in the current session. Only valid when the
|
|
324
|
+
* prior statement actually triggered a sequence; otherwise returns -1.
|
|
325
|
+
* For predictable behavior callers should hand-roll `INSERT ... RETURNING
|
|
326
|
+
* id` + `col_int(0)` instead. */
|
|
327
|
+
int fresco_pg_last_insert_rowid(int h) {
|
|
328
|
+
if (h < 1 || h > FRESCO_PG_MAX_HANDLES) return -1;
|
|
329
|
+
PGconn *conn = fresco_pg_handles[h - 1];
|
|
330
|
+
if (conn == NULL) return -1;
|
|
331
|
+
PGresult *res = PQexec(conn, "SELECT lastval()");
|
|
332
|
+
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) < 1) {
|
|
333
|
+
PQclear(res);
|
|
334
|
+
return -1;
|
|
335
|
+
}
|
|
336
|
+
int rowid = atoi(PQgetvalue(res, 0, 0));
|
|
337
|
+
PQclear(res);
|
|
338
|
+
return rowid;
|
|
339
|
+
}
|