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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/exe/fresco +3 -0
  3. data/lib/fresco/application.rb +12 -0
  4. data/lib/fresco/cli/build.rb +682 -0
  5. data/lib/fresco/cli/dev.rb +17 -0
  6. data/lib/fresco/cli/dev_loop.rb +815 -0
  7. data/lib/fresco/cli/new.rb +120 -0
  8. data/lib/fresco/cli/release.rb +76 -0
  9. data/lib/fresco/cli.rb +56 -0
  10. data/lib/fresco/database_config.rb +34 -0
  11. data/lib/fresco/generators/app/Gemfile.tt +18 -0
  12. data/lib/fresco/generators/app/README.md.tt +32 -0
  13. data/lib/fresco/generators/app/app/action.rb.tt +20 -0
  14. data/lib/fresco/generators/app/app/actions/root_path.rb.tt +5 -0
  15. data/lib/fresco/generators/app/app/views/layouts/application.html.erb +29 -0
  16. data/lib/fresco/generators/app/app/views/root_path.html.erb +8 -0
  17. data/lib/fresco/generators/app/app.rb.tt +15 -0
  18. data/lib/fresco/generators/app/bin/build +2 -0
  19. data/lib/fresco/generators/app/bin/dev +2 -0
  20. data/lib/fresco/generators/app/bin/release +2 -0
  21. data/lib/fresco/generators/app/config/app.rb.tt +26 -0
  22. data/lib/fresco/generators/app/config/database.rb +17 -0
  23. data/lib/fresco/generators/app/config/routes.rb +11 -0
  24. data/lib/fresco/generators/app/db/schema.rb +14 -0
  25. data/lib/fresco/generators/app/public/404.html +87 -0
  26. data/lib/fresco/generators/app/public/500.html +84 -0
  27. data/lib/fresco/migration_builder.rb +55 -0
  28. data/lib/fresco/model_builder.rb +54 -0
  29. data/lib/fresco/paths.rb +20 -0
  30. data/lib/fresco/router.rb +67 -0
  31. data/lib/fresco/runtime/boot.rb +34 -0
  32. data/lib/fresco/runtime/db_postgres.rb +403 -0
  33. data/lib/fresco/runtime/db_sqlite.rb +495 -0
  34. data/lib/fresco/runtime/http.c +456 -0
  35. data/lib/fresco/runtime/postgres.c +339 -0
  36. data/lib/fresco/runtime/runtime.rb +1810 -0
  37. data/lib/fresco/runtime/sqlite.c +220 -0
  38. data/lib/fresco/runtime/welcome.rb +152 -0
  39. data/lib/fresco/schema_builder.rb +71 -0
  40. data/lib/fresco/templates/dispatch.rb.erb +32 -0
  41. data/lib/fresco/templates/layout_dispatch.rb.erb +16 -0
  42. data/lib/fresco/templates/manifest.rb.erb +5 -0
  43. data/lib/fresco/templates/migrations.rb.erb +152 -0
  44. data/lib/fresco/templates/model.rb.erb +223 -0
  45. data/lib/fresco/templates/view.rb.erb +5 -0
  46. data/lib/fresco/version.rb +3 -0
  47. data/lib/fresco.rb +61 -0
  48. 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
+ }