Class: Jimmu::Database

Inherits:
Object
  • Object
show all
Defined in:
lib/jimmu.rb

Overview

A small SQLite wrapper exposing the query / exec / transaction API described in the Jimmu spec. A single Database instance may be shared across the many request-handling threads the server spawns; all access to the underlying sqlite3 connection handle is serialized through a re-entrant Monitor so that (a) concurrent requests never touch the C-level handle at the same time and (b) a block passed to #transaction can itself call #query / #exec on the same thread without deadlocking.

Constant Summary collapse

SQLITE_OK =
0
SQLITE_ROW =
100
SQLITE_DONE =
101
SQLITE_INTEGER =
1
SQLITE_FLOAT =
2
SQLITE_TEXT =
3
SQLITE_BLOB =
4
SQLITE_NULL =
5
SQLITE_TRANSIENT =
Fiddle::Pointer.new(-1)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Database

Returns a new instance of Database.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/jimmu.rb', line 208

def initialize(path)
  unless SQLite3FFI::LOADED
    raise DatabaseError,
          "Could not locate the SQLite3 shared library on this system " \
          "(looked for: #{SQLite3FFI::LIB_CANDIDATES.join(', ')}). " \
          "Install SQLite3 (e.g. `apt install libsqlite3-0`, `brew install sqlite3`); " \
          "it ships with macOS by default, and Windows needs sqlite3.dll on the PATH."
  end

  @path = path
  dir = File.dirname(path)
  FileUtils.mkdir_p(dir) unless dir == '.' || Dir.exist?(dir)

  @lock = Monitor.new
  dbptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
  rc = SQLite3FFI.sqlite3_open(path.to_s, dbptr)
  @db = dbptr.ptr
  if rc != SQLITE_OK
    raise DatabaseError, "Could not open database at #{path.inspect} (sqlite3_open returned #{rc})"
  end
  SQLite3FFI.sqlite3_busy_timeout(@db, 5000)
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



206
207
208
# File 'lib/jimmu.rb', line 206

def path
  @path
end

Instance Method Details

#closeObject



279
280
281
282
283
284
285
# File 'lib/jimmu.rb', line 279

def close
  @lock.synchronize do
    next if @closed
    SQLite3FFI.sqlite3_close(@db)
    @closed = true
  end
end

#exec(sql, *binds) ⇒ Object

Run an INSERT / UPDATE / DELETE / DDL statement. Returns the number of rows changed by the statement (via sqlite3_changes).

db.exec("INSERT INTO users(name) VALUES (?)", "Yamato")


244
245
246
# File 'lib/jimmu.rb', line 244

def exec(sql, *binds)
  @lock.synchronize { run_exec(sql, binds) }
end

#last_insert_rowidObject

The rowid of the most recent successful INSERT on this connection.



275
276
277
# File 'lib/jimmu.rb', line 275

def last_insert_rowid
  @lock.synchronize { SQLite3FFI.sqlite3_last_insert_rowid(@db) }
end

#query(sql, *binds) ⇒ Object

Run a SELECT (or any statement that returns rows) and return an Array of Hashes, keyed by column name (as Symbols).

db.query("SELECT * FROM users")
db.query("SELECT * FROM users WHERE id = ?", id)


236
237
238
# File 'lib/jimmu.rb', line 236

def query(sql, *binds)
  @lock.synchronize { run_query(sql, binds) }
end

#transactionObject

Wrap a block in BEGIN / COMMIT. If the block raises, the transaction is rolled back and the error re-raised.

db.transaction do
db.exec(...)
db.exec(...)
end


255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/jimmu.rb', line 255

def transaction
  @lock.synchronize do
    run_exec('BEGIN', [])
    begin
      result = yield
      run_exec('COMMIT', [])
      result
    rescue Exception => e
      begin
        run_exec('ROLLBACK', [])
      rescue Exception
        nil # the original error is more useful than a rollback failure
      end
      raise e
    end
  end
end