Class: ActiveRecord::ConnectionAdapters::LibsqlAdapter

Inherits:
AbstractAdapter
  • Object
show all
Defined in:
lib/active_record/connection_adapters/libsql_adapter.rb

Constant Summary collapse

ADAPTER_NAME =
'Turso'
QUOTED_COLUMN_NAMES =

insert_all! / upsert_all は self.class.quote_column_name(クラスメソッド)を呼ぶ。AbstractAdapter の ClassMethods はこれを NotImplementedError にするため、クラスメソッドとして上書きする。

Concurrent::Map.new
QUOTED_TABLE_NAMES =

:nodoc:

Concurrent::Map.new
NATIVE_DATABASE_TYPES =

SQLite 互換の型マッピング(libSQL は SQLite 方言)datetime / timestamp は ‘datetime’ を使う。‘TEXT’ にすると PRAGMA table_info が ‘TEXT’ を返し、AR の type_map が Type::Text にマッピングしてしまう。Type::Text は Time を UTC に変換せず文字列化するため、WHERE scheduled_at <= ? の比較が文字列比較で壊れる。

{
  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: 'INTEGER' },
  json: { name: 'TEXT' }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.quote_column_name(name) ⇒ Object



28
29
30
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 28

def quote_column_name(name)
  QUOTED_COLUMN_NAMES[name] ||= %("#{name.to_s.gsub('"', '""')}").freeze
end

.quote_table_name(name) ⇒ Object



32
33
34
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 32

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

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


142
143
144
145
146
147
148
149
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 142

def active?
  return false unless @raw_connection

  @raw_connection.query('SELECT 1')
  true
rescue StandardError
  false
end

#adapter_nameObject


Adapter 識別




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

def adapter_name
  ADAPTER_NAME
end

#affected_rows(_raw_result) ⇒ Object



259
260
261
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 259

def affected_rows(_raw_result)
  @last_affected_rows || 0
end

#begin_db_transactionObject


トランザクション




267
268
269
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 267

def begin_db_transaction
  @raw_connection&.begin_transaction
end

#build_insert_sql(insert) ⇒ Object


insert_all! / upsert_all(ON CONFLICT 構文)SQLite3 adapter と同じ実装




196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 196

def build_insert_sql(insert) # :nodoc:
  sql = +"INSERT #{insert.into} #{insert.values_list}"

  if insert.skip_duplicates?
    sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
  elsif insert.update_duplicates?
    sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
    if insert.raw_update_sql?
      sql << insert.raw_update_sql
    else
      sql << insert.touch_model_timestamps_unless { |column| "#{column} IS excluded.#{column}" }
      sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(',')
    end
  end

  # RETURNING 句は supports_insert_returning? が false(デフォルト)のため生成されない。
  # perform_query は INSERT を write クエリとして扱い結果を捨てるため、
  # RETURNING を有効にする場合は perform_query の read_query? 判定も合わせて修正が必要。
  sql
end

#cast_result(raw_result) ⇒ Object

perform_query が返した結果をそのまま使う(すでに ActiveRecord::Result)



255
256
257
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 255

def cast_result(raw_result)
  raw_result
end

#columns(table_name) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 304

def columns(table_name)
  result = internal_exec_query(
    "PRAGMA table_info(#{quote_table_name(table_name)})",
    'SCHEMA'
  )
  result.map do |row|
    sql_type    = row['type'].to_s
    cast_type   = type_map.lookup(sql_type) || Type::Value.new
    sql_type_md = (sql_type)
    null        = row['notnull'].to_i.zero?
    COLUMN_BUILDER.call(row['name'], cast_type, row['dflt_value'], sql_type_md, null)
  end
end

#commit_db_transactionObject



271
272
273
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 271

def commit_db_transaction
  @raw_connection&.commit_transaction
end

#connect!Object


接続管理(AR 8 スタイル)AR の ConnectionPool はスレッドごとに独立した Adapter インスタンスを払い出すため




137
138
139
140
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 137

def connect!
  @raw_database, @raw_connection = build_libsql_connection
  super
end

#discard!Object

fork 後の子プロセスで呼ばれる。sqlite3 gem の fork safety が接続を強制クローズするため、参照を破棄して子プロセスで新しい接続を確立できるようにする。AR の ConnectionPool が fork 後に各コネクションに対して呼ぶ。



161
162
163
164
165
166
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 161

def discard!
  @raw_connection = nil
  @raw_database = nil
  TursoLibsql.reinitialize_runtime!
  super
end

#disconnect!Object



151
152
153
154
155
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 151

def disconnect!
  @raw_connection = nil
  @raw_database = nil
  super
end

#exec_rollback_db_transactionObject



275
276
277
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 275

def exec_rollback_db_transaction
  @raw_connection&.rollback_transaction
end

#last_inserted_id(_result) ⇒ Object


INSERT 後の id AR 8 は last_inserted_id(result) を呼ぶ




284
285
286
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 284

def last_inserted_id(_result)
  @raw_connection&.last_insert_rowid
end

#native_database_typesObject


スキーマ情報




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

def native_database_types
  NATIVE_DATABASE_TYPES
end

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

AR 8 が with_raw_connection { |conn| } で呼ぶ中核メソッド



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 223

def perform_query(raw_connection, sql, _binds, type_casted_binds, prepare:, notification_payload:, batch: false)
  # バインドパラメータを SQL に展開する(libsql の ? プレースホルダーに対応)
  expanded_sql = if type_casted_binds&.any?
                   i = -1
                   sql.gsub('?') do
                     i += 1
                     quote(type_casted_binds[i])
                   end
                 else
                   sql
                 end

  # libSQL / SQLite は FOR UPDATE / FOR UPDATE SKIP LOCKED / FOR SHARE を
  # サポートしない。AR の SQLite3 adapter はこれらを除去するが、
  # libsql_adapter は直接 Hrana HTTP / SQLite3::Database を呼ぶため自前で除去する。
  expanded_sql = sanitize_for_update(expanded_sql)

  if read_query?(expanded_sql)
    rows = raw_connection.query(expanded_sql)
    notification_payload[:row_count] = rows.size if notification_payload
    build_result(rows)
  else
    affected = raw_connection.execute(expanded_sql)
    @last_affected_rows = affected.to_i
    notification_payload[:row_count] = @last_affected_rows if notification_payload
    ActiveRecord::Result.empty
  end
rescue RuntimeError => e
  raise translate_exception(e, message: e.message, sql: expanded_sql, binds: [])
end

#quote_string(s) ⇒ Object

SQLite / libSQL はバックスラッシュをエスケープ文字として扱わない。AbstractAdapter のデフォルト実装は \ → \\ の変換を行うため、バックスラッシュを含む文字列の保存・取得で値が化ける。SQLite3Adapter と同様に、シングルクォートのエスケープのみ行う。



334
335
336
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 334

def quote_string(s)
  s.gsub("'", "''")
end

#quoted_falseObject



342
343
344
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 342

def quoted_false
  '0'
end

#quoted_trueObject



338
339
340
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 338

def quoted_true
  '1'
end

#supports_ddl_transactions?Boolean

Returns:

  • (Boolean)


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

def supports_ddl_transactions?
  false
end

#supports_explain?Boolean

Returns:

  • (Boolean)


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

def supports_explain?
  false
end

#supports_insert_conflict_target?Boolean

Returns:

  • (Boolean)


119
120
121
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 119

def supports_insert_conflict_target?
  true
end

#supports_insert_on_duplicate_skip?Boolean

insert_all! / upsert_all のサポートlibSQL / SQLite は ON CONFLICT 構文をサポートする

Returns:

  • (Boolean)


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

def supports_insert_on_duplicate_skip?
  true
end

#supports_insert_on_duplicate_update?Boolean

Returns:

  • (Boolean)


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

def supports_insert_on_duplicate_update?
  true
end

#supports_lazy_transactions?Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 105

def supports_lazy_transactions?
  false
end

#supports_migrations?Boolean

Returns:

  • (Boolean)


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

def supports_migrations?
  true
end

#supports_primary_key?Boolean

Returns:

  • (Boolean)


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

def supports_primary_key?
  true
end

#supports_savepoints?Boolean

Returns:

  • (Boolean)


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

def supports_savepoints?
  false
end

#syncObject

Embedded Replica モードでリモートから最新フレームを手動同期する。Remote モードでは何もしない(no-op)。



187
188
189
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 187

def sync
  @raw_database&.sync
end

#table_exists?(table_name) ⇒ Boolean

Returns:

  • (Boolean)


318
319
320
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 318

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

#tables(_name = nil) ⇒ Object



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

def tables(_name = nil)
  result = internal_exec_query(
    "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
    'SCHEMA'
  )
  result.rows.flatten
end

#write_query?(sql) ⇒ Boolean

Returns:

  • (Boolean)


123
124
125
126
127
# File 'lib/active_record/connection_adapters/libsql_adapter.rb', line 123

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