Class: ActiveRecord::ConnectionAdapters::LibsqlAdapter

Inherits:
AbstractAdapter
  • Object
show all
Extended by:
Quoting::ClassMethods
Includes:
Quoting
Defined in:
lib/active_record/connection_adapters/libsql_adapter.rb

Defined Under Namespace

Modules: Quoting

Constant Summary collapse

ADAPTER_NAME =
"Libsql"
NATIVE_DATABASE_TYPES =

— Native database types —

{
  primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
  string: { name: "TEXT" },
  text: { name: "TEXT" },
  integer: { name: "INTEGER" },
  float: { name: "REAL" },
  decimal: { name: "REAL" },
  datetime: { name: "DATETIME" },
  timestamp: { name: "DATETIME" },
  time: { name: "TIME" },
  date: { name: "DATE" },
  binary: { name: "BLOB" },
  boolean: { name: "BOOLEAN" },
  json: { name: "TEXT" }
}.freeze
TYPE_MAP =
Type::TypeMap.new.tap { |m| initialize_type_map(m) }
EXTENDED_TYPE_MAPS =
Concurrent::Map.new

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Quoting::ClassMethods

quote_column_name

Constructor Details

#initializeLibsqlAdapter

— Connection lifecycle —



109
110
111
112
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 109

def initialize(...)
  super
  @raw_database = nil
end

Class Method Details

.quote_table_name(name) ⇒ Object



77
78
79
80
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 77

def self.quote_table_name(name)
  @quoted_table_names ||= {}
  @quoted_table_names[name] ||= %("#{name.to_s.gsub('"', '""').gsub('.', '"."')}").freeze
end

Instance Method Details

#active?Boolean

Returns:



118
119
120
121
122
123
124
125
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 118

def active?
  return false unless @raw_connection

  @raw_connection.query("SELECT 1")
  true
rescue ::Libsql::Error, ::Libsql::ClosedError
  false
end

#affected_rows(result) ⇒ Object



187
188
189
190
191
192
193
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 187

def affected_rows(result)
  if result.respond_to?(:affected_rows)
    result.affected_rows
  else
    @last_affected_rows
  end
end

#begin_db_transactionObject

— Transactions —



201
202
203
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 201

def begin_db_transaction
  @raw_connection.execute("BEGIN")
end

#cast_result(result) ⇒ Object



183
184
185
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 183

def cast_result(result)
  result
end

#column_definitions(table_name) ⇒ Object



243
244
245
246
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 243

def column_definitions(table_name)
  result = @raw_connection.query("PRAGMA table_info(#{quote_table_name(table_name)})")
  result.to_a
end

#commit_db_transactionObject



205
206
207
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 205

def commit_db_transaction
  @raw_connection.execute("COMMIT")
end

#connectObject



114
115
116
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 114

def connect
  @raw_database, @raw_connection = build_libsql_connection
end

#connected?Boolean

Returns:



127
128
129
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 127

def connected?
  !@raw_connection.nil?
end

#create_savepoint(name = current_savepoint_name) ⇒ Object



213
214
215
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 213

def create_savepoint(name = current_savepoint_name)
  @raw_connection.execute("SAVEPOINT #{quote_column_name(name)}")
end

#discard!Object

Clean up Rust runtime after fork (Puma / Unicorn safety).



140
141
142
143
144
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 140

def discard!
  @raw_database&.discard!
  @raw_connection = nil
  @raw_database = nil
end

#disconnect!Object



131
132
133
134
135
136
137
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 131

def disconnect!
  super
  @raw_connection&.close
  @raw_database&.close
  @raw_connection = nil
  @raw_database = nil
end

#exec_rollback_db_transactionObject



209
210
211
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 209

def exec_rollback_db_transaction
  @raw_connection.execute("ROLLBACK")
end

#exec_rollback_to_savepoint(name = current_savepoint_name) ⇒ Object



221
222
223
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 221

def exec_rollback_to_savepoint(name = current_savepoint_name)
  @raw_connection.execute("ROLLBACK TO SAVEPOINT #{quote_column_name(name)}")
end

#indexes(table_name) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 273

def indexes(table_name)
  index_list = @raw_connection.query("PRAGMA index_list(#{quote_table_name(table_name)})").to_a

  index_list.filter_map do |idx|
    next if idx["name"].start_with?("sqlite_")

    columns = @raw_connection.query("PRAGMA index_info(#{quote_table_name(idx['name'])})").to_a
    column_names = columns.sort_by { |c| c["seqno"].to_i }.map { |c| c["name"] }

    IndexDefinition.new(
      table_name,
      idx["name"],
      idx["unique"].to_i != 0,
      column_names
    )
  end
end

#last_inserted_id(_result) ⇒ Object



195
196
197
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 195

def last_inserted_id(_result)
  @last_inserted_id
end

#native_database_typesObject



103
104
105
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 103

def native_database_types
  NATIVE_DATABASE_TYPES
end

#new_column_from_field(_table_name, field, _definitions) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 248

def new_column_from_field(_table_name, field, _definitions)
  default_value = field["dflt_value"]

  # Unquote string defaults
  default_value = default_value[1..-2] if default_value&.start_with?("'") && default_value.end_with?("'")

  type = field["type"]
   = (type)
  cast_type = lookup_cast_type(type)
  null = field["notnull"].to_i.zero?
  default_function = nil

  COLUMN_BUILDER.call(
    field["name"], cast_type, default_value, , null, default_function
  )
end

#perform_query(raw_connection, sql, _binds, type_casted_binds, prepare:, notification_payload:, batch: false) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 159

def perform_query(raw_connection, sql, _binds, type_casted_binds,
                  prepare:, notification_payload:, batch: false)
  sanitized = sanitize_for_update(sql)
  params = type_casted_binds || []

  result =
    if batch
      raw_connection.execute_batch(sanitized)
      build_empty_result(affected: 0)
    elsif write_query?(sanitized)
      affected = raw_connection.execute(sanitized, params)
      @last_inserted_id = raw_connection.last_insert_rowid
      build_empty_result(affected: affected)
    else
      rows_obj = raw_connection.query(sanitized, params)
      build_read_result(rows_obj, raw_connection)
    end

  notification_payload[:row_count] = result&.length || 0
  notification_payload[:affected_rows] = affected_rows(result) if AR_8_1_OR_LATER

  result
end

#primary_keys(table_name) ⇒ Object



265
266
267
268
269
270
271
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 265

def primary_keys(table_name)
  result = @raw_connection.query("PRAGMA table_info(#{quote_table_name(table_name)})")
  result.to_a
        .select { |row| row["pk"].to_i.positive? }
        .sort_by { |row| row["pk"].to_i }
        .map { |row| row["name"] }
end

#quote_table_name(name) ⇒ Object



82
83
84
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 82

def quote_table_name(name)
  self.class.quote_table_name(name)
end

#quoted_falseObject



87
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 87

def quoted_false = "0"

#quoted_trueObject



86
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 86

def quoted_true = "1"

#release_savepoint(name = current_savepoint_name) ⇒ Object



217
218
219
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 217

def release_savepoint(name = current_savepoint_name)
  @raw_connection.execute("RELEASE SAVEPOINT #{quote_column_name(name)}")
end

#rename_column(table_name, column_name, new_column_name) ⇒ Object



297
298
299
300
301
302
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 297

def rename_column(table_name, column_name, new_column_name, **)
  execute(
    "ALTER TABLE #{quote_table_name(table_name)} " \
    "RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
  )
end

#rename_table(table_name, new_name) ⇒ Object

— Schema DDL —



293
294
295
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 293

def rename_table(table_name, new_name, **)
  execute("ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}")
end

#supports_ddl_transactions?Boolean

Returns:



98
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 98

def supports_ddl_transactions? = false

#supports_explain?Boolean

Returns:



99
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 99

def supports_explain? = false

#supports_foreign_keys?Boolean

Returns:



96
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 96

def supports_foreign_keys? = true

#supports_insert_returning?Boolean

Returns:



101
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 101

def supports_insert_returning? = false

#supports_json?Boolean

Returns:



97
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 97

def supports_json? = true

#supports_lazy_transactions?Boolean

Returns:



100
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 100

def supports_lazy_transactions? = false

#supports_migrations?Boolean

— Feature flags —

Returns:



93
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 93

def supports_migrations? = true

#supports_primary_key?Boolean

Returns:



94
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 94

def supports_primary_key? = true

#supports_savepoints?Boolean

Returns:



95
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 95

def supports_savepoints? = true

#syncObject

Sync embedded replica with remote.



147
148
149
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 147

def sync
  @raw_database&.sync
end

#table_exists?(table_name) ⇒ Boolean

Returns:



233
234
235
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 233

def table_exists?(table_name)
  tables.include?(table_name.to_s)
end

#tablesObject

— Schema introspection —



227
228
229
230
231
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 227

def tables
  query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
  result = @raw_connection.query(query)
  result.to_a.map { |row| row["name"] }
end

#translate_exception(exception, message:, sql:, binds:) ⇒ Object

— Exception translation —



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 306

def translate_exception(exception, message:, sql:, binds:)
  # Libsql::Error inherits from RuntimeError, so the default
  # translate_exception would return it as-is. We must handle
  # it explicitly to convert to AR exception classes.
  msg = exception.message
  if /NOT NULL constraint failed/i.match?(msg)
    NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
  elsif /UNIQUE constraint failed/i.match?(msg)
    RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
  elsif /FOREIGN KEY constraint failed/i.match?(msg)
    InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
  elsif exception.is_a?(::Libsql::Error)
    StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
  else
    super
  end
end

#unquoted_falseObject



89
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 89

def unquoted_false = 0

#unquoted_trueObject



88
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 88

def unquoted_true = 1

#viewsObject



237
238
239
240
241
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 237

def views
  query = "SELECT name FROM sqlite_master WHERE type='view' AND name NOT LIKE 'sqlite_%' ORDER BY name"
  result = @raw_connection.query(query)
  result.to_a.map { |row| row["name"] }
end

#write_query?(sql) ⇒ Boolean

— Query execution pipeline —

Returns:



153
154
155
156
157
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 153

def write_query?(sql)
  !READ_QUERY.match?(sql)
rescue ArgumentError
  !READ_QUERY.match?(sql.b)
end