./bin/cli/am in lux-fw-0.1.35 vs ./bin/cli/am in lux-fw-0.2.1

- old
+ new

@@ -1,18 +1,33 @@ #!/usr/bin/env ruby -ENV['DB_NAME'] = ARGV[0] if ARGV[0] +require 'bundler/setup' +require 'dotenv' -require './config/application' +Dotenv.load +Bundler.require(:default) +# load app config +require './config/db' + +# Sequel extension and plugin test +DB.run %[DROP TABLE IF EXISTS lux_tests;] +DB.run %[CREATE TABLE lux_tests (int_array integer[] default '{}', text_array text[] default '{}');] +class LuxTest < Sequel::Model; end; +LuxTest.new.save +die('"DB.extension :pg_array" not loaded') unless LuxTest.first.int_array.class == Sequel::Postgres::PGArray +DB.run %[DROP TABLE IF EXISTS lux_tests;] + class AutoMigrate attr_accessor :fields class << self - def table(table_name) - die "table [#{table_name}] not in plural -> expected [#{table_name.to_s.pluralize}]" unless table_name.to_s.pluralize == table_name.to_s + def table table_name + die "Table [#{table_name}] not in plural -> expected [#{table_name.to_s.pluralize}]" unless table_name.to_s.pluralize == table_name.to_s + die 'Table name "%s" is not permited' % table_name if [:categories].include?(table_name) + unless DB.table_exists?(table_name.to_s) # http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html DB.create_table table_name do primary_key :id, Integer index :id, unique: true @@ -47,129 +62,140 @@ ### def initialize table_name @table_name = table_name @fields = {} + + klass = @table_name.to_s.classify + + if Object.const_defined?(klass) + puts 'Table name "%s" is not allowed - class "%s" is allready defined'.red % [table_name, klass] + exit + end + + eval %[class ::%s < Sequel::Model; end;] % klass + @object = klass.constantize.new end + def transaction_do text + self.class.transaction_do text + end + def log_run what puts ' %s' % what.green self.class.transaction_do what end def fix_fields for vals in @fields.values type = vals[0] opts = vals[1] - opts[:limit] ||= 255 if type == :string + opts[:limit] ||= 255 if type == :string opts[:default] ||= false if type == :boolean - - opts[:null] = true unless opts[:null].class.name == 'FalseClass' - opts[:array] ||= false - opts[:unique] ||= false - opts[:default] = [] if opts[:array] + opts[:null] = true unless opts[:null].class.name == 'FalseClass' + opts[:array] ||= false + opts[:unique] ||= false + opts[:default] = [] if opts[:array] end end def get_db_column_type field type, opts = @fields[field] db_type = type db_type = :varchar if type == :string db_type = Time if type == :datetime + if opts[:array] db_type = '%s(%s)' % [db_type, opts[:limit]] if type == :string db_type = '%s[]' % db_type else db_type = 'varchar(%s)' % opts[:limit] if opts[:limit] end + db_type end def update - begin - obj = @table_name.to_s.classify.constantize - o = obj.new - rescue - puts "Object #{@table_name.to_s.classify.red} does not exist, yet table #{@table_name.to_s.red} exists!" - return - end + puts "Table #{@table_name.to_s.yellow}, #{@fields.keys.length} fields" # remove extra fields - existing_fields = o.attributes.keys - ['id'] - for field in (existing_fields - @fields.keys.map(&:to_s)) + for field in (@object.columns - @fields.keys - [:id]) print "Remove colum #{@table_name}.#{field} (y/N): ".light_blue if STDIN.gets.chomp.downcase.index('y') DB.drop_column @table_name, field puts " drop_column #{field}".green end end - puts "Table #{@table_name.to_s.yellow}, #{@fields.keys.length} fields" - # loop trough defined fileds in schema for field, opts_in in @fields type = opts_in[0] opts = opts_in[1] db_type = get_db_column_type(field) # create missing columns - unless obj.columns.index(field.to_sym) + unless @object.columns.index(field.to_sym) DB.add_column @table_name, field, db_type, opts if opts[:array] default = type == :string ? "ARRAY[]::character varying[]" : "ARRAY[]::integer[]" - - self.class.transaction_do "ALTER TABLE #{@table_name} ALTER COLUMN #{field} SET DEFAULT #{default};" + transaction_do "ALTER TABLE #{@table_name} ALTER COLUMN #{field} SET DEFAULT #{default};" end puts " add_column #{field}, #{db_type}, #{opts.to_json}".green next end - if current = obj.db_schema[field.to_sym] + if current = @object.db_schema[field] # unhandled db schema changes will not happen # --- # field - field name # current - current db_schema # type - new proposed type in schema # opts - new proposed types # if we have type set as array and in db it is not array, fix that - if opts[:array] && !current[:db_type].include?('[]') + if opts[:array] + # covert to array unless is array + if !current[:db_type].include?('[]') + transaction_do %[ + alter table #{@table_name} alter #{field} drop default; + alter table #{@table_name} alter #{field} type #{current[:db_type]}[] using array[#{field}]; + alter table #{@table_name} alter #{field} set default '{}'; + ] - self.class.transaction_do %[ - alter table #{@table_name} alter #{field} drop default; - alter table #{@table_name} alter #{field} type #{current[:db_type]}[] using array[#{field}]; - alter table #{@table_name} alter #{field} set default '{}'; - ] - - puts " Coverted #{@table_name}.#{field} to array type".green + puts " Coverted #{@table_name}.#{field} to array type".green + elsif !current[:default] + # force default for array to be present + default = type == :string ? "ARRAY[]::character varying[]" : "ARRAY[]::integer[]" + transaction_do %[alter table #{@table_name} alter #{field} set default #{default};] + end end # if we have array but scema says no array if !opts[:array] && current[:db_type].include?('[]') m = current[:type] == :integer ? "#{field}[0]" : "array_to_string(#{field}, ',')" - self.class.transaction_do %[ + transaction_do %[ alter table #{@table_name} alter #{field} drop default; alter table #{@table_name} alter #{field} type #{current[:db_type].sub('[]','')} using #{m}; ] puts " Coverted #{@table_name}.#{field}[] to non array type".red end # if varchar limit size has changed - if type == :string && current[:max_length] != opts[:limit] - self.class.transaction_do "ALTER TABLE #{@table_name} ALTER COLUMN #{field} TYPE varchar(#{opts[:limit]});" + if type == :string && !opts[:array] && current[:max_length] != opts[:limit] + transaction_do "ALTER TABLE #{@table_name} ALTER COLUMN #{field} TYPE varchar(#{opts[:limit]});" puts " #{field} limit, #{current[:max_length]}-> #{opts[:limit]}".green end # covert from varchar to text if type == :text && current[:max_length] - self.class.transaction_do "ALTER TABLE #{@table_name} ALTER COLUMN #{field} SET DATA TYPE text" + transaction_do "ALTER TABLE #{@table_name} ALTER COLUMN #{field} SET DATA TYPE text" puts " #{field} limit from #{current[:max_length]} to no limit (text type)".green end # null true or false if current[:allow_null] != opts[:null] @@ -219,11 +245,11 @@ end rescue; end end def rename field_old, field_new - existing_fields = @table_name.to_s.classify.constantize.new.attributes.keys.map(&:to_sym) + existing_fields = @table_name.to_s.classify.constantize.new.columns if existing_fields.index(field_old.to_sym) && ! existing_fields.index(field_new.to_sym) DB.rename_column(@table_name, field_old, field_new) puts " * renamed #{@table_name}.#{field_old} to #{@table_name}.#{field_new}" puts ' * please run auto migration again' @@ -234,14 +260,14 @@ def method_missing type, *args name = args[0] opts = args[1] || {} if [:string, :integer, :text, :boolean, :datetime, :date, :jsonb].index(type) - @fields[name] = [type, opts] + @fields[name.to_sym] = [type, opts] elsif [:decimal].index(type) opts[:precision] ||= 8 opts[:scale] ||= 2 - @fields[name] = [:decimal, opts] + @fields[name.to_sym] = [:decimal, opts] elsif type == :timestamps opts[:null] ||= false @fields[:created_at] = [:datetime, opts] @fields[:created_by] = [:integer, opts] @fields[:updated_at] = [:datetime, opts]