Module: SmartBrain::DB

Defined in:
lib/smart_brain/db.rb

Overview

SmartBrain::DB owns the Sequel/Postgres connection and schema migration.

Backed by config/brain.yml storage.database (overridable via SMARTBRAIN_DB_HOST,PORT,NAME,USER,PASSWORD env). Connections are memoized per connection signature so the runtime, server, and CLI share one pool.

Constant Summary collapse

SCHEMA_SQL_PATH =
File.expand_path('../../db/migrate/001_init.sql', __dir__)
MIGRATIONS_DIR =
File.expand_path('../../db/migrate', __dir__)
DEFAULT_ADAPTER =
'postgres'

Class Method Summary collapse

Class Method Details

.connect(config) ⇒ Object

Returns a memoized Sequel::Database for the given config (or config hash).



20
21
22
23
24
25
26
27
28
29
# File 'lib/smart_brain/db.rb', line 20

def connect(config)
  opts = connection_opts(config)
  signature = opts.hash
  return pools[signature] if pools.key?(signature)

  db = Sequel.connect(opts)
  db.sql_log_level = :error if db.respond_to?(:sql_log_level=)
  db.extension(:pg_json) rescue nil
  pools[signature] = db
end

.disconnect!Object

Drop all memoized connections (used by tests / reconfigure).



55
56
57
58
# File 'lib/smart_brain/db.rb', line 55

def disconnect!
  pools.each_value(&:disconnect)
  pools.clear
end

.migrate(db) ⇒ Object

Run all bundled migration SQL files in order (each is idempotent: CREATE TABLE/INDEX IF NOT EXISTS, ADD COLUMN IF NOT EXISTS).



33
34
35
36
37
38
# File 'lib/smart_brain/db.rb', line 33

def migrate(db)
  Dir.glob(File.join(MIGRATIONS_DIR, '*.sql')).sort.each do |path|
    db.run(File.read(path))
  end
  true
end

.reachable?(config) ⇒ Boolean

True if a Postgres connection can be established for the given config.

Returns:

  • (Boolean)


41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/smart_brain/db.rb', line 41

def reachable?(config)
  db = Sequel.connect(connection_opts(config))
  begin
    db.test_connection
  rescue StandardError
    false
  else
    true
  ensure
    db.disconnect if db
  end
end

.stringify_keys(obj) ⇒ Object

Stringify keys for storage into a JSONB column (Sequel pg_json stores string keys; symbol keys round-trip fine but strings are canonical).



83
84
85
86
87
# File 'lib/smart_brain/db.rb', line 83

def stringify_keys(obj)
  return obj unless obj.is_a?(Hash)

  obj.transform_keys(&:to_s)
end

.symbolize_jsonb(obj) ⇒ Object

Recursively symbolize a JSONB value read from Postgres. Robust whether the adapter returns a parsed Hash/Array, a Sequel JSONBHash/JSONBArray wrapper (NOT a Hash subclass in recent Sequel), or a raw JSON String.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/smart_brain/db.rb', line 63

def symbolize_jsonb(obj)
  case obj
  when String
    begin
      parsed = JSON.parse(obj)
      parsed.is_a?(Hash) || parsed.is_a?(Array) ? symbolize_jsonb(parsed) : obj
    rescue JSON::ParserError, TypeError
      obj
    end
  when ->(o) { o.respond_to?(:to_hash) }
    obj.to_hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = symbolize_jsonb(v) }
  when ->(o) { !o.nil? && !o.is_a?(String) && o.respond_to?(:to_a) }
    obj.to_a.map { |v| symbolize_jsonb(v) }
  else
    obj
  end
end