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,495 @@
|
|
|
1
|
+
# Fresco SQLite adapter. Copied verbatim to generated/db_adapter.rb
|
|
2
|
+
# by `fresco build` when config/database.rb declares `Fresco.database
|
|
3
|
+
# :sqlite`. Always loaded between runtime.rb and config/app.rb — boot
|
|
4
|
+
# order in lib/fresco/runtime/boot.rb.
|
|
5
|
+
#
|
|
6
|
+
# Contains everything an action needs to talk to SQLite under both
|
|
7
|
+
# CRuby (dev) and Spinel (release): the FFI module pointing at
|
|
8
|
+
# generated/runtime/sqlite.c, the Ruby wrapper, and the
|
|
9
|
+
# `Fresco::Db::Active` alias that the M3+ codegen will target.
|
|
10
|
+
#
|
|
11
|
+
# The `Sqlite` module's ffi_cflags/ffi_lib pull
|
|
12
|
+
# generated/runtime/sqlite.c into Spinel's cc invocation and
|
|
13
|
+
# `-lsqlite3` into the link line. Under CRuby the same declarations
|
|
14
|
+
# are no-ops (`fresco dev` stubs them on Module) and dev installs
|
|
15
|
+
# singleton-method stand-ins that route through the `sqlite3` gem.
|
|
16
|
+
module Sqlite
|
|
17
|
+
ffi_cflags "generated/runtime/sqlite.c"
|
|
18
|
+
ffi_lib "sqlite3"
|
|
19
|
+
|
|
20
|
+
ffi_func :fresco_sqlite_open, [:str], :int
|
|
21
|
+
ffi_func :fresco_sqlite_close, [:int], :int
|
|
22
|
+
ffi_func :fresco_sqlite_exec, [:int, :str], :int
|
|
23
|
+
ffi_func :fresco_sqlite_prepare, [:int, :str], :int
|
|
24
|
+
ffi_func :fresco_sqlite_bind_str, [:int, :int, :str], :int
|
|
25
|
+
ffi_func :fresco_sqlite_bind_int, [:int, :int, :int], :int
|
|
26
|
+
ffi_func :fresco_sqlite_step, [:int], :int
|
|
27
|
+
ffi_func :fresco_sqlite_col_str, [:int, :int], :str
|
|
28
|
+
ffi_func :fresco_sqlite_col_int, [:int, :int], :int
|
|
29
|
+
ffi_func :fresco_sqlite_col_count, [:int], :int
|
|
30
|
+
ffi_func :fresco_sqlite_finalize, [:int], :int
|
|
31
|
+
ffi_func :fresco_sqlite_reset, [:int], :int
|
|
32
|
+
ffi_func :fresco_sqlite_last_insert_rowid, [:int], :int
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Fresco
|
|
36
|
+
module Db
|
|
37
|
+
# Thin Ruby cover over the Sqlite FFI module. Methods return values
|
|
38
|
+
# straight from the C side — handle ids as Int, cursor ids as Int,
|
|
39
|
+
# status codes as Int (0 success, -1 error), column values as Str
|
|
40
|
+
# or Int. No exceptions on DB errors; callers branch on `< 0`.
|
|
41
|
+
#
|
|
42
|
+
# Spinel-shape constraints worth flagging up front:
|
|
43
|
+
# - Attribute is named `dbh` (not `handle`) — the latter widens
|
|
44
|
+
# poly-dispatch return types across classes that also define
|
|
45
|
+
# `handle` (see Tep's same workaround). Keeps every method
|
|
46
|
+
# name on this class collision-free across the framework.
|
|
47
|
+
# - `cursor_id` is returned by #prepare and threaded through
|
|
48
|
+
# #step/#bind_*/#col_*/#finalize/#reset. The single-cursor
|
|
49
|
+
# pattern works too — just call them with one cursor id at a
|
|
50
|
+
# time — but the API allows several open at once (M3+ codegen
|
|
51
|
+
# stacks finders inside each other).
|
|
52
|
+
# - Defaults on every typed parameter pin Spinel's analyzer to
|
|
53
|
+
# the right slot type — same rule as Params/Response in this
|
|
54
|
+
# file. Required-but-defaultless params collapse to mrb_int
|
|
55
|
+
# under Spinel (see [[spinel_initialize_kwargs]]).
|
|
56
|
+
class SQLite
|
|
57
|
+
attr_accessor :dbh
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
@dbh = -1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns true on success, false on failure. Path may be a real
|
|
64
|
+
# file or `:memory:` for an anonymous in-memory database. Calling
|
|
65
|
+
# #open twice on the same instance leaks the prior handle — close
|
|
66
|
+
# first.
|
|
67
|
+
def open(path = "")
|
|
68
|
+
h = Sqlite.fresco_sqlite_open(path)
|
|
69
|
+
return false if h < 0
|
|
70
|
+
@dbh = h
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close
|
|
75
|
+
if @dbh >= 0
|
|
76
|
+
Sqlite.fresco_sqlite_close(@dbh)
|
|
77
|
+
@dbh = -1
|
|
78
|
+
end
|
|
79
|
+
0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Run a no-bind statement (DDL, BEGIN/COMMIT, queries with literal
|
|
83
|
+
# constants). Returns true on success. For any user-supplied value
|
|
84
|
+
# always use prepare + bind + step + finalize.
|
|
85
|
+
def exec(sql = "")
|
|
86
|
+
return false if @dbh < 0
|
|
87
|
+
Sqlite.fresco_sqlite_exec(@dbh, sql) == 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Open a cursor on a parameterised statement. Returns an Int
|
|
91
|
+
# cursor id (>= 1) on success, -1 on failure. Subsequent bind_str/
|
|
92
|
+
# bind_int/step/col_str/col_int/col_count/finalize/reset calls
|
|
93
|
+
# take this id as their first argument. Always pair with #finalize.
|
|
94
|
+
def prepare(sql = "")
|
|
95
|
+
return -1 if @dbh < 0
|
|
96
|
+
Sqlite.fresco_sqlite_prepare(@dbh, sql)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def bind_str(cid = 0, idx = 0, value = "")
|
|
100
|
+
Sqlite.fresco_sqlite_bind_str(cid, idx, value)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def bind_int(cid = 0, idx = 0, value = 0)
|
|
104
|
+
Sqlite.fresco_sqlite_bind_int(cid, idx, value)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 1 -> row available, 0 -> done, -1 -> error or invalid cursor.
|
|
108
|
+
def step(cid = 0); Sqlite.fresco_sqlite_step(cid); end
|
|
109
|
+
def col_str(cid = 0, idx = 0); Sqlite.fresco_sqlite_col_str(cid, idx); end
|
|
110
|
+
def col_int(cid = 0, idx = 0); Sqlite.fresco_sqlite_col_int(cid, idx); end
|
|
111
|
+
def col_count(cid = 0); Sqlite.fresco_sqlite_col_count(cid); end
|
|
112
|
+
def finalize(cid = 0); Sqlite.fresco_sqlite_finalize(cid); end
|
|
113
|
+
def reset(cid = 0); Sqlite.fresco_sqlite_reset(cid); end
|
|
114
|
+
|
|
115
|
+
def last_rowid
|
|
116
|
+
return -1 if @dbh < 0
|
|
117
|
+
Sqlite.fresco_sqlite_last_insert_rowid(@dbh)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Convenience: prepare a single-row single-column query with at
|
|
121
|
+
# most one Str bind, step once, return col 0 as Str, finalize.
|
|
122
|
+
# Pass `""` for no bind. Mirrors Tep::SQLite#first_str so M1
|
|
123
|
+
# smoke tests stay terse.
|
|
124
|
+
def first_str(sql = "", p1 = "")
|
|
125
|
+
return "" if @dbh < 0
|
|
126
|
+
cid = Sqlite.fresco_sqlite_prepare(@dbh, sql)
|
|
127
|
+
return "" if cid < 0
|
|
128
|
+
if p1.length > 0
|
|
129
|
+
Sqlite.fresco_sqlite_bind_str(cid, 1, p1)
|
|
130
|
+
end
|
|
131
|
+
result = ""
|
|
132
|
+
if Sqlite.fresco_sqlite_step(cid) == 1
|
|
133
|
+
result = Sqlite.fresco_sqlite_col_str(cid, 0)
|
|
134
|
+
end
|
|
135
|
+
Sqlite.fresco_sqlite_finalize(cid)
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def first_int(sql = "", p1 = "")
|
|
140
|
+
return 0 if @dbh < 0
|
|
141
|
+
cid = Sqlite.fresco_sqlite_prepare(@dbh, sql)
|
|
142
|
+
return 0 if cid < 0
|
|
143
|
+
if p1.length > 0
|
|
144
|
+
Sqlite.fresco_sqlite_bind_str(cid, 1, p1)
|
|
145
|
+
end
|
|
146
|
+
result = 0
|
|
147
|
+
if Sqlite.fresco_sqlite_step(cid) == 1
|
|
148
|
+
result = Sqlite.fresco_sqlite_col_int(cid, 0)
|
|
149
|
+
end
|
|
150
|
+
Sqlite.fresco_sqlite_finalize(cid)
|
|
151
|
+
result
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Alias the chosen adapter so generated code (M3+) and user actions
|
|
156
|
+
# can refer to one name regardless of which is linked.
|
|
157
|
+
#
|
|
158
|
+
# Don't call `Fresco::Db::Active.new` directly from action code:
|
|
159
|
+
# Spinel's analyzer doesn't chase constant aliases through Active,
|
|
160
|
+
# treats the receiver as int, and emits 0 for every subsequent
|
|
161
|
+
# method call on the returned object. Use `Fresco::Db.new_instance`
|
|
162
|
+
# below instead — the typed factory gives Spinel a concrete class
|
|
163
|
+
# to bind dispatch against.
|
|
164
|
+
Active = SQLite
|
|
165
|
+
|
|
166
|
+
# Adapter-agnostic factory. Returns a fresh unopened SQLite (or
|
|
167
|
+
# Postgres, in the other template) instance. Action code that
|
|
168
|
+
# wants to be DB-agnostic calls this; Spinel's analyzer sees the
|
|
169
|
+
# concrete return type per-build and dispatches correctly.
|
|
170
|
+
def self.new_instance
|
|
171
|
+
SQLite.new
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Lazy connection handle for the generated model layer. Held as
|
|
175
|
+
# an Int (the slot-table index returned by fresco_sqlite_open),
|
|
176
|
+
# NOT as a wrapper instance — module-level `@x = SQLite.new` at
|
|
177
|
+
# load time tripped Spinel's static-initializer requirement; an
|
|
178
|
+
# int default sidesteps it entirely. Initialised to -1 ("not
|
|
179
|
+
# opened yet"); ensure_connection_dbh! lazy-opens on first model
|
|
180
|
+
# call and re-uses the slot for the process lifetime. M5 replaces
|
|
181
|
+
# this with explicit per-worker boot!/shutdown! plumbing.
|
|
182
|
+
@conn_dbh = -1
|
|
183
|
+
|
|
184
|
+
def self.ensure_connection_dbh!
|
|
185
|
+
if @conn_dbh < 0
|
|
186
|
+
h = Sqlite.fresco_sqlite_open(Fresco.database_path)
|
|
187
|
+
@conn_dbh = h if h > 0
|
|
188
|
+
end
|
|
189
|
+
@conn_dbh
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# SQL query logging. Off by default — set FRESCO_LOG_SQL to any
|
|
193
|
+
# non-empty value to enable (mirrors the NO_COLOR convention used
|
|
194
|
+
# by Color.enabled? in runtime.rb).
|
|
195
|
+
#
|
|
196
|
+
# Output format — lines are nested under the [request] log line by
|
|
197
|
+
# buffering during the request and flushing after log_request_full
|
|
198
|
+
# prints. With bindings captured at bind_int/bind_str call time,
|
|
199
|
+
# the rendered line looks like:
|
|
200
|
+
#
|
|
201
|
+
# [request] GET /users 200 (1234us)
|
|
202
|
+
# [db cached] SELECT id, name FROM users WHERE id = ? [$1=5]
|
|
203
|
+
#
|
|
204
|
+
# Buffering uses a single "current pending entry" (per-module, not
|
|
205
|
+
# per-cursor) — codegen always sequences `prepare → bind* → step`
|
|
206
|
+
# without interleaving, so the latest pending row is the right one
|
|
207
|
+
# to attribute binds to. Each new prepare/exec flushes the prior
|
|
208
|
+
# pending entry to the buffer first, so nothing is lost even if a
|
|
209
|
+
# caller does two prepares back-to-back without intervening steps.
|
|
210
|
+
#
|
|
211
|
+
# Outside a request (migrations, db:migrate, db:rollback) buffering
|
|
212
|
+
# is off — lines print directly so they interleave naturally with
|
|
213
|
+
# the [migrate] progress lines. The runtime toggles `@in_request`
|
|
214
|
+
# via begin_request! / flush_log! around each request.
|
|
215
|
+
def self.log_sql?
|
|
216
|
+
ENV.fetch("FRESCO_LOG_SQL", "") != ""
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# In-request buffering state. `@in_request` flips on at the start
|
|
220
|
+
# of each request via begin_request! and back off when flush_log!
|
|
221
|
+
# drains the buffer. `@log_buffer` is seeded with a placeholder
|
|
222
|
+
# entry that's immediately deleted — the same seed-and-clear trick
|
|
223
|
+
# the rest of runtime.rb uses to pin an Array<String> shape under
|
|
224
|
+
# Spinel's analyzer (a bare `[]` would lower to a poly-typed array
|
|
225
|
+
# and the first push would widen each entry to RbVal).
|
|
226
|
+
@in_request = false
|
|
227
|
+
@log_buffer = [""]
|
|
228
|
+
@log_buffer.delete_at(0)
|
|
229
|
+
|
|
230
|
+
# `@has_pending` is the timing-state flag: true between a
|
|
231
|
+
# prepare/cached_prepare and the first step that follows. We need
|
|
232
|
+
# it independent of `@pending_sql` because timing runs even when
|
|
233
|
+
# FRESCO_LOG_SQL is off (so @pending_sql may be empty), but the
|
|
234
|
+
# prepare→step bracket still needs to be measured for the
|
|
235
|
+
# request-line roll-up.
|
|
236
|
+
@has_pending = false
|
|
237
|
+
@pending_t0 = 0
|
|
238
|
+
@pending_sql = ""
|
|
239
|
+
@pending_label = ""
|
|
240
|
+
@pending_binds = ""
|
|
241
|
+
|
|
242
|
+
# The per-request SQL totals (`@db_sql_total_micros`,
|
|
243
|
+
# `@db_sql_count`) live on the `Fresco` module in runtime.rb,
|
|
244
|
+
# not here. That's the workaround for the Spinel cross-file-
|
|
245
|
+
# value-return collapse — see the comment on those counters in
|
|
246
|
+
# runtime.rb. Below we call `Fresco.add_sql_micros!` (side-effect
|
|
247
|
+
# cross-file call, works fine) and `Fresco.reset_db_sql_stats!`
|
|
248
|
+
# (same).
|
|
249
|
+
def self.begin_request!
|
|
250
|
+
@in_request = true
|
|
251
|
+
Fresco.reset_db_sql_stats!
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Called from the request handler after log_request_full prints.
|
|
255
|
+
# Drains any pending entry (covering queries that prepared but
|
|
256
|
+
# never reached step — rare, but possible if a handler raises
|
|
257
|
+
# mid-bind), then dumps every buffered line indented under the
|
|
258
|
+
# [request] line. Resets state for the next request.
|
|
259
|
+
#
|
|
260
|
+
# NB: clear the buffer in place rather than re-assigning the
|
|
261
|
+
# ivar. Reassigning `@log_buffer = [""]; @log_buffer.delete_at(0)`
|
|
262
|
+
# every request orphans the old array; Spinel doesn't reclaim it
|
|
263
|
+
# and a few hundred requests in we get a bus error on the next
|
|
264
|
+
# access. delete_at-from-the-tail is a safe in-place reset —
|
|
265
|
+
# `sp_StrArray_delete_at` is a slot pop, no realloc.
|
|
266
|
+
def self.flush_log!
|
|
267
|
+
flush_pending!
|
|
268
|
+
i = 0
|
|
269
|
+
while i < @log_buffer.length
|
|
270
|
+
puts @log_buffer[i]
|
|
271
|
+
i += 1
|
|
272
|
+
end
|
|
273
|
+
while @log_buffer.length > 0
|
|
274
|
+
@log_buffer.delete_at(@log_buffer.length - 1)
|
|
275
|
+
end
|
|
276
|
+
@in_request = false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Emit the current pending entry (if any) and clear it. Two jobs:
|
|
280
|
+
# 1. Compute the [prepare → now] duration and fold it into the
|
|
281
|
+
# per-request totals. Runs even when FRESCO_LOG_SQL is off so
|
|
282
|
+
# the request-line roll-up stays accurate.
|
|
283
|
+
# 2. When FRESCO_LOG_SQL is on, emit the formatted log line with
|
|
284
|
+
# that duration, the column-binding annotation, and the
|
|
285
|
+
# [db] / [db cached] marker.
|
|
286
|
+
# Used as a boundary between consecutive prepares (so a prepare-
|
|
287
|
+
# without-step still gets timed and surfaced) and as the bind→step
|
|
288
|
+
# hand-off (so step's emission picks up the accumulated binds).
|
|
289
|
+
def self.flush_pending!
|
|
290
|
+
return unless @has_pending
|
|
291
|
+
dt = Sock.sphttp_elapsed_micros - @pending_t0
|
|
292
|
+
Fresco.add_sql_micros!(dt)
|
|
293
|
+
if @pending_sql != ""
|
|
294
|
+
line = @pending_label + Color.dim("(" + fmt_micros(dt) + ") ") + color_sql(@pending_sql)
|
|
295
|
+
if @pending_binds != ""
|
|
296
|
+
line += Color.dim(" [" + @pending_binds + "]")
|
|
297
|
+
end
|
|
298
|
+
emit(line)
|
|
299
|
+
end
|
|
300
|
+
@has_pending = false
|
|
301
|
+
@pending_t0 = 0
|
|
302
|
+
@pending_sql = ""
|
|
303
|
+
@pending_label = ""
|
|
304
|
+
@pending_binds = ""
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Single-line emit. Routes through the buffer when we're inside a
|
|
308
|
+
# request (so the line lands under the [request] log), or straight
|
|
309
|
+
# to stdout for migrations / one-off CLI work where there's no
|
|
310
|
+
# request to nest under.
|
|
311
|
+
def self.emit(line = "")
|
|
312
|
+
if @in_request
|
|
313
|
+
@log_buffer.push(line)
|
|
314
|
+
else
|
|
315
|
+
puts line
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Record one bind on the current pending entry. When `name` is
|
|
320
|
+
# non-empty, use it as the label (`email="bob"`); when empty, fall
|
|
321
|
+
# back to the placeholder index (`$1="bob"`). Model codegen passes
|
|
322
|
+
# the column name through; ad-hoc `Db.exec` / `Model.query` / hand-
|
|
323
|
+
# written SQL paths call without a name and get the `$N` form.
|
|
324
|
+
# Strings get double-quoted so a numeric-looking string ("42")
|
|
325
|
+
# isn't ambiguous with the int 42 when reading logs.
|
|
326
|
+
def self.record_bind_int(idx = 0, value = 0, name = "")
|
|
327
|
+
return if @pending_sql == ""
|
|
328
|
+
sep = @pending_binds == "" ? "" : ", "
|
|
329
|
+
label = name == "" ? "$" + idx.to_s : name
|
|
330
|
+
@pending_binds = @pending_binds + sep + label + "=" + value.to_s
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def self.record_bind_str(idx = 0, value = "", name = "")
|
|
334
|
+
return if @pending_sql == ""
|
|
335
|
+
sep = @pending_binds == "" ? "" : ", "
|
|
336
|
+
label = name == "" ? "$" + idx.to_s : name
|
|
337
|
+
@pending_binds = @pending_binds + sep + label + "=\"" + value + "\""
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Adapter-agnostic surface for the model codegen. Each generated
|
|
341
|
+
# `User.find` / `User.insert` etc. calls these (rather than the
|
|
342
|
+
# wrapper class) so the same model source runs against either
|
|
343
|
+
# backend — only the SQL strings differ (and those are emitted
|
|
344
|
+
# by bin/build at model-codegen time).
|
|
345
|
+
def self.exec(sql = "")
|
|
346
|
+
h = ensure_connection_dbh!
|
|
347
|
+
return false if h < 0
|
|
348
|
+
flush_pending! if @has_pending
|
|
349
|
+
t0 = Sock.sphttp_elapsed_micros
|
|
350
|
+
result = Sqlite.fresco_sqlite_exec(h, sql) == 0
|
|
351
|
+
dt = Sock.sphttp_elapsed_micros - t0
|
|
352
|
+
Fresco.add_sql_micros!(dt)
|
|
353
|
+
if log_sql?
|
|
354
|
+
emit(Color.dim(" [db] (" + fmt_micros(dt) + ") ") + color_sql(sql))
|
|
355
|
+
end
|
|
356
|
+
result
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def self.prepare(sql = "")
|
|
360
|
+
h = ensure_connection_dbh!
|
|
361
|
+
return -1 if h < 0
|
|
362
|
+
flush_pending! if @has_pending
|
|
363
|
+
@pending_t0 = Sock.sphttp_elapsed_micros
|
|
364
|
+
@has_pending = true
|
|
365
|
+
if log_sql?
|
|
366
|
+
@pending_sql = sql
|
|
367
|
+
@pending_label = Color.dim(" [db] ")
|
|
368
|
+
@pending_binds = ""
|
|
369
|
+
end
|
|
370
|
+
Sqlite.fresco_sqlite_prepare(h, sql)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Prepared-statement cache. Seed-and-clear pins the hash to
|
|
374
|
+
# StrIntHash for Spinel's analyzer — sql string → cursor id.
|
|
375
|
+
# Generated model methods call `cached_prepare` and skip the
|
|
376
|
+
# matching `finalize`; the cursor lives for the process lifetime
|
|
377
|
+
# and gets reset (rewound + bindings preserved) on subsequent
|
|
378
|
+
# calls. Ad-hoc callers writing one-shot queries still use the
|
|
379
|
+
# plain `prepare` / `finalize` pair below.
|
|
380
|
+
@stmt_cache = { "__t" => 0 }
|
|
381
|
+
@stmt_cache.delete("__t")
|
|
382
|
+
|
|
383
|
+
def self.cached_prepare(sql = "")
|
|
384
|
+
if @stmt_cache.key?(sql)
|
|
385
|
+
cid = @stmt_cache[sql]
|
|
386
|
+
Sqlite.fresco_sqlite_reset(cid)
|
|
387
|
+
flush_pending! if @has_pending
|
|
388
|
+
@pending_t0 = Sock.sphttp_elapsed_micros
|
|
389
|
+
@has_pending = true
|
|
390
|
+
if log_sql?
|
|
391
|
+
@pending_sql = sql
|
|
392
|
+
@pending_label = Color.dim(" [db cached] ")
|
|
393
|
+
@pending_binds = ""
|
|
394
|
+
end
|
|
395
|
+
return cid
|
|
396
|
+
end
|
|
397
|
+
cid = prepare(sql)
|
|
398
|
+
@stmt_cache[sql] = cid if cid > 0
|
|
399
|
+
cid
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# The trailing `name = ""` is the column-name annotation for log
|
|
403
|
+
# output — codegen passes it from the schema, ad-hoc callers omit
|
|
404
|
+
# it. The FFI delegate ignores it; it's purely for the log layer.
|
|
405
|
+
def self.bind_str(cid = 0, idx = 0, value = "", name = "")
|
|
406
|
+
record_bind_str(idx, value, name) if log_sql?
|
|
407
|
+
Sqlite.fresco_sqlite_bind_str(cid, idx, value)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def self.bind_int(cid = 0, idx = 0, value = 0, name = "")
|
|
411
|
+
record_bind_int(idx, value, name) if log_sql?
|
|
412
|
+
Sqlite.fresco_sqlite_bind_int(cid, idx, value)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Two timing paths:
|
|
416
|
+
# - First step after a prepare/cached_prepare: flush_pending!
|
|
417
|
+
# computes `dt = now - @pending_t0` (covers prepare + binds
|
|
418
|
+
# + this step) and folds it into the per-request totals.
|
|
419
|
+
# - Iteration step on an already-flushed cursor (SELECT row
|
|
420
|
+
# pump): time just this call and accumulate. A 1000-row
|
|
421
|
+
# SELECT counts every row's micros toward the request total,
|
|
422
|
+
# not just the first.
|
|
423
|
+
def self.step(cid = 0)
|
|
424
|
+
if @has_pending
|
|
425
|
+
result = Sqlite.fresco_sqlite_step(cid)
|
|
426
|
+
flush_pending!
|
|
427
|
+
return result
|
|
428
|
+
end
|
|
429
|
+
t0 = Sock.sphttp_elapsed_micros
|
|
430
|
+
result = Sqlite.fresco_sqlite_step(cid)
|
|
431
|
+
Fresco.add_sql_micros!(Sock.sphttp_elapsed_micros - t0)
|
|
432
|
+
result
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def self.col_str(cid = 0, idx = 0); Sqlite.fresco_sqlite_col_str(cid, idx); end
|
|
436
|
+
def self.col_int(cid = 0, idx = 0); Sqlite.fresco_sqlite_col_int(cid, idx); end
|
|
437
|
+
def self.finalize(cid = 0); Sqlite.fresco_sqlite_finalize(cid); end
|
|
438
|
+
def self.reset(cid = 0); Sqlite.fresco_sqlite_reset(cid); end
|
|
439
|
+
|
|
440
|
+
def self.last_rowid
|
|
441
|
+
h = ensure_connection_dbh!
|
|
442
|
+
return -1 if h < 0
|
|
443
|
+
Sqlite.fresco_sqlite_last_insert_rowid(h)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Eager-open the connection. Called once from Fresco::App#run at
|
|
447
|
+
# startup. ensure_connection_dbh! already lazy-opens on first
|
|
448
|
+
# use, but eager-open surfaces a bad DSN at process start
|
|
449
|
+
# instead of inside the first request — cheaper to fail there.
|
|
450
|
+
def self.boot!
|
|
451
|
+
ensure_connection_dbh!
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Transaction support. The block runs between BEGIN and COMMIT;
|
|
455
|
+
# set `Fresco::Db.rollback!` inside the block to force a
|
|
456
|
+
# ROLLBACK instead. We don't rely on Ruby `rescue` for rollback-
|
|
457
|
+
# on-exception — Spinel's rescue handling across FFI boundaries
|
|
458
|
+
# is iffy, and the explicit-opt-in shape is what the plan
|
|
459
|
+
# specifies. Returns true on commit, false on rollback.
|
|
460
|
+
@tx_rollback_requested = false
|
|
461
|
+
|
|
462
|
+
def self.rollback!
|
|
463
|
+
@tx_rollback_requested = true
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Transaction primitives. Spinel doesn't lower custom `&block`
|
|
467
|
+
# parameters cleanly (see the `each(&blk)` note in runtime.rb's
|
|
468
|
+
# Params class), so we expose explicit begin/commit/rollback
|
|
469
|
+
# instead of a block-yielding `transaction { ... }`. Pattern:
|
|
470
|
+
#
|
|
471
|
+
# Fresco::Db.begin_tx
|
|
472
|
+
# User.insert(...)
|
|
473
|
+
# Fresco::Db.rollback! if some_condition
|
|
474
|
+
# Fresco::Db.commit_tx
|
|
475
|
+
#
|
|
476
|
+
# commit_tx checks the rollback flag and emits ROLLBACK instead
|
|
477
|
+
# of COMMIT if it's set. Returns true on commit, false on
|
|
478
|
+
# rollback so callers can branch on the outcome.
|
|
479
|
+
def self.begin_tx
|
|
480
|
+
exec("BEGIN")
|
|
481
|
+
@tx_rollback_requested = false
|
|
482
|
+
true
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def self.commit_tx
|
|
486
|
+
if @tx_rollback_requested
|
|
487
|
+
exec("ROLLBACK")
|
|
488
|
+
@tx_rollback_requested = false
|
|
489
|
+
return false
|
|
490
|
+
end
|
|
491
|
+
exec("COMMIT")
|
|
492
|
+
true
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|