1
|
|
# frozen_string_literal: true
|
2
|
|
|
3
|
2
|
require "yaml"
|
4
|
2
|
require "json"
|
5
|
|
|
6
|
|
##
|
7
|
|
# Extension based upon Sequel::Migration and Sequel::Migrator
|
8
|
|
#
|
9
|
|
# Adds the Sequel::Seed module and the Sequel::Seed::Base and Sequel::Seeder
|
10
|
|
# classes, which allow the user to easily group entity changes and seed/fixture
|
11
|
|
# the database to a newer version only (unlike migrations, seeds are not
|
12
|
|
# directional).
|
13
|
|
#
|
14
|
|
# To load the extension:
|
15
|
|
#
|
16
|
|
# Sequel.extension :seed
|
17
|
|
#
|
18
|
|
# It is also important to set the environment:
|
19
|
|
#
|
20
|
|
# Sequel::Seed.setup(:development)
|
21
|
|
|
22
|
2
|
module Sequel
|
23
|
2
|
class << self
|
24
|
|
##
|
25
|
|
# Creates a Seed subclass according to the given +block+.
|
26
|
|
#
|
27
|
|
# The +env_labels+ lists on which environments the seed should be applicable.
|
28
|
|
# If the current environment is not applicable, the seed is ignored. On the
|
29
|
|
# other hand, if it is applicable, it will be listed in Seed.descendants and
|
30
|
|
# subject to application (if it was not applied yet).
|
31
|
|
#
|
32
|
|
# Expected seed call:
|
33
|
|
#
|
34
|
|
# Sequel.seed(:test) do # seed is only applicable to the test environment
|
35
|
|
# def run
|
36
|
|
# Entity.create attribute: value
|
37
|
|
# end
|
38
|
|
# end
|
39
|
|
#
|
40
|
|
# Wildcard seed:
|
41
|
|
#
|
42
|
|
# Sequel.seed do # seed is applicable to every environment, or no environment
|
43
|
|
# def run
|
44
|
|
# Entity.create attribute: value
|
45
|
|
# end
|
46
|
|
# end
|
47
|
|
#
|
48
|
|
|
49
|
2
|
def seed(*env_labels, &block)
|
50
|
2
|
return if env_labels.length > 0 && !env_labels.map(&:to_sym).include?(Seed.environment)
|
51
|
|
|
52
|
2
|
seed = Class.new(Seed::Base)
|
53
|
2
|
seed.class_eval(&block) if block_given?
|
54
|
2
|
Seed::Base.inherited(seed) unless Seed::Base.descendants.include?(seed)
|
55
|
2
|
seed
|
56
|
|
end
|
57
|
|
end
|
58
|
|
|
59
|
2
|
module Seed
|
60
|
2
|
class Error < Sequel::Error
|
61
|
|
end
|
62
|
|
|
63
|
2
|
class << self
|
64
|
2
|
attr_reader :environment
|
65
|
|
|
66
|
|
##
|
67
|
|
# Sets the Sequel::Seed"s environment to +env+ over which the Seeds should be applied
|
68
|
2
|
def setup(env, opts = {})
|
69
|
2
|
@environment = env.to_sym
|
70
|
2
|
@options ||= {}
|
71
|
2
|
@options[:disable_warning] ||= opts[:disable_warning] || false
|
72
|
|
end
|
73
|
|
|
74
|
|
##
|
75
|
|
# Keep backward compatibility on how to setup the Sequel::Seed environment
|
76
|
|
#
|
77
|
|
# Sets the environment +env+ over which the Seeds should be applied
|
78
|
2
|
def environment=(env)
|
79
|
2
|
setup(env)
|
80
|
|
end
|
81
|
|
|
82
|
|
##
|
83
|
|
# Keep backward compatibility on how to get Sequel::Seed::Base class descendants
|
84
|
2
|
def descendants
|
85
|
2
|
Base.descendants
|
86
|
|
end
|
87
|
|
|
88
|
|
##
|
89
|
|
# Keep backward compatibility on how to append a Sequel::Seed::Base descendant class
|
90
|
2
|
def inherited(base)
|
91
|
2
|
Base.inherited(base)
|
92
|
|
end
|
93
|
|
end
|
94
|
|
|
95
|
|
##
|
96
|
|
# Helper methods for the Sequel::Seed project.
|
97
|
|
|
98
|
2
|
module Helpers
|
99
|
2
|
class << self
|
100
|
2
|
def camelize(term, uppercase_first_letter = true)
|
101
|
2
|
string = term.to_s
|
102
|
2
|
if uppercase_first_letter
|
103
|
2
|
string.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
104
|
|
else
|
105
|
0
|
string.first + camelize(string)[1..-1]
|
106
|
|
end
|
107
|
|
end
|
108
|
|
end
|
109
|
|
end
|
110
|
|
|
111
|
2
|
module SeedDescriptor
|
112
|
2
|
def apply_seed_descriptor(seed_descriptor)
|
113
|
2
|
case seed_descriptor
|
114
|
|
when Hash
|
115
|
2
|
apply_seed_hash(seed_descriptor)
|
116
|
|
when Array
|
117
|
2
|
seed_descriptor.each { |seed_hash| apply_seed_hash(seed_hash) }
|
118
|
|
end
|
119
|
|
end
|
120
|
|
|
121
|
2
|
private
|
122
|
|
|
123
|
2
|
def apply_seed_hash(seed_hash)
|
124
|
2
|
return unless seed_hash.class <= Hash
|
125
|
2
|
if seed_hash.has_key?("environment")
|
126
|
2
|
case seed_hash["environment"]
|
127
|
|
when String, Symbol
|
128
|
2
|
return if seed_hash["environment"].to_sym != Seed.environment
|
129
|
|
when Array
|
130
|
0
|
return unless seed_hash["environment"].map(&:to_sym).include?(Seed.environment)
|
131
|
|
end
|
132
|
|
end
|
133
|
|
|
134
|
2
|
keys = seed_hash.keys
|
135
|
2
|
keys.delete("environment")
|
136
|
2
|
keys.each do |key|
|
137
|
2
|
key_hash = seed_hash[key]
|
138
|
2
|
entries = nil
|
139
|
2
|
class_name = if key_hash.has_key?("class")
|
140
|
2
|
entries = key_hash["entries"]
|
141
|
2
|
key_hash["class"]
|
142
|
|
else
|
143
|
2
|
Helpers.camelize(key)
|
144
|
|
end
|
145
|
|
# It will raise an error if the class name is not defined
|
146
|
2
|
class_const = Kernel.const_get(class_name)
|
147
|
2
|
if entries
|
148
|
2
|
entries.each { |hash| create_model(class_const, hash) }
|
149
|
|
else
|
150
|
2
|
create_model(class_const, key_hash)
|
151
|
|
end
|
152
|
|
end
|
153
|
|
end
|
154
|
|
|
155
|
2
|
def create_model(class_const, hash)
|
156
|
2
|
object_instance = class_const.new
|
157
|
2
|
object_instance_attr = hash.each do |attr, value|
|
158
|
2
|
object_instance.set({attr.to_sym => value})
|
159
|
|
end
|
160
|
2
|
raise(Error, "Attempt to create invalid model instance of #{class_name}") unless object_instance.valid?
|
161
|
2
|
object_instance.save
|
162
|
|
end
|
163
|
|
end
|
164
|
|
|
165
|
2
|
class Base
|
166
|
2
|
class << self
|
167
|
2
|
def apply
|
168
|
2
|
new.run
|
169
|
|
end
|
170
|
|
|
171
|
2
|
def descendants
|
172
|
2
|
@descendants ||= []
|
173
|
|
end
|
174
|
|
|
175
|
2
|
def inherited(base)
|
176
|
2
|
descendants << base
|
177
|
|
end
|
178
|
|
end
|
179
|
|
|
180
|
2
|
def run
|
181
|
|
end
|
182
|
|
end
|
183
|
|
|
184
|
|
##
|
185
|
|
# Class resposible for applying all the seeds related to the current environment,
|
186
|
|
# if and only if they were not previously applied.
|
187
|
|
#
|
188
|
|
# To apply the seeds/fixtures:
|
189
|
|
#
|
190
|
|
# Sequel::Seeder.apply(db, directory)
|
191
|
|
#
|
192
|
|
# +db+ holds the Sequel database connection
|
193
|
|
#
|
194
|
|
# +directory+ the path to the seeds/fixtures files
|
195
|
|
end
|
196
|
|
|
197
|
2
|
class Seeder
|
198
|
2
|
SEED_FILE_PATTERN = /\A(\d+)_.+\.(rb|json|yml|yaml)\z/i.freeze
|
199
|
2
|
RUBY_SEED_FILE_PATTERN = /\A(\d+)_.+\.(rb)\z/i.freeze
|
200
|
2
|
YAML_SEED_FILE_PATTERN = /\A(\d+)_.+\.(yml|yaml)\z/i.freeze
|
201
|
2
|
JSON_SEED_FILE_PATTERN = /\A(\d+)_.+\.(json)\z/i.freeze
|
202
|
2
|
SEED_SPLITTER = "_".freeze
|
203
|
2
|
MINIMUM_TIMESTAMP = 20000101
|
204
|
|
|
205
|
2
|
Error = Seed::Error
|
206
|
|
|
207
|
2
|
def self.apply(db, directory, opts = {})
|
208
|
2
|
seeder_class(directory).new(db, directory, opts).run
|
209
|
|
end
|
210
|
|
|
211
|
2
|
def self.seeder_class(directory)
|
212
|
2
|
if self.equal?(Seeder)
|
213
|
2
|
Dir.new(directory).each do |file|
|
214
|
2
|
next unless SEED_FILE_PATTERN.match(file)
|
215
|
2
|
return TimestampSeeder if file.split(SEED_SPLITTER, 2).first.to_i > MINIMUM_TIMESTAMP
|
216
|
|
end
|
217
|
2
|
raise(Error, "seeder not available for files; please check the configured seed directory \"#{directory}\". Also ensure seed files are in YYYYMMDD_seed_file.rb format.")
|
218
|
|
else
|
219
|
0
|
self
|
220
|
|
end
|
221
|
|
end
|
222
|
|
|
223
|
2
|
attr_reader :column
|
224
|
|
|
225
|
2
|
attr_reader :db
|
226
|
|
|
227
|
2
|
attr_reader :directory
|
228
|
|
|
229
|
2
|
attr_reader :ds
|
230
|
|
|
231
|
2
|
attr_reader :files
|
232
|
|
|
233
|
2
|
attr_reader :table
|
234
|
|
|
235
|
2
|
def initialize(db, directory, opts = {})
|
236
|
2
|
raise(Error, "Must supply a valid seed path") unless File.directory?(directory)
|
237
|
2
|
@db = db
|
238
|
2
|
@directory = directory
|
239
|
2
|
@allow_missing_seed_files = opts[:allow_missing_seed_files]
|
240
|
2
|
@files = get_seed_files
|
241
|
2
|
schema, table = @db.send(:schema_and_table, opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE))
|
242
|
2
|
@table = schema ? Sequel::SQL::QualifiedIdentifier.new(schema, table) : table
|
243
|
2
|
@column = opts[:column] || self.class.const_get(:DEFAULT_SCHEMA_COLUMN)
|
244
|
2
|
@ds = schema_dataset
|
245
|
2
|
@use_transactions = opts[:use_transactions]
|
246
|
|
end
|
247
|
|
|
248
|
2
|
private
|
249
|
|
|
250
|
2
|
def checked_transaction(seed, &block)
|
251
|
2
|
use_trans = if @use_transactions.nil?
|
252
|
2
|
@db.supports_transactional_ddl?
|
253
|
|
else
|
254
|
0
|
@use_transactions
|
255
|
|
end
|
256
|
|
|
257
|
2
|
if use_trans
|
258
|
2
|
db.transaction(&block)
|
259
|
|
else
|
260
|
2
|
yield
|
261
|
|
end
|
262
|
|
end
|
263
|
|
|
264
|
2
|
def remove_seed_classes
|
265
|
2
|
Seed::Base.descendants.each do |c|
|
266
|
2
|
Object.send(:remove_const, c.to_s) rescue nil
|
267
|
|
end
|
268
|
2
|
Seed::Base.descendants.clear
|
269
|
|
end
|
270
|
|
|
271
|
2
|
def seed_version_from_file(filename)
|
272
|
0
|
filename.split(SEED_SPLITTER, 2).first.to_i
|
273
|
|
end
|
274
|
|
end
|
275
|
|
|
276
|
|
##
|
277
|
|
# A Seeder subclass to apply timestamped seeds/fixtures files.
|
278
|
|
# It follows the same syntax & semantics for the Seeder superclass.
|
279
|
|
#
|
280
|
|
# To apply the seeds/fixtures:
|
281
|
|
#
|
282
|
|
# Sequel::TimestampSeeder.apply(db, directory)
|
283
|
|
#
|
284
|
|
# +db+ holds the Sequel database connection
|
285
|
|
#
|
286
|
|
# +directory+ the path to the seeds/fixtures files
|
287
|
|
|
288
|
2
|
class TimestampSeeder < Seeder
|
289
|
2
|
DEFAULT_SCHEMA_COLUMN = :filename
|
290
|
2
|
DEFAULT_SCHEMA_TABLE = :schema_seeds
|
291
|
|
|
292
|
2
|
Error = Seed::Error
|
293
|
|
|
294
|
2
|
attr_reader :applied_seeds
|
295
|
|
|
296
|
2
|
attr_reader :seed_tuples
|
297
|
|
|
298
|
2
|
def initialize(db, directory, opts = {})
|
299
|
2
|
super
|
300
|
2
|
@applied_seeds = get_applied_seeds
|
301
|
2
|
@seed_tuples = get_seed_tuples
|
302
|
|
end
|
303
|
|
|
304
|
2
|
def run
|
305
|
2
|
seed_tuples.each do |s, f|
|
306
|
2
|
t = Time.now
|
307
|
2
|
db.log_info("Applying seed file `#{f}`")
|
308
|
2
|
checked_transaction(s) do
|
309
|
2
|
s.apply
|
310
|
2
|
fi = f.downcase
|
311
|
2
|
ds.insert(column => fi)
|
312
|
|
end
|
313
|
2
|
db.log_info("Seed file `#{f}` applied, it took #{sprintf("%0.6f", Time.now - t)} seconds")
|
314
|
|
end
|
315
|
|
nil
|
316
|
|
end
|
317
|
|
|
318
|
2
|
private
|
319
|
|
|
320
|
2
|
def get_applied_seeds
|
321
|
2
|
am = ds.select_order_map(column)
|
322
|
2
|
missing_seed_files = am - files.map { |f| File.basename(f).downcase }
|
323
|
2
|
if missing_seed_files.length > 0 && !@allow_missing_seed_files
|
324
|
0
|
raise(Error, "Seed files not in file system: #{missing_seed_files.join(", ")}")
|
325
|
|
end
|
326
|
2
|
am
|
327
|
|
end
|
328
|
|
|
329
|
2
|
def get_seed_files
|
330
|
2
|
files = []
|
331
|
2
|
Dir.new(directory).each do |file|
|
332
|
2
|
next unless SEED_FILE_PATTERN.match(file)
|
333
|
2
|
files << File.join(directory, file)
|
334
|
|
end
|
335
|
2
|
files.sort_by { |f| SEED_FILE_PATTERN.match(File.basename(f))[1].to_i }
|
336
|
|
end
|
337
|
|
|
338
|
2
|
def get_seed_tuples
|
339
|
2
|
remove_seed_classes
|
340
|
2
|
seeds = []
|
341
|
2
|
ms = Seed::Base.descendants
|
342
|
2
|
files.each do |path|
|
343
|
2
|
f = File.basename(path)
|
344
|
2
|
fi = f.downcase
|
345
|
2
|
if !applied_seeds.include?(fi)
|
346
|
|
#begin
|
347
|
2
|
load(path) if RUBY_SEED_FILE_PATTERN.match(f)
|
348
|
2
|
create_yaml_seed(path) if YAML_SEED_FILE_PATTERN.match(f)
|
349
|
2
|
create_json_seed(path) if JSON_SEED_FILE_PATTERN.match(f)
|
350
|
|
#rescue Exception => e
|
351
|
|
#raise(Error, "error while processing seed file #{path}: #{e.inspect}")
|
352
|
|
#end
|
353
|
2
|
el = [ms.last, f]
|
354
|
2
|
next if ms.last.nil?
|
355
|
2
|
if ms.last < Seed::Base && !seeds.include?(el)
|
356
|
2
|
seeds << el
|
357
|
|
end
|
358
|
|
end
|
359
|
|
end
|
360
|
2
|
seeds
|
361
|
|
end
|
362
|
|
|
363
|
2
|
def create_yaml_seed(path)
|
364
|
2
|
seed_descriptor = YAML::load(File.open(path))
|
365
|
2
|
seed = Class.new(Seed::Base)
|
366
|
2
|
seed.const_set "YAML_SEED", seed_descriptor
|
367
|
2
|
seed.class_eval do
|
368
|
2
|
include Seed::SeedDescriptor
|
369
|
|
|
370
|
2
|
def run
|
371
|
2
|
seed_descriptor = self.class.const_get "YAML_SEED"
|
372
|
2
|
raise(Error, "YAML seed improperly defined") if seed_descriptor.nil?
|
373
|
2
|
self.apply_seed_descriptor(seed_descriptor)
|
374
|
|
end
|
375
|
|
end
|
376
|
2
|
Seed::Base.inherited(seed) unless Seed::Base.descendants.include?(seed)
|
377
|
2
|
seed
|
378
|
|
end
|
379
|
|
|
380
|
2
|
def create_json_seed(path)
|
381
|
2
|
seed_descriptor = JSON.parse(File.read(path))
|
382
|
2
|
seed = Class.new(Seed::Base)
|
383
|
2
|
seed.const_set "JSON_SEED", seed_descriptor
|
384
|
2
|
seed.class_eval do
|
385
|
2
|
include Seed::SeedDescriptor
|
386
|
|
|
387
|
2
|
def run
|
388
|
2
|
seed_descriptor = self.class.const_get "JSON_SEED"
|
389
|
2
|
raise(Error, "JSON seed improperly defined") if seed_descriptor.nil?
|
390
|
2
|
self.apply_seed_descriptor(seed_descriptor)
|
391
|
|
end
|
392
|
|
end
|
393
|
2
|
Seed::Base.inherited(seed) unless Seed::Base.descendants.include?(seed)
|
394
|
2
|
seed
|
395
|
|
end
|
396
|
|
|
397
|
2
|
def schema_dataset
|
398
|
2
|
c = column
|
399
|
2
|
ds = db.from(table)
|
400
|
2
|
if !db.table_exists?(table)
|
401
|
2
|
db.create_table(table) { String c, primary_key: true }
|
402
|
2
|
elsif !ds.columns.include?(c)
|
403
|
0
|
raise(Error, "Seeder table \"#{table}\" does not contain column \"#{c}\"")
|
404
|
|
end
|
405
|
2
|
ds
|
406
|
|
end
|
407
|
|
end
|
408
|
|
end
|