Module: Fresco::Db

Defined in:
lib/fresco/runtime/db_sqlite.rb,
lib/fresco/runtime/db_postgres.rb

Defined Under Namespace

Classes: Postgres, SQLite

Constant Summary collapse

Active =

Alias the chosen adapter so generated code (M3+) and user actions can refer to one name regardless of which is linked.

Don’t call ‘Fresco::Db::Active.new` directly from action code: Spinel’s analyzer doesn’t chase constant aliases through Active, treats the receiver as int, and emits 0 for every subsequent method call on the returned object. Use ‘Fresco::Db.new_instance` below instead — the typed factory gives Spinel a concrete class to bind dispatch against.

Postgres

Class Method Summary collapse

Class Method Details

.begin_request!Object

The per-request SQL totals (‘@db_sql_total_micros`, `@db_sql_count`) live on the `Fresco` module in runtime.rb, not here. That’s the workaround for the Spinel cross-file- value-return collapse — see the comment on those counters in runtime.rb. Below we call ‘Fresco.add_sql_micros!` (side-effect cross-file call, works fine) and `Fresco.reset_db_sql_stats!` (same).



249
250
251
252
# File 'lib/fresco/runtime/db_sqlite.rb', line 249

def self.begin_request!
  @in_request = true
  Fresco.reset_db_sql_stats!
end

.begin_txObject

Transaction support. Same shape as the SQLite-template version — explicit ‘Fresco::Db.rollback!` instead of rescue-on-raise because Spinel’s rescue handling across FFI boundaries is iffy. Transaction primitives. See db_sqlite.rb for design notes —Spinel doesn’t lower ‘&block` cleanly so we expose explicit begin/commit/rollback instead of a block-yielding transaction.



479
480
481
482
483
# File 'lib/fresco/runtime/db_sqlite.rb', line 479

def self.begin_tx
  exec("BEGIN")
  @tx_rollback_requested = false
  true
end

.bind_int(cid = 0, idx = 0, value = 0, name = "") ⇒ Object



410
411
412
413
# File 'lib/fresco/runtime/db_sqlite.rb', line 410

def self.bind_int(cid = 0, idx = 0, value = 0, name = "")
  record_bind_int(idx, value, name) if log_sql?
  Sqlite.fresco_sqlite_bind_int(cid, idx, value)
end

.bind_str(cid = 0, idx = 0, value = "", name = "") ⇒ Object

The trailing ‘name = “”` is the column-name annotation for log output — see db_sqlite.rb for design notes.



405
406
407
408
# File 'lib/fresco/runtime/db_sqlite.rb', line 405

def self.bind_str(cid = 0, idx = 0, value = "", name = "")
  record_bind_str(idx, value, name) if log_sql?
  Sqlite.fresco_sqlite_bind_str(cid, idx, value)
end

.boot!Object

Eager-open the connection. Called once from Fresco::App#run at startup. Same rationale as the SQLite-template version —surface a bad DSN at process start instead of inside the first request.



450
451
452
# File 'lib/fresco/runtime/db_sqlite.rb', line 450

def self.boot!
  ensure_connection_dbh!
end

.cached_prepare(sql = "") ⇒ Object



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/fresco/runtime/db_sqlite.rb', line 383

def self.cached_prepare(sql = "")
  if @stmt_cache.key?(sql)
    cid = @stmt_cache[sql]
    Sqlite.fresco_sqlite_reset(cid)
    flush_pending! if @has_pending
    @pending_t0  = Sock.sphttp_elapsed_micros
    @has_pending = true
    if log_sql?
      @pending_sql   = sql
      @pending_label = Color.dim("  [db cached] ")
      @pending_binds = ""
    end
    return cid
  end
  cid = prepare(sql)
  @stmt_cache[sql] = cid if cid > 0
  cid
end

.col_int(cid = 0, idx = 0) ⇒ Object



436
# File 'lib/fresco/runtime/db_sqlite.rb', line 436

def self.col_int(cid = 0, idx = 0); Sqlite.fresco_sqlite_col_int(cid, idx); end

.col_str(cid = 0, idx = 0) ⇒ Object



435
# File 'lib/fresco/runtime/db_sqlite.rb', line 435

def self.col_str(cid = 0, idx = 0); Sqlite.fresco_sqlite_col_str(cid, idx); end

.commit_txObject



485
486
487
488
489
490
491
492
493
# File 'lib/fresco/runtime/db_sqlite.rb', line 485

def self.commit_tx
  if @tx_rollback_requested
    exec("ROLLBACK")
    @tx_rollback_requested = false
    return false
  end
  exec("COMMIT")
  true
end

.emit(line = "") ⇒ Object

Single-line emit. Routes through the buffer when we’re inside a request (so the line lands under the [request] log), or straight to stdout for migrations / one-off CLI work where there’s no request to nest under.



311
312
313
314
315
316
317
# File 'lib/fresco/runtime/db_sqlite.rb', line 311

def self.emit(line = "")
  if @in_request
    @log_buffer.push(line)
  else
    puts line
  end
end

.ensure_connection_dbh!Object



184
185
186
187
188
189
190
# File 'lib/fresco/runtime/db_sqlite.rb', line 184

def self.ensure_connection_dbh!
  if @conn_dbh < 0
    h = Sqlite.fresco_sqlite_open(Fresco.database_path)
    @conn_dbh = h if h > 0
  end
  @conn_dbh
end

.exec(sql = "") ⇒ Object

Adapter-agnostic surface for the model codegen. See db_sqlite.rb for design notes — same method names, same return conventions; only the underlying FFI delegate differs.



345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/fresco/runtime/db_sqlite.rb', line 345

def self.exec(sql = "")
  h = ensure_connection_dbh!
  return false if h < 0
  flush_pending! if @has_pending
  t0 = Sock.sphttp_elapsed_micros
  result = Sqlite.fresco_sqlite_exec(h, sql) == 0
  dt = Sock.sphttp_elapsed_micros - t0
  Fresco.add_sql_micros!(dt)
  if log_sql?
    emit(Color.dim("  [db] (" + fmt_micros(dt) + ") ") + color_sql(sql))
  end
  result
end

.finalize(cid = 0) ⇒ Object



437
# File 'lib/fresco/runtime/db_sqlite.rb', line 437

def self.finalize(cid = 0);         Sqlite.fresco_sqlite_finalize(cid);     end

.flush_log!Object

See db_sqlite.rb for the in-place-clear rationale — repeated ‘@log_buffer = [“”]` reassignment crashes under Spinel after a few hundred requests (orphaned-array bus error).



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/fresco/runtime/db_sqlite.rb', line 266

def self.flush_log!
  flush_pending!
  i = 0
  while i < @log_buffer.length
    puts @log_buffer[i]
    i += 1
  end
  while @log_buffer.length > 0
    @log_buffer.delete_at(@log_buffer.length - 1)
  end
  @in_request = false
end

.flush_pending!Object

Emit the current pending entry (if any) and clear it. Two jobs:

1. Compute the [prepare → now] duration and fold it into the
   per-request totals. Runs even when FRESCO_LOG_SQL is off so
   the request-line roll-up stays accurate.
2. When FRESCO_LOG_SQL is on, emit the formatted log line with
   that duration, the column-binding annotation, and the
   [db] / [db cached] marker.

Used as a boundary between consecutive prepares (so a prepare- without-step still gets timed and surfaced) and as the bind→step hand-off (so step’s emission picks up the accumulated binds).



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/fresco/runtime/db_sqlite.rb', line 289

def self.flush_pending!
  return unless @has_pending
  dt = Sock.sphttp_elapsed_micros - @pending_t0
  Fresco.add_sql_micros!(dt)
  if @pending_sql != ""
    line = @pending_label + Color.dim("(" + fmt_micros(dt) + ") ") + color_sql(@pending_sql)
    if @pending_binds != ""
      line += Color.dim("  [" + @pending_binds + "]")
    end
    emit(line)
  end
  @has_pending   = false
  @pending_t0    = 0
  @pending_sql   = ""
  @pending_label = ""
  @pending_binds = ""
end

.last_rowidObject



440
441
442
443
444
# File 'lib/fresco/runtime/db_sqlite.rb', line 440

def self.last_rowid
  h = ensure_connection_dbh!
  return -1 if h < 0
  Sqlite.fresco_sqlite_last_insert_rowid(h)
end

.log_sql?Boolean

SQL query logging. See db_sqlite.rb for full design notes — same FRESCO_LOG_SQL env gate, same in-request buffer, same ‘[db]` / `[db cached]` line format, same verb-driven color palette via `color_sql` in runtime.rb, same per-bind value capture for the trailing `[$1=…, $2=…]` annotation.

Returns:

  • (Boolean)


215
216
217
# File 'lib/fresco/runtime/db_sqlite.rb', line 215

def self.log_sql?
  ENV.fetch("FRESCO_LOG_SQL", "") != ""
end

.new_instanceObject

Adapter-agnostic factory. Returns a fresh unopened Postgres (or SQLite, in the other template) instance. Action code that wants to be DB-agnostic calls this; Spinel’s analyzer sees the concrete return type per-build and dispatches correctly.



170
171
172
# File 'lib/fresco/runtime/db_sqlite.rb', line 170

def self.new_instance
  SQLite.new
end

.prepare(sql = "") ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/fresco/runtime/db_sqlite.rb', line 359

def self.prepare(sql = "")
  h = ensure_connection_dbh!
  return -1 if h < 0
  flush_pending! if @has_pending
  @pending_t0  = Sock.sphttp_elapsed_micros
  @has_pending = true
  if log_sql?
    @pending_sql   = sql
    @pending_label = Color.dim("  [db] ")
    @pending_binds = ""
  end
  Sqlite.fresco_sqlite_prepare(h, sql)
end

.record_bind_int(idx = 0, value = 0, name = "") ⇒ Object

Record one bind on the current pending entry. When ‘name` is non-empty, use it as the label (`email=“bob”`); when empty, fall back to the placeholder index (`$1=“bob”`). Model codegen passes the column name through; ad-hoc `Db.exec` / `Model.query` / hand- written SQL paths call without a name and get the `$N` form. Strings get double-quoted so a numeric-looking string (“42”) isn’t ambiguous with the int 42 when reading logs.



326
327
328
329
330
331
# File 'lib/fresco/runtime/db_sqlite.rb', line 326

def self.record_bind_int(idx = 0, value = 0, name = "")
  return if @pending_sql == ""
  sep   = @pending_binds == "" ? "" : ", "
  label = name == "" ? "$" + idx.to_s : name
  @pending_binds = @pending_binds + sep + label + "=" + value.to_s
end

.record_bind_str(idx = 0, value = "", name = "") ⇒ Object



333
334
335
336
337
338
# File 'lib/fresco/runtime/db_sqlite.rb', line 333

def self.record_bind_str(idx = 0, value = "", name = "")
  return if @pending_sql == ""
  sep   = @pending_binds == "" ? "" : ", "
  label = name == "" ? "$" + idx.to_s : name
  @pending_binds = @pending_binds + sep + label + "=\"" + value + "\""
end

.reset(cid = 0) ⇒ Object



438
# File 'lib/fresco/runtime/db_sqlite.rb', line 438

def self.reset(cid = 0);            Sqlite.fresco_sqlite_reset(cid);        end

.rollback!Object



462
463
464
# File 'lib/fresco/runtime/db_sqlite.rb', line 462

def self.rollback!
  @tx_rollback_requested = true
end

.step(cid = 0) ⇒ Object

See db_sqlite.rb for design notes on the two timing paths.



423
424
425
426
427
428
429
430
431
432
433
# File 'lib/fresco/runtime/db_sqlite.rb', line 423

def self.step(cid = 0)
  if @has_pending
    result = Sqlite.fresco_sqlite_step(cid)
    flush_pending!
    return result
  end
  t0 = Sock.sphttp_elapsed_micros
  result = Sqlite.fresco_sqlite_step(cid)
  Fresco.add_sql_micros!(Sock.sphttp_elapsed_micros - t0)
  result
end