cloudflare-d1 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/d1.yaml +560 -0
- data/.claude/test-driving-rails.md +5482 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/DATABASE_MANAGEMENT.md +365 -0
- data/LICENSE.txt +21 -0
- data/README.md +253 -0
- data/RELEASING.md +199 -0
- data/Rakefile +16 -0
- data/cloudflare-d1.gemspec +40 -0
- data/examples/roda/.dockerignore +14 -0
- data/examples/roda/.gitignore +11 -0
- data/examples/roda/Dockerfile +21 -0
- data/examples/roda/Gemfile +12 -0
- data/examples/roda/Gemfile.lock +35 -0
- data/examples/roda/README.md +459 -0
- data/examples/roda/app.rb +165 -0
- data/examples/roda/db/migrations/001_create_users.rb +17 -0
- data/examples/roda/setup.sh +128 -0
- data/examples/roda/worker.js +119 -0
- data/examples/roda/wrangler.jsonc +68 -0
- data/lib/active_record/connection_adapters/cloudflare_d1_adapter.rb +447 -0
- data/lib/cloudflare/d1/client.rb +149 -0
- data/lib/cloudflare/d1/model.rb +46 -0
- data/lib/cloudflare/d1/railtie.rb +13 -0
- data/lib/cloudflare/d1/version.rb +7 -0
- data/lib/cloudflare/d1.rb +39 -0
- data/lib/sequel/adapters/cloudflare_d1.rb +265 -0
- data/lib/tasks/database.rake +158 -0
- data/lib/tasks/sequel.rake +199 -0
- data/sig/cloudflare/d1.rbs +6 -0
- metadata +163 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sequel"
|
|
4
|
+
require "sequel/adapters/utils/unmodified_identifiers"
|
|
5
|
+
require "cloudflare/d1/client"
|
|
6
|
+
|
|
7
|
+
module Sequel
|
|
8
|
+
module CloudflareD1
|
|
9
|
+
class Database < Sequel::Database
|
|
10
|
+
include Sequel::UnmodifiedIdentifiers::DatabaseMethods
|
|
11
|
+
|
|
12
|
+
set_adapter_scheme :cloudflare_d1
|
|
13
|
+
|
|
14
|
+
def connect(server)
|
|
15
|
+
opts = server_opts(server)
|
|
16
|
+
|
|
17
|
+
@account_id = opts[:account_id] || ENV["CLOUDFLARE_ACCOUNT_ID"]
|
|
18
|
+
@api_token = opts[:api_token] || ENV["CLOUDFLARE_API_TOKEN"]
|
|
19
|
+
@database_id = opts[:database] || opts[:database_id]
|
|
20
|
+
|
|
21
|
+
raise Error, "account_id is required" unless @account_id
|
|
22
|
+
raise Error, "api_token is required" unless @api_token
|
|
23
|
+
raise Error, "database_id is required" unless @database_id
|
|
24
|
+
|
|
25
|
+
@client = ::Cloudflare::D1::Client.new(
|
|
26
|
+
account_id: @account_id,
|
|
27
|
+
api_token: @api_token
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def disconnect_connection(conn)
|
|
32
|
+
# No persistent connection to close
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def execute(sql, opts = OPTS)
|
|
36
|
+
synchronize(opts[:server]) do |conn|
|
|
37
|
+
result = @client.query(
|
|
38
|
+
database_id: @database_id,
|
|
39
|
+
sql: sql
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
result.dig("result", 0, "results")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def execute_dui(sql, opts = OPTS)
|
|
47
|
+
synchronize(opts[:server]) do |conn|
|
|
48
|
+
result = @client.query(
|
|
49
|
+
database_id: @database_id,
|
|
50
|
+
sql: sql
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
meta = result.dig("result", 0, "meta")
|
|
54
|
+
meta["changes"] || 0
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def execute_insert(sql, opts = OPTS)
|
|
59
|
+
synchronize(opts[:server]) do |conn|
|
|
60
|
+
result = @client.query(
|
|
61
|
+
database_id: @database_id,
|
|
62
|
+
sql: sql
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
meta = result.dig("result", 0, "meta")
|
|
66
|
+
meta["last_row_id"]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fetch_rows(sql)
|
|
71
|
+
rows = execute(sql)
|
|
72
|
+
rows.each { |row| yield row } if rows
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def execute_ddl(sql, opts = OPTS)
|
|
76
|
+
execute(sql, opts)
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Schema methods
|
|
81
|
+
|
|
82
|
+
def tables(opts = OPTS)
|
|
83
|
+
sql = <<-SQL
|
|
84
|
+
SELECT name FROM sqlite_master
|
|
85
|
+
WHERE type = 'table'
|
|
86
|
+
AND name NOT LIKE 'sqlite_%'
|
|
87
|
+
ORDER BY name
|
|
88
|
+
SQL
|
|
89
|
+
|
|
90
|
+
rows = execute(sql)
|
|
91
|
+
rows.map { |row| row["name"].to_sym }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def schema(table, opts = OPTS)
|
|
95
|
+
sql = "PRAGMA table_info(#{literal(table.to_s)})"
|
|
96
|
+
rows = execute(sql)
|
|
97
|
+
|
|
98
|
+
rows.map do |row|
|
|
99
|
+
column = row["name"].to_sym
|
|
100
|
+
info = {
|
|
101
|
+
type: type_literal_to_sequel_type(row["type"]),
|
|
102
|
+
allow_null: row["notnull"] == 0,
|
|
103
|
+
default: row["dflt_value"],
|
|
104
|
+
primary_key: row["pk"] == 1,
|
|
105
|
+
db_type: row["type"]
|
|
106
|
+
}
|
|
107
|
+
[column, info]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def indexes(table, opts = OPTS)
|
|
112
|
+
sql = "PRAGMA index_list(#{literal(table.to_s)})"
|
|
113
|
+
index_list = execute(sql)
|
|
114
|
+
|
|
115
|
+
index_list.map do |idx|
|
|
116
|
+
index_name = idx["name"]
|
|
117
|
+
unique = idx["unique"] == 1
|
|
118
|
+
|
|
119
|
+
columns_sql = "PRAGMA index_info(#{literal(index_name)})"
|
|
120
|
+
columns = execute(columns_sql)
|
|
121
|
+
.sort_by { |c| c["seqno"] }
|
|
122
|
+
.map { |c| c["name"].to_sym }
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
name: index_name.to_sym,
|
|
126
|
+
unique: unique,
|
|
127
|
+
columns: columns
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def supports_create_table_if_not_exists?
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def supports_drop_table_if_exists?
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def supports_transaction_isolation_levels?
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def supports_savepoints?
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def supports_prepared_transactions?
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def supports_deferrable_constraints?
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def supports_transactional_ddl?
|
|
157
|
+
false
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def transaction(opts = OPTS)
|
|
161
|
+
# D1 API doesn't support transactions across HTTP requests
|
|
162
|
+
# So we just yield without transaction support
|
|
163
|
+
yield
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def database_type
|
|
167
|
+
:cloudflare_d1
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def dataset_class_default
|
|
171
|
+
Dataset
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def type_literal_to_sequel_type(type)
|
|
177
|
+
case type.to_s.upcase
|
|
178
|
+
when /INT/i
|
|
179
|
+
:integer
|
|
180
|
+
when /REAL|FLOAT|DOUBLE/i
|
|
181
|
+
:float
|
|
182
|
+
when /TEXT|CHAR|CLOB/i
|
|
183
|
+
:string
|
|
184
|
+
when /BLOB/i
|
|
185
|
+
:blob
|
|
186
|
+
when /NUMERIC|DECIMAL/i
|
|
187
|
+
:decimal
|
|
188
|
+
else
|
|
189
|
+
:string
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class Dataset < Sequel::Dataset
|
|
195
|
+
include Sequel::UnmodifiedIdentifiers::DatasetMethods
|
|
196
|
+
|
|
197
|
+
def fetch_rows(sql)
|
|
198
|
+
rows = execute(sql)
|
|
199
|
+
if rows
|
|
200
|
+
rows.each do |row|
|
|
201
|
+
# Convert string keys to symbols
|
|
202
|
+
symbolized_row = row.transform_keys(&:to_sym)
|
|
203
|
+
yield symbolized_row
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def insert_select(*values)
|
|
209
|
+
# D1 doesn't support RETURNING clause
|
|
210
|
+
execute_insert(insert_sql(*values))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def supports_insert_select?
|
|
214
|
+
false
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def supports_returning?(type)
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def supports_window_functions?
|
|
222
|
+
true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def supports_cte?
|
|
226
|
+
true
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def supports_where_true?
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def supports_derived_column_lists?
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def complex_expression_sql_append(sql, op, args)
|
|
238
|
+
case op
|
|
239
|
+
when :'||'
|
|
240
|
+
# SQLite uses || for concatenation
|
|
241
|
+
super
|
|
242
|
+
else
|
|
243
|
+
super
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def literal_false
|
|
248
|
+
"0"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def literal_true
|
|
252
|
+
"1"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def quote_identifiers?
|
|
256
|
+
true
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def quoted_identifier_append(sql, name)
|
|
260
|
+
sql << '"' << name.to_s.gsub('"', '""') << '"'
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cloudflare/d1/client"
|
|
4
|
+
|
|
5
|
+
namespace :db do
|
|
6
|
+
desc "Create the Cloudflare D1 database"
|
|
7
|
+
task create: :load_config do
|
|
8
|
+
config = ActiveRecord::Base.connection_config
|
|
9
|
+
client = Cloudflare::D1::Client.new(
|
|
10
|
+
account_id: config[:account_id] || ENV["CLOUDFLARE_ACCOUNT_ID"],
|
|
11
|
+
api_token: config[:api_token] || ENV["CLOUDFLARE_API_TOKEN"]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
database_name = config[:database] || config[:database_id]
|
|
15
|
+
|
|
16
|
+
if database_name =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
17
|
+
puts "Database ID already exists: #{database_name}"
|
|
18
|
+
puts "Skipping creation."
|
|
19
|
+
else
|
|
20
|
+
begin
|
|
21
|
+
result = client.create_database(name: database_name)
|
|
22
|
+
puts "Created database: #{result['name']} (#{result['uuid']})"
|
|
23
|
+
puts ""
|
|
24
|
+
puts "Add this to your database.yml:"
|
|
25
|
+
puts " database_id: #{result['uuid']}"
|
|
26
|
+
rescue Cloudflare::D1::Client::APIError => e
|
|
27
|
+
if e.message.include?("already exists")
|
|
28
|
+
puts "Database already exists: #{database_name}"
|
|
29
|
+
else
|
|
30
|
+
raise
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Drop the Cloudflare D1 database"
|
|
37
|
+
task drop: :load_config do
|
|
38
|
+
config = ActiveRecord::Base.connection_config
|
|
39
|
+
client = Cloudflare::D1::Client.new(
|
|
40
|
+
account_id: config[:account_id] || ENV["CLOUDFLARE_ACCOUNT_ID"],
|
|
41
|
+
api_token: config[:api_token] || ENV["CLOUDFLARE_API_TOKEN"]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
database_id = config[:database_id] || config[:database]
|
|
45
|
+
|
|
46
|
+
unless database_id =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
47
|
+
abort "Error: database_id must be a UUID. Cannot drop database by name."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
print "Are you sure you want to drop database #{database_id}? [y/N] "
|
|
51
|
+
confirmation = STDIN.gets.chomp
|
|
52
|
+
|
|
53
|
+
if confirmation.downcase == "y"
|
|
54
|
+
client.delete_database(database_id: database_id)
|
|
55
|
+
puts "Dropped database: #{database_id}"
|
|
56
|
+
else
|
|
57
|
+
puts "Cancelled."
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "List all Cloudflare D1 databases"
|
|
62
|
+
task list: :load_config do
|
|
63
|
+
config = ActiveRecord::Base.connection_config
|
|
64
|
+
client = Cloudflare::D1::Client.new(
|
|
65
|
+
account_id: config[:account_id] || ENV["CLOUDFLARE_ACCOUNT_ID"],
|
|
66
|
+
api_token: config[:api_token] || ENV["CLOUDFLARE_API_TOKEN"]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
databases = client.list_databases
|
|
70
|
+
|
|
71
|
+
if databases.empty?
|
|
72
|
+
puts "No databases found."
|
|
73
|
+
else
|
|
74
|
+
puts "Cloudflare D1 Databases:"
|
|
75
|
+
puts "-" * 80
|
|
76
|
+
databases.each do |db|
|
|
77
|
+
puts "Name: #{db['name']}"
|
|
78
|
+
puts "UUID: #{db['uuid']}"
|
|
79
|
+
puts "Created: #{db['created_at']}"
|
|
80
|
+
puts "Tables: #{db['num_tables']}"
|
|
81
|
+
puts "-" * 80
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc "Migrate the Cloudflare D1 database (run pending migrations)"
|
|
87
|
+
task migrate: :load_config do
|
|
88
|
+
ActiveRecord::Base.establish_connection
|
|
89
|
+
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
|
|
90
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
desc "Rollback the database one migration"
|
|
94
|
+
task rollback: :load_config do
|
|
95
|
+
step = ENV["STEP"] ? ENV["STEP"].to_i : 1
|
|
96
|
+
ActiveRecord::Base.establish_connection
|
|
97
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).rollback(step)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc "Display migration status"
|
|
101
|
+
task :migrate_status => :load_config do
|
|
102
|
+
ActiveRecord::Base.establish_connection
|
|
103
|
+
|
|
104
|
+
unless ActiveRecord::Base.connection.table_exists?("schema_migrations")
|
|
105
|
+
puts "Schema migrations table doesn't exist yet."
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
db_list = ActiveRecord::SchemaMigration.all_versions
|
|
110
|
+
|
|
111
|
+
puts "\nDatabase: #{ActiveRecord::Base.connection_config[:database_id]}\n\n"
|
|
112
|
+
puts "#{'Status'.center(10)} #{'Migration ID'.ljust(14)} Migration Name"
|
|
113
|
+
puts "-" * 80
|
|
114
|
+
|
|
115
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrations.each do |migration|
|
|
116
|
+
status = db_list.include?(migration.version.to_s) ? "up" : "down"
|
|
117
|
+
puts "#{status.center(10)} #{migration.version.to_s.ljust(14)} #{migration.name}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
puts
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc "Reset the database (drop, create, migrate)"
|
|
124
|
+
task reset: [:drop, :create, :migrate]
|
|
125
|
+
|
|
126
|
+
desc "Setup the database (create and migrate)"
|
|
127
|
+
task setup: [:create, :migrate]
|
|
128
|
+
|
|
129
|
+
task :load_config do
|
|
130
|
+
require "active_record"
|
|
131
|
+
require "active_record/connection_adapters/cloudflare_d1_adapter"
|
|
132
|
+
|
|
133
|
+
# Load Rails database configuration if available
|
|
134
|
+
if defined?(Rails)
|
|
135
|
+
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
|
|
136
|
+
ActiveRecord::Base.establish_connection(Rails.env.to_sym)
|
|
137
|
+
else
|
|
138
|
+
# Load from config/database.yml if it exists
|
|
139
|
+
db_config_path = File.expand_path("config/database.yml", Dir.pwd)
|
|
140
|
+
if File.exist?(db_config_path)
|
|
141
|
+
require "yaml"
|
|
142
|
+
require "erb"
|
|
143
|
+
configs = YAML.load(ERB.new(File.read(db_config_path)).result) || {}
|
|
144
|
+
env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
145
|
+
config = configs[env]
|
|
146
|
+
|
|
147
|
+
if config
|
|
148
|
+
ActiveRecord::Base.configurations = { env => config }
|
|
149
|
+
ActiveRecord::Base.establish_connection(config)
|
|
150
|
+
else
|
|
151
|
+
abort "No database configuration found for environment: #{env}"
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
abort "No database configuration found. Create config/database.yml or set ENV variables."
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sequel"
|
|
4
|
+
require "cloudflare/d1/client"
|
|
5
|
+
|
|
6
|
+
namespace :sequel do
|
|
7
|
+
namespace :db do
|
|
8
|
+
desc "Create the Cloudflare D1 database"
|
|
9
|
+
task :create do
|
|
10
|
+
require_sequel_connection_config
|
|
11
|
+
|
|
12
|
+
database_name = ENV["DATABASE_NAME"] || abort("DATABASE_NAME environment variable required")
|
|
13
|
+
|
|
14
|
+
if database_name =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
15
|
+
puts "Database ID already exists: #{database_name}"
|
|
16
|
+
puts "Skipping creation."
|
|
17
|
+
else
|
|
18
|
+
client = cloudflare_client
|
|
19
|
+
begin
|
|
20
|
+
result = client.create_database(name: database_name)
|
|
21
|
+
puts "Created database: #{result['name']} (#{result['uuid']})"
|
|
22
|
+
puts ""
|
|
23
|
+
puts "Set this environment variable:"
|
|
24
|
+
puts " DATABASE_URL=cloudflare_d1://#{result['uuid']}"
|
|
25
|
+
rescue Cloudflare::D1::Client::APIError => e
|
|
26
|
+
if e.message.include?("already exists")
|
|
27
|
+
puts "Database already exists: #{database_name}"
|
|
28
|
+
else
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "Drop the Cloudflare D1 database"
|
|
36
|
+
task :drop do
|
|
37
|
+
require_sequel_connection_config
|
|
38
|
+
|
|
39
|
+
database_id = ENV["DATABASE_ID"] || abort("DATABASE_ID environment variable required")
|
|
40
|
+
|
|
41
|
+
unless database_id =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
42
|
+
abort "Error: DATABASE_ID must be a UUID"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
print "Are you sure you want to drop database #{database_id}? [y/N] "
|
|
46
|
+
confirmation = STDIN.gets.chomp
|
|
47
|
+
|
|
48
|
+
if confirmation.downcase == "y"
|
|
49
|
+
client = cloudflare_client
|
|
50
|
+
client.delete_database(database_id: database_id)
|
|
51
|
+
puts "Dropped database: #{database_id}"
|
|
52
|
+
else
|
|
53
|
+
puts "Cancelled."
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "List all Cloudflare D1 databases"
|
|
58
|
+
task :list do
|
|
59
|
+
require_sequel_connection_config
|
|
60
|
+
|
|
61
|
+
client = cloudflare_client
|
|
62
|
+
databases = client.list_databases
|
|
63
|
+
|
|
64
|
+
if databases.empty?
|
|
65
|
+
puts "No databases found."
|
|
66
|
+
else
|
|
67
|
+
puts "Cloudflare D1 Databases:"
|
|
68
|
+
puts "-" * 80
|
|
69
|
+
databases.each do |db|
|
|
70
|
+
puts "Name: #{db['name']}"
|
|
71
|
+
puts "UUID: #{db['uuid']}"
|
|
72
|
+
puts "Created: #{db['created_at']}"
|
|
73
|
+
puts "Tables: #{db['num_tables']}"
|
|
74
|
+
puts "-" * 80
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
desc "Run migrations"
|
|
80
|
+
task :migrate, [:version] do |t, args|
|
|
81
|
+
require_sequel_connection_config
|
|
82
|
+
Sequel.extension :migration
|
|
83
|
+
|
|
84
|
+
db_url = ENV["DATABASE_URL"] || abort("DATABASE_URL environment variable required")
|
|
85
|
+
migrations_path = ENV["MIGRATIONS_PATH"] || "db/migrations"
|
|
86
|
+
|
|
87
|
+
Sequel.connect(db_url) do |db|
|
|
88
|
+
version = args[:version].to_i if args[:version]
|
|
89
|
+
|
|
90
|
+
if version
|
|
91
|
+
puts "Migrating to version #{version}"
|
|
92
|
+
Sequel::Migrator.run(db, migrations_path, target: version)
|
|
93
|
+
else
|
|
94
|
+
puts "Migrating to latest version"
|
|
95
|
+
Sequel::Migrator.run(db, migrations_path)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
puts "Migration complete"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc "Rollback the database one migration"
|
|
103
|
+
task :rollback, [:step] do |t, args|
|
|
104
|
+
require_sequel_connection_config
|
|
105
|
+
Sequel.extension :migration
|
|
106
|
+
|
|
107
|
+
db_url = ENV["DATABASE_URL"] || abort("DATABASE_URL environment variable required")
|
|
108
|
+
migrations_path = ENV["MIGRATIONS_PATH"] || "db/migrations"
|
|
109
|
+
|
|
110
|
+
Sequel.connect(db_url) do |db|
|
|
111
|
+
# Get current version
|
|
112
|
+
if db.table_exists?(:schema_info)
|
|
113
|
+
current_version = db[:schema_info].get(:version)
|
|
114
|
+
|
|
115
|
+
if current_version && current_version > 0
|
|
116
|
+
step = args[:step] ? args[:step].to_i : 1
|
|
117
|
+
|
|
118
|
+
# Get list of migrations
|
|
119
|
+
migrations = Dir[File.join(migrations_path, "*.rb")]
|
|
120
|
+
.map { |f| File.basename(f).split("_").first.to_i }
|
|
121
|
+
.sort
|
|
122
|
+
|
|
123
|
+
# Find target version
|
|
124
|
+
current_idx = migrations.index(current_version)
|
|
125
|
+
if current_idx && current_idx > 0
|
|
126
|
+
target_version = migrations[current_idx - step]
|
|
127
|
+
|
|
128
|
+
puts "Rolling back from version #{current_version} to #{target_version}"
|
|
129
|
+
Sequel::Migrator.run(db, migrations_path, target: target_version)
|
|
130
|
+
puts "Rollback complete"
|
|
131
|
+
else
|
|
132
|
+
puts "Already at first migration"
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
puts "No migrations to rollback"
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
puts "Schema info table doesn't exist yet"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
desc "Display migration status"
|
|
144
|
+
task :status do
|
|
145
|
+
require_sequel_connection_config
|
|
146
|
+
Sequel.extension :migration
|
|
147
|
+
|
|
148
|
+
db_url = ENV["DATABASE_URL"] || abort("DATABASE_URL environment variable required")
|
|
149
|
+
migrations_path = ENV["MIGRATIONS_PATH"] || "db/migrations"
|
|
150
|
+
|
|
151
|
+
Sequel.connect(db_url) do |db|
|
|
152
|
+
unless db.table_exists?(:schema_info)
|
|
153
|
+
puts "Schema info table doesn't exist yet."
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
current_version = db[:schema_info].get(:version) || 0
|
|
158
|
+
|
|
159
|
+
puts "\nDatabase: #{ENV['DATABASE_ID'] || ENV['DATABASE_URL']}\n"
|
|
160
|
+
puts "Current version: #{current_version}\n\n"
|
|
161
|
+
puts "#{'Status'.center(10)} #{'Version'.ljust(14)} Migration File"
|
|
162
|
+
puts "-" * 80
|
|
163
|
+
|
|
164
|
+
migrations = Dir[File.join(migrations_path, "*.rb")].sort
|
|
165
|
+
migrations.each do |migration_file|
|
|
166
|
+
filename = File.basename(migration_file)
|
|
167
|
+
version = filename.split("_").first.to_i
|
|
168
|
+
name = filename.gsub(/^\d+_/, "").gsub(/\.rb$/, "")
|
|
169
|
+
|
|
170
|
+
status = version <= current_version ? "up" : "down"
|
|
171
|
+
puts "#{status.center(10)} #{version.to_s.ljust(14)} #{name}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
puts
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
desc "Reset the database (drop, create, migrate)"
|
|
179
|
+
task reset: [:drop, :create, :migrate]
|
|
180
|
+
|
|
181
|
+
desc "Setup the database (create and migrate)"
|
|
182
|
+
task setup: [:create, :migrate]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def require_sequel_connection_config
|
|
186
|
+
require "sequel"
|
|
187
|
+
require "sequel/adapters/cloudflare_d1"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def cloudflare_client
|
|
191
|
+
account_id = ENV["CLOUDFLARE_ACCOUNT_ID"] || abort("CLOUDFLARE_ACCOUNT_ID environment variable required")
|
|
192
|
+
api_token = ENV["CLOUDFLARE_API_TOKEN"] || abort("CLOUDFLARE_API_TOKEN environment variable required")
|
|
193
|
+
|
|
194
|
+
Cloudflare::D1::Client.new(
|
|
195
|
+
account_id: account_id,
|
|
196
|
+
api_token: api_token
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
end
|