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

Read our documentation on viewing source code .

Loading