Class: Tina4::Drivers::SqliteDriver

Inherits:
Object
  • Object
show all
Includes:
SchemaSplit
Defined in:
lib/tina4/drivers/sqlite_driver.rb

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SchemaSplit

#split_schema

Class Attribute Details

.write_lockObject (readonly)

Returns the value of attribute write_lock.



20
21
22
# File 'lib/tina4/drivers/sqlite_driver.rb', line 20

def write_lock
  @write_lock
end

Instance Attribute Details

#connectionObject (readonly)

Returns the value of attribute connection.



9
10
11
# File 'lib/tina4/drivers/sqlite_driver.rb', line 9

def connection
  @connection
end

Class Method Details

.resolve_path(connection_string) ⇒ Object

Resolve a SQLite URL / path against the project root (cwd).

Convention (matches tina4-python, tina4-php, tina4-nodejs):

sqlite::memory:              → :memory:
sqlite:///:memory:           → :memory:
sqlite:///app.db             → {cwd}/app.db  (relative)
sqlite:///data/app.db        → {cwd}/data/app.db  (relative; auto-mkdir under cwd)
sqlite:////var/data/app.db   → /var/data/app.db  (absolute; no auto-mkdir)
sqlite:///C:/Users/app.db    → C:/Users/app.db  (Windows absolute)

Never mkdir outside cwd — that was the root cause of the “Read-only file system: ‘/data’” crash on macOS.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/tina4/drivers/sqlite_driver.rb', line 45

def self.resolve_path(connection_string)
  return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:"

  # Strip the scheme + up to three slashes, preserving a potential fourth
  # slash (absolute) or drive letter.
  raw = connection_string.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "")
  return ":memory:" if raw == ":memory:"

  is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/)
  is_unix_abs    = raw.start_with?("/")

  if is_windows_abs || is_unix_abs
    # Absolute — trust the user; don't auto-mkdir outside cwd.
    raw
  else
    # Relative — resolve under cwd; auto-mkdir parent dir.
    resolved = File.join(Dir.pwd, raw)
    parent = File.dirname(resolved)
    require "fileutils"
    FileUtils.mkdir_p(parent) unless File.directory?(parent)
    resolved
  end
end

Instance Method Details

#apply_limit(sql, limit, offset = 0) ⇒ Object



112
113
114
# File 'lib/tina4/drivers/sqlite_driver.rb', line 112

def apply_limit(sql, limit, offset = 0)
  "#{sql} LIMIT #{limit} OFFSET #{offset}"
end

#begin_transactionObject



116
117
118
# File 'lib/tina4/drivers/sqlite_driver.rb', line 116

def begin_transaction
  @connection.execute("BEGIN TRANSACTION")
end

#closeObject



69
70
71
# File 'lib/tina4/drivers/sqlite_driver.rb', line 69

def close
  @connection&.close
end

#coerce_params(params) ⇒ Object

Coerce Ruby values to types the sqlite3 gem can bind. The gem RAISES (“can’t prepare TrueClass”) on a raw boolean, so map true/false to 1/0 —SQLite stores booleans as INTEGER 0/1. Time/DateTime serialise to ISO-8601 so a datetime field round-trips. Parity with the Python/PHP/Node adapters, which coerce booleans at the same bind boundary.



87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/tina4/drivers/sqlite_driver.rb', line 87

def coerce_params(params)
  return params unless params.is_a?(Array)

  params.map do |value|
    case value
    when true then 1
    when false then 0
    when Time, DateTime then value.respond_to?(:iso8601) ? value.iso8601 : value.to_s
    else value
    end
  end
end

#columns(table_name) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/tina4/drivers/sqlite_driver.rb', line 154

def columns(table_name)
  # v3.13.14 (#48): PRAGMA accepts an attached-schema prefix.
  schema, tbl = split_schema(table_name)
  pragma = schema && identifier?(schema) && identifier?(tbl) ? "#{schema}.table_info(#{tbl})" : "table_info(#{table_name})"
  rows = execute_query("PRAGMA #{pragma}")
  rows.map do |r|
    {
      name: r[:name],
      type: r[:type],
      nullable: r[:notnull] == 0,
      default: r[:dflt_value],
      primary_key: r[:pk] == 1
    }
  end
end

#commitObject

Committing/rolling back when no transaction is open is a harmless no-op, NOT a failure — SQLite raises “cannot commit - no transaction is active” in that case. Swallow ONLY that specific condition so a stray commit (e.g. after an autocommit standalone write) doesn’t poison the Database-level @last_error. A genuine commit/rollback failure (disk I/O, constraint deferral, locked DB) still propagates so Database#commit can FAIL LOUD per the DB-contract.



127
128
129
130
131
# File 'lib/tina4/drivers/sqlite_driver.rb', line 127

def commit
  @connection.execute("COMMIT")
rescue SQLite3::SQLException => e
  raise unless e.message.to_s.downcase.include?("no transaction is active")
end

#connect(connection_string, username: nil, password: nil) ⇒ Object



23
24
25
26
27
28
29
30
31
# File 'lib/tina4/drivers/sqlite_driver.rb', line 23

def connect(connection_string, username: nil, password: nil)
  require "sqlite3"
  db_path = self.class.resolve_path(connection_string)

  @connection = SQLite3::Database.new(db_path)
  @connection.results_as_hash = true
  @connection.execute("PRAGMA journal_mode=WAL")
  @connection.execute("PRAGMA foreign_keys=ON")
end

#execute(sql, params = []) ⇒ Object



78
79
80
# File 'lib/tina4/drivers/sqlite_driver.rb', line 78

def execute(sql, params = [])
  @connection.execute(sql, coerce_params(params))
end

#execute_query(sql, params = []) ⇒ Object



73
74
75
76
# File 'lib/tina4/drivers/sqlite_driver.rb', line 73

def execute_query(sql, params = [])
  results = @connection.execute(sql, coerce_params(params))
  results.map { |row| symbolize_keys(row) }
end

#last_insert_idObject



100
101
102
# File 'lib/tina4/drivers/sqlite_driver.rb', line 100

def last_insert_id
  @connection.last_insert_row_id
end

#placeholderObject



104
105
106
# File 'lib/tina4/drivers/sqlite_driver.rb', line 104

def placeholder
  "?"
end

#placeholders(count) ⇒ Object



108
109
110
# File 'lib/tina4/drivers/sqlite_driver.rb', line 108

def placeholders(count)
  (["?"] * count).join(", ")
end

#rollbackObject



133
134
135
136
137
# File 'lib/tina4/drivers/sqlite_driver.rb', line 133

def rollback
  @connection.execute("ROLLBACK")
rescue SQLite3::SQLException => e
  raise unless e.message.to_s.downcase.include?("no transaction is active")
end

#table_exists?(name) ⇒ Boolean

v3.13.14 (#48): a SQLite “schema” is an ATTACH alias (“extra.widget”). Query that database’s own sqlite_master when the prefix is a plain identifier; otherwise treat the whole string as a bare table name.

Returns:

  • (Boolean)


142
143
144
145
146
147
# File 'lib/tina4/drivers/sqlite_driver.rb', line 142

def table_exists?(name)
  schema, tbl = split_schema(name)
  master = schema && identifier?(schema) ? "#{schema}.sqlite_master" : "sqlite_master"
  rows = execute_query("SELECT 1 FROM #{master} WHERE type='table' AND name=?", [tbl])
  !rows.empty?
end

#tablesObject



149
150
151
152
# File 'lib/tina4/drivers/sqlite_driver.rb', line 149

def tables
  rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
  rows.map { |r| r[:name] }
end