Class: Tina4::Drivers::SqliteDriver

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SchemaSplit

#split_schema

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.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/tina4/drivers/sqlite_driver.rb', line 33

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



82
83
84
# File 'lib/tina4/drivers/sqlite_driver.rb', line 82

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

#begin_transactionObject



86
87
88
# File 'lib/tina4/drivers/sqlite_driver.rb', line 86

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

#closeObject



57
58
59
# File 'lib/tina4/drivers/sqlite_driver.rb', line 57

def close
  @connection&.close
end

#columns(table_name) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/tina4/drivers/sqlite_driver.rb', line 113

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



90
91
92
# File 'lib/tina4/drivers/sqlite_driver.rb', line 90

def commit
  @connection.execute("COMMIT")
end

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



11
12
13
14
15
16
17
18
19
# File 'lib/tina4/drivers/sqlite_driver.rb', line 11

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



66
67
68
# File 'lib/tina4/drivers/sqlite_driver.rb', line 66

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

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



61
62
63
64
# File 'lib/tina4/drivers/sqlite_driver.rb', line 61

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

#last_insert_idObject



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

def last_insert_id
  @connection.last_insert_row_id
end

#placeholderObject



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

def placeholder
  "?"
end

#placeholders(count) ⇒ Object



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

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

#rollbackObject



94
95
96
# File 'lib/tina4/drivers/sqlite_driver.rb', line 94

def rollback
  @connection.execute("ROLLBACK")
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)


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

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



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

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