Class: ActiveRecord::ConnectionAdapters::CloudflareD1Adapter

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

Constant Summary collapse

ADAPTER_NAME =
"CloudflareD1"
NATIVE_DATABASE_TYPES =
{
  primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL",
  string: { name: "TEXT" },
  text: { name: "TEXT" },
  integer: { name: "INTEGER" },
  float: { name: "REAL" },
  decimal: { name: "NUMERIC" },
  datetime: { name: "TEXT" },
  time: { name: "TEXT" },
  date: { name: "TEXT" },
  binary: { name: "BLOB" },
  boolean: { name: "INTEGER" },
  json: { name: "TEXT" }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCloudflareD1Adapter

Returns a new instance of CloudflareD1Adapter.

Raises:

  • (ArgumentError)


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 38

def initialize(...)
  super

  @config = @config.symbolize_keys
  @account_id = @config[:account_id] || ENV["CLOUDFLARE_ACCOUNT_ID"]
  @api_token = @config[:api_token] || ENV["CLOUDFLARE_API_TOKEN"]
  @database_id = @config[:database_id] || @config[:database]

  raise ArgumentError, "account_id is required" unless @account_id
  raise ArgumentError, "api_token is required" unless @api_token
  raise ArgumentError, "database_id is required" unless @database_id

  @client = Cloudflare::D1::Client.new(
    account_id: @account_id,
    api_token: @api_token
  )

  @connection_parameters = @config
  @prepared_statements = false
end

Class Method Details

.dbconsole(config, options = {}) ⇒ Object



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

def dbconsole(config, options = {})
  puts "D1 database console not available via adapter"
  puts "Use: wrangler d1 execute #{config[:database_id]} --command='<SQL>'"
end

.new_client(config) ⇒ Object



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

def new_client(config)
  config
end

Instance Method Details

#active?Boolean

Connection management

Returns:

  • (Boolean)


61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 61

def active?
  return false unless @client

  # Try a simple query to check if connection is active
  result = @client.query(
    database_id: @database_id,
    sql: "SELECT 1"
  )
  result["success"] == true
rescue
  false
end

#adapter_nameObject

Adapter info



96
97
98
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 96

def adapter_name
  ADAPTER_NAME
end

#affected_rows(result) ⇒ Object



194
195
196
197
198
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 194

def affected_rows(result)
  meta = result.instance_variable_get(:@meta)
  return 0 unless meta
  meta["changes"] || 0
end

#columns(table_name) ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 267

def columns(table_name)
  sql = "PRAGMA table_info(#{quote_table_name(table_name)})"
  result = execute(sql)

  return [] unless result && result[0] && result[0]["results"]

  result[0]["results"].map do |column_data|
    column_name = column_data["name"]
    sql_type = column_data["type"]
    nullable = column_data["notnull"] == 0
    default = column_data["dflt_value"]
     = (sql_type)
    cast_type = lookup_cast_type(sql_type)

    ActiveRecord::ConnectionAdapters::Column.new(
      column_name,
      cast_type,
      default,
      ,
      nullable
    )
  end
end

#data_sourcesObject



215
216
217
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 215

def data_sources
  tables
end

#discard!Object



86
87
88
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 86

def discard!
  @client = nil
end

#disconnect!Object



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

def disconnect!
  @client = nil
end

#exec_query(sql, name = "SQL", binds = [], prepare: false) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 163

def exec_query(sql, name = "SQL", binds = [], prepare: false)
  log(sql, name, binds) do
    result = @client.query(
      database_id: @database_id,
      sql: sql,
      params: binds.map { |b| type_cast(b) }
    )

    rows = result.dig("result", 0, "results") || []
    columns = rows.first&.keys || []

    ActiveRecord::Result.new(columns, rows.map(&:values), result.dig("result", 0, "meta") || {})
  end
end

#execute(sql, name = nil) ⇒ Object

Query execution



152
153
154
155
156
157
158
159
160
161
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 152

def execute(sql, name = nil)
  log(sql, name) do
    result = @client.query(
      database_id: @database_id,
      sql: sql
    )

    result["result"]
  end
end

#indexes(table_name) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 305

def indexes(table_name)
  sql = "PRAGMA index_list(#{quote_table_name(table_name)})"
  result = execute(sql)

  return [] unless result && result[0] && result[0]["results"]

  result[0]["results"].map do |index_data|
    index_name = index_data["name"]
    unique = index_data["unique"] == 1

    # Get index columns
    columns_sql = "PRAGMA index_info(#{quote(index_name)})"
    columns_result = execute(columns_sql)
    columns = []

    if columns_result && columns_result[0] && columns_result[0]["results"]
      columns = columns_result[0]["results"]
        .sort_by { |c| c["seqno"] }
        .map { |c| c["name"] }
    end

    ActiveRecord::ConnectionAdapters::IndexDefinition.new(
      table_name,
      index_name,
      unique,
      columns
    )
  end
end

#insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 178

def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
  sql = to_sql(arel, binds)

  log(sql, name, binds) do
    result = @client.query(
      database_id: @database_id,
      sql: sql,
      params: binds.map { |b| type_cast(b) }
    )

    # Return the last inserted ID wrapped in array for multi-insert support
    last_id = result.dig("result", 0, "meta", "last_row_id") || id_value
    [last_id]
  end
end

#internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true) ⇒ Object



223
224
225
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 223

def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true)
  exec_query(sql, name, binds, prepare: prepare)
end

#native_database_typesObject



301
302
303
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 301

def native_database_types
  NATIVE_DATABASE_TYPES
end

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



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 227

def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch:)
  result = @client.query(
    database_id: @database_id,
    sql: sql,
    params: type_casted_binds
  )

  rows = result.dig("result", 0, "results") || []
  columns = rows.first&.keys || []
  meta = result.dig("result", 0, "meta") || {}

  ar_result = ActiveRecord::Result.new(columns, rows.map(&:values), meta)

  # Store meta for affected_rows to access
  ar_result.instance_variable_set(:@meta, meta)

  ar_result
end

#primary_key(table_name) ⇒ Object



291
292
293
294
295
296
297
298
299
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 291

def primary_key(table_name)
  sql = "PRAGMA table_info(#{quote_table_name(table_name)})"
  result = execute(sql)

  return "id" unless result && result[0] && result[0]["results"]

  pk_column = result[0]["results"].find { |col| col["pk"] == 1 }
  pk_column&.fetch("name", "id")
end

#quote_column_name(name) ⇒ Object



345
346
347
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 345

def quote_column_name(name)
  "\"#{name.to_s.gsub('"', '""')}\""
end

#quote_string(s) ⇒ Object

Quoting



337
338
339
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 337

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

#quote_table_name(name) ⇒ Object



341
342
343
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 341

def quote_table_name(name)
  "\"#{name.to_s.gsub('"', '""')}\""
end

#quoted_date(value) ⇒ Object



357
358
359
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 357

def quoted_date(value)
  value.utc.iso8601
end

#quoted_falseObject



353
354
355
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 353

def quoted_false
  "0"
end

#quoted_trueObject



349
350
351
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 349

def quoted_true
  "1"
end

#reconnectObject Also known as: reconnect!



74
75
76
77
78
79
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 74

def reconnect
  @client = Cloudflare::D1::Client.new(
    account_id: @account_id,
    api_token: @api_token
  )
end

#reset!Object



90
91
92
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 90

def reset!
  reconnect
end

#select_all(arel, name = nil, binds = [], preparable: nil, async: false, allow_retry: false) ⇒ Object



200
201
202
203
204
205
206
207
208
209
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 200

def select_all(arel, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
  arel = arel_from_relation(arel)
  sql = to_sql(arel, binds)

  if preparable.nil?
    preparable = prepared_statements
  end

  exec_query(sql, name, binds, prepare: preparable)
end

#supports_common_table_expressions?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 128

def supports_common_table_expressions?
  true
end

#supports_concurrent_connections?Boolean

Returns:

  • (Boolean)


146
147
148
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 146

def supports_concurrent_connections?
  true
end

#supports_datetime_with_precision?Boolean

Returns:

  • (Boolean)


120
121
122
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 120

def supports_datetime_with_precision?
  false
end

#supports_ddl_transactions?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 100

def supports_ddl_transactions?
  false # D1 API doesn't support transactions across HTTP requests
end

#supports_explain?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 112

def supports_explain?
  true
end

#supports_index_sort_order?Boolean

Returns:

  • (Boolean)


116
117
118
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 116

def supports_index_sort_order?
  true
end

#supports_insert_on_conflict?Boolean Also known as: supports_insert_on_duplicate_skip?, supports_insert_on_duplicate_update?

Returns:

  • (Boolean)


140
141
142
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 140

def supports_insert_on_conflict?
  true
end

#supports_insert_returning?Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 136

def supports_insert_returning?
  false
end

#supports_json?Boolean

Returns:

  • (Boolean)


124
125
126
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 124

def supports_json?
  true
end

#supports_savepoints?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 104

def supports_savepoints?
  false
end

#supports_transaction_isolation?Boolean

Returns:

  • (Boolean)


108
109
110
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 108

def supports_transaction_isolation?
  false
end

#supports_virtual_columns?Boolean

Returns:

  • (Boolean)


132
133
134
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 132

def supports_virtual_columns?
  false
end

#table_exists?(table_name) ⇒ Boolean

Returns:

  • (Boolean)


263
264
265
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 263

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

#tables(name = nil) ⇒ Object

Schema introspection



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

def tables(name = nil)
  sql = "    SELECT name FROM sqlite_master\n    WHERE type = 'table'\n    AND name NOT LIKE 'sqlite_%'\n    AND name NOT IN ('ar_internal_metadata')\n    ORDER BY name\n  SQL\n\n  result = execute(sql, name)\n  return [] unless result && result[0] && result[0][\"results\"]\n\n  result[0][\"results\"].map { |row| row[\"name\"] }\nend\n"

#type_cast(value) ⇒ Object

Type casting



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 363

def type_cast(value)
  case value
  when ActiveModel::Attribute
    type_cast(value.value_for_database)
  when TrueClass
    1
  when FalseClass
    0
  when Time, DateTime
    value.utc.iso8601
  when Date
    value.iso8601
  else
    value
  end
end

#viewsObject



211
212
213
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 211

def views
  []
end

#write_query?(sql) ⇒ Boolean

Returns:

  • (Boolean)


219
220
221
# File 'lib/active_record/connection_adapters/cloudflare_d1_adapter.rb', line 219

def write_query?(sql)
  !sql.match?(/\A\s*(SELECT|PRAGMA|EXPLAIN)/i)
end