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,403 @@
1
+ # Fresco Postgres adapter. Copied verbatim to generated/db_adapter.rb
2
+ # by `fresco build` when config/database.rb declares `Fresco.database
3
+ # :postgres`. Always loaded between runtime.rb and config/app.rb —
4
+ # boot order in lib/fresco/runtime/boot.rb.
5
+ #
6
+ # Same Ruby surface as db_sqlite.rb — `Fresco::Db::Postgres` exposes
7
+ # open/close/exec/prepare/bind_str/bind_int/step/col_str/col_int/
8
+ # col_count/finalize/reset/last_rowid + the first_str/first_int
9
+ # helpers. Generated code (M3+) writes against `Fresco::Db::Active`
10
+ # so swapping adapters is a config edit, not an action edit.
11
+ #
12
+ # Placeholder syntax differs from SQLite: libpq uses `$1, $2, ...` not
13
+ # `?`. User-written SQL has to use the adapter's native syntax; M3+
14
+ # codegen will paper over this when emitting prepared statements.
15
+ #
16
+ # Build dependency: generated/runtime/postgres.c (copied from the
17
+ # fresco gem at build time) needs libpq's headers (`libpq-fe.h`) and
18
+ # library (`-lpq`). `fresco release` auto-detects Homebrew/Postgres.app
19
+ # layouts and exports CPATH/LIBRARY_PATH accordingly. On Linux:
20
+ # `apt-get install libpq-dev`.
21
+ # `Pg` (not `Postgres`) so the FFI module name doesn't collide with
22
+ # `Fresco::Db::Postgres`. Matches the `fresco_pg_*` C-side prefix and
23
+ # mirrors the `Sqlite` (FFI) vs `SQLite` (wrapper) split.
24
+ module Pg
25
+ ffi_cflags "generated/runtime/postgres.c"
26
+ ffi_lib "pq"
27
+
28
+ ffi_func :fresco_pg_open, [:str], :int
29
+ ffi_func :fresco_pg_close, [:int], :int
30
+ ffi_func :fresco_pg_exec, [:int, :str], :int
31
+ ffi_func :fresco_pg_prepare, [:int, :str], :int
32
+ ffi_func :fresco_pg_bind_str, [:int, :int, :str], :int
33
+ ffi_func :fresco_pg_bind_int, [:int, :int, :int], :int
34
+ ffi_func :fresco_pg_step, [:int], :int
35
+ ffi_func :fresco_pg_col_str, [:int, :int], :str
36
+ ffi_func :fresco_pg_col_int, [:int, :int], :int
37
+ ffi_func :fresco_pg_col_count, [:int], :int
38
+ ffi_func :fresco_pg_finalize, [:int], :int
39
+ ffi_func :fresco_pg_reset, [:int], :int
40
+ ffi_func :fresco_pg_last_insert_rowid, [:int], :int
41
+ end
42
+
43
+ module Fresco
44
+ module Db
45
+ # Thin Ruby cover over the Postgres FFI module. Same shape as
46
+ # Fresco::Db::SQLite — identical method names, identical return
47
+ # conventions (0/-1 status, 1/0/-1 step, Int/Str cols, etc.).
48
+ #
49
+ # Differences worth knowing per-adapter:
50
+ # - URL-based open instead of file path. Pass a libpq conninfo
51
+ # string ("postgres://user@host/db" or "host=... user=...").
52
+ # - last_rowid runs `SELECT lastval()` — only valid when the
53
+ # prior statement triggered a sequence. For predictable INSERT
54
+ # id reads, use `INSERT ... RETURNING id` + step + col_int(0).
55
+ # - Placeholders are `$1, $2, ...` not `?`.
56
+ class Postgres
57
+ attr_accessor :dbh
58
+
59
+ def initialize
60
+ @dbh = -1
61
+ end
62
+
63
+ # Returns true on success, false on failure. URL accepts any libpq
64
+ # conninfo string. Calling #open twice on the same instance leaks
65
+ # the prior handle — close first.
66
+ def open(url = "")
67
+ h = Pg.fresco_pg_open(url)
68
+ return false if h < 0
69
+ @dbh = h
70
+ true
71
+ end
72
+
73
+ def close
74
+ if @dbh >= 0
75
+ Pg.fresco_pg_close(@dbh)
76
+ @dbh = -1
77
+ end
78
+ 0
79
+ end
80
+
81
+ # Run a no-bind statement (DDL, BEGIN/COMMIT, queries with literal
82
+ # constants). Returns true on success. For any user-supplied value
83
+ # always use prepare + bind + step + finalize.
84
+ def exec(sql = "")
85
+ return false if @dbh < 0
86
+ Pg.fresco_pg_exec(@dbh, sql) == 0
87
+ end
88
+
89
+ # Open a cursor on a parameterised statement. Returns an Int
90
+ # cursor id (>= 1) on success, -1 on failure. The query doesn't
91
+ # actually hit Postgres until the first #step — bind_str/bind_int
92
+ # calls between prepare and step accumulate the values that will
93
+ # be passed to PQexecParams.
94
+ def prepare(sql = "")
95
+ return -1 if @dbh < 0
96
+ Pg.fresco_pg_prepare(@dbh, sql)
97
+ end
98
+
99
+ def bind_str(cid = 0, idx = 0, value = "")
100
+ Pg.fresco_pg_bind_str(cid, idx, value)
101
+ end
102
+
103
+ def bind_int(cid = 0, idx = 0, value = 0)
104
+ Pg.fresco_pg_bind_int(cid, idx, value)
105
+ end
106
+
107
+ # 1 -> row available, 0 -> done, -1 -> error or invalid cursor.
108
+ # The first call fires PQexecParams with accumulated binds;
109
+ # subsequent calls walk the result one row at a time.
110
+ def step(cid = 0); Pg.fresco_pg_step(cid); end
111
+ def col_str(cid = 0, idx = 0); Pg.fresco_pg_col_str(cid, idx); end
112
+ def col_int(cid = 0, idx = 0); Pg.fresco_pg_col_int(cid, idx); end
113
+ def col_count(cid = 0); Pg.fresco_pg_col_count(cid); end
114
+ def finalize(cid = 0); Pg.fresco_pg_finalize(cid); end
115
+ def reset(cid = 0); Pg.fresco_pg_reset(cid); end
116
+
117
+ def last_rowid
118
+ return -1 if @dbh < 0
119
+ Pg.fresco_pg_last_insert_rowid(@dbh)
120
+ end
121
+
122
+ # Single-row single-column convenience. Same semantics as
123
+ # Fresco::Db::SQLite#first_str — pass `""` for no bind. Always
124
+ # finalises the cursor before returning.
125
+ def first_str(sql = "", p1 = "")
126
+ return "" if @dbh < 0
127
+ cid = Pg.fresco_pg_prepare(@dbh, sql)
128
+ return "" if cid < 0
129
+ if p1.length > 0
130
+ Pg.fresco_pg_bind_str(cid, 1, p1)
131
+ end
132
+ result = ""
133
+ if Pg.fresco_pg_step(cid) == 1
134
+ result = Pg.fresco_pg_col_str(cid, 0)
135
+ end
136
+ Pg.fresco_pg_finalize(cid)
137
+ result
138
+ end
139
+
140
+ def first_int(sql = "", p1 = "")
141
+ return 0 if @dbh < 0
142
+ cid = Pg.fresco_pg_prepare(@dbh, sql)
143
+ return 0 if cid < 0
144
+ if p1.length > 0
145
+ Pg.fresco_pg_bind_str(cid, 1, p1)
146
+ end
147
+ result = 0
148
+ if Pg.fresco_pg_step(cid) == 1
149
+ result = Pg.fresco_pg_col_int(cid, 0)
150
+ end
151
+ Pg.fresco_pg_finalize(cid)
152
+ result
153
+ end
154
+ end
155
+
156
+ # Alias the chosen adapter so generated code (M3+) and user actions
157
+ # can refer to one name regardless of which is linked.
158
+ #
159
+ # Don't call `Fresco::Db::Active.new` directly from action code:
160
+ # Spinel's analyzer doesn't chase constant aliases through Active,
161
+ # treats the receiver as int, and emits 0 for every subsequent
162
+ # method call on the returned object. Use `Fresco::Db.new_instance`
163
+ # below instead — the typed factory gives Spinel a concrete class
164
+ # to bind dispatch against.
165
+ Active = Postgres
166
+
167
+ # Adapter-agnostic factory. Returns a fresh unopened Postgres (or
168
+ # SQLite, in the other template) instance. Action code that wants
169
+ # to be DB-agnostic calls this; Spinel's analyzer sees the concrete
170
+ # return type per-build and dispatches correctly.
171
+ def self.new_instance
172
+ Postgres.new
173
+ end
174
+
175
+ # Lazy connection handle for the generated model layer. See the
176
+ # SQLite-template version for design notes — same pattern, same
177
+ # int-default-pinning rationale.
178
+ @conn_dbh = -1
179
+
180
+ def self.ensure_connection_dbh!
181
+ if @conn_dbh < 0
182
+ h = Pg.fresco_pg_open(Fresco.database_url)
183
+ @conn_dbh = h if h > 0
184
+ end
185
+ @conn_dbh
186
+ end
187
+
188
+ # SQL query logging. See db_sqlite.rb for full design notes — same
189
+ # FRESCO_LOG_SQL env gate, same in-request buffer, same `[db]` /
190
+ # `[db cached]` line format, same verb-driven color palette via
191
+ # `color_sql` in runtime.rb, same per-bind value capture for the
192
+ # trailing `[$1=..., $2=...]` annotation.
193
+ def self.log_sql?
194
+ ENV.fetch("FRESCO_LOG_SQL", "") != ""
195
+ end
196
+
197
+ @in_request = false
198
+ @log_buffer = [""]
199
+ @log_buffer.delete_at(0)
200
+
201
+ # See db_sqlite.rb for design notes on `@has_pending` vs
202
+ # `@pending_sql`: the former is the timing-state flag (always
203
+ # tracked), the latter is set only when FRESCO_LOG_SQL is on.
204
+ # Per-request totals live on `Fresco` (in runtime.rb) — same
205
+ # workaround for Spinel cross-file value-return collapse.
206
+ @has_pending = false
207
+ @pending_t0 = 0
208
+ @pending_sql = ""
209
+ @pending_label = ""
210
+ @pending_binds = ""
211
+
212
+ def self.begin_request!
213
+ @in_request = true
214
+ Fresco.reset_db_sql_stats!
215
+ end
216
+
217
+ # See db_sqlite.rb for the in-place-clear rationale — repeated
218
+ # `@log_buffer = [""]` reassignment crashes under Spinel after a
219
+ # few hundred requests (orphaned-array bus error).
220
+ def self.flush_log!
221
+ flush_pending!
222
+ i = 0
223
+ while i < @log_buffer.length
224
+ puts @log_buffer[i]
225
+ i += 1
226
+ end
227
+ while @log_buffer.length > 0
228
+ @log_buffer.delete_at(@log_buffer.length - 1)
229
+ end
230
+ @in_request = false
231
+ end
232
+
233
+ def self.flush_pending!
234
+ return unless @has_pending
235
+ dt = Sock.sphttp_elapsed_micros - @pending_t0
236
+ Fresco.add_sql_micros!(dt)
237
+ if @pending_sql != ""
238
+ line = @pending_label + Color.dim("(" + fmt_micros(dt) + ") ") + color_sql(@pending_sql)
239
+ if @pending_binds != ""
240
+ line += Color.dim(" [" + @pending_binds + "]")
241
+ end
242
+ emit(line)
243
+ end
244
+ @has_pending = false
245
+ @pending_t0 = 0
246
+ @pending_sql = ""
247
+ @pending_label = ""
248
+ @pending_binds = ""
249
+ end
250
+
251
+ def self.emit(line = "")
252
+ if @in_request
253
+ @log_buffer.push(line)
254
+ else
255
+ puts line
256
+ end
257
+ end
258
+
259
+ def self.record_bind_int(idx = 0, value = 0, name = "")
260
+ return if @pending_sql == ""
261
+ sep = @pending_binds == "" ? "" : ", "
262
+ label = name == "" ? "$" + idx.to_s : name
263
+ @pending_binds = @pending_binds + sep + label + "=" + value.to_s
264
+ end
265
+
266
+ def self.record_bind_str(idx = 0, value = "", name = "")
267
+ return if @pending_sql == ""
268
+ sep = @pending_binds == "" ? "" : ", "
269
+ label = name == "" ? "$" + idx.to_s : name
270
+ @pending_binds = @pending_binds + sep + label + "=\"" + value + "\""
271
+ end
272
+
273
+ # Adapter-agnostic surface for the model codegen. See db_sqlite.rb
274
+ # for design notes — same method names, same return conventions;
275
+ # only the underlying FFI delegate differs.
276
+ def self.exec(sql = "")
277
+ h = ensure_connection_dbh!
278
+ return false if h < 0
279
+ flush_pending! if @has_pending
280
+ t0 = Sock.sphttp_elapsed_micros
281
+ result = Pg.fresco_pg_exec(h, sql) == 0
282
+ dt = Sock.sphttp_elapsed_micros - t0
283
+ Fresco.add_sql_micros!(dt)
284
+ if log_sql?
285
+ emit(Color.dim(" [db] (" + fmt_micros(dt) + ") ") + color_sql(sql))
286
+ end
287
+ result
288
+ end
289
+
290
+ def self.prepare(sql = "")
291
+ h = ensure_connection_dbh!
292
+ return -1 if h < 0
293
+ flush_pending! if @has_pending
294
+ @pending_t0 = Sock.sphttp_elapsed_micros
295
+ @has_pending = true
296
+ if log_sql?
297
+ @pending_sql = sql
298
+ @pending_label = Color.dim(" [db] ")
299
+ @pending_binds = ""
300
+ end
301
+ Pg.fresco_pg_prepare(h, sql)
302
+ end
303
+
304
+ # Prepared-statement cache. See db_sqlite.rb for design notes —
305
+ # same shape, same StrIntHash pinning, same cursor-lifetime
306
+ # contract. For Postgres the reset clears the cached PGresult
307
+ # so the next step re-fires PQexecParams with whatever binds
308
+ # the caller sets between cached_prepare and step.
309
+ @stmt_cache = { "__t" => 0 }
310
+ @stmt_cache.delete("__t")
311
+
312
+ def self.cached_prepare(sql = "")
313
+ if @stmt_cache.key?(sql)
314
+ cid = @stmt_cache[sql]
315
+ Pg.fresco_pg_reset(cid)
316
+ flush_pending! if @has_pending
317
+ @pending_t0 = Sock.sphttp_elapsed_micros
318
+ @has_pending = true
319
+ if log_sql?
320
+ @pending_sql = sql
321
+ @pending_label = Color.dim(" [db cached] ")
322
+ @pending_binds = ""
323
+ end
324
+ return cid
325
+ end
326
+ cid = prepare(sql)
327
+ @stmt_cache[sql] = cid if cid > 0
328
+ cid
329
+ end
330
+
331
+ # The trailing `name = ""` is the column-name annotation for log
332
+ # output — see db_sqlite.rb for design notes.
333
+ def self.bind_str(cid = 0, idx = 0, value = "", name = "")
334
+ record_bind_str(idx, value, name) if log_sql?
335
+ Pg.fresco_pg_bind_str(cid, idx, value)
336
+ end
337
+
338
+ def self.bind_int(cid = 0, idx = 0, value = 0, name = "")
339
+ record_bind_int(idx, value, name) if log_sql?
340
+ Pg.fresco_pg_bind_int(cid, idx, value)
341
+ end
342
+
343
+ # See db_sqlite.rb for design notes on the two timing paths.
344
+ def self.step(cid = 0)
345
+ if @has_pending
346
+ result = Pg.fresco_pg_step(cid)
347
+ flush_pending!
348
+ return result
349
+ end
350
+ t0 = Sock.sphttp_elapsed_micros
351
+ result = Pg.fresco_pg_step(cid)
352
+ Fresco.add_sql_micros!(Sock.sphttp_elapsed_micros - t0)
353
+ result
354
+ end
355
+
356
+ def self.col_str(cid = 0, idx = 0); Pg.fresco_pg_col_str(cid, idx); end
357
+ def self.col_int(cid = 0, idx = 0); Pg.fresco_pg_col_int(cid, idx); end
358
+ def self.finalize(cid = 0); Pg.fresco_pg_finalize(cid); end
359
+ def self.reset(cid = 0); Pg.fresco_pg_reset(cid); end
360
+
361
+ def self.last_rowid
362
+ h = ensure_connection_dbh!
363
+ return -1 if h < 0
364
+ Pg.fresco_pg_last_insert_rowid(h)
365
+ end
366
+
367
+ # Eager-open the connection. Called once from Fresco::App#run at
368
+ # startup. Same rationale as the SQLite-template version —
369
+ # surface a bad DSN at process start instead of inside the first
370
+ # request.
371
+ def self.boot!
372
+ ensure_connection_dbh!
373
+ end
374
+
375
+ @tx_rollback_requested = false
376
+
377
+ def self.rollback!
378
+ @tx_rollback_requested = true
379
+ end
380
+
381
+ # Transaction support. Same shape as the SQLite-template version
382
+ # — explicit `Fresco::Db.rollback!` instead of rescue-on-raise
383
+ # because Spinel's rescue handling across FFI boundaries is iffy.
384
+ # Transaction primitives. See db_sqlite.rb for design notes —
385
+ # Spinel doesn't lower `&block` cleanly so we expose explicit
386
+ # begin/commit/rollback instead of a block-yielding transaction.
387
+ def self.begin_tx
388
+ exec("BEGIN")
389
+ @tx_rollback_requested = false
390
+ true
391
+ end
392
+
393
+ def self.commit_tx
394
+ if @tx_rollback_requested
395
+ exec("ROLLBACK")
396
+ @tx_rollback_requested = false
397
+ return false
398
+ end
399
+ exec("COMMIT")
400
+ true
401
+ end
402
+ end
403
+ end