class Ridgepole::DSLParser class Context class TableDefinition attr_reader :__definition def initialize(table_name, base) @__definition = {} @table_name = table_name @base = base end def column(name, type, options = {}) name = name.to_s @__definition[name] = { :type => type, :options => options, } end TYPES = [ # https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L274 :string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean, # https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L79 :daterange, :numrange, :tsrange, :tstzrange, :int4range, :int8range, :binary, :boolean, :bigint, :xml, :tsvector, :hstore, :inet, :cidr, :macaddr, :uuid, :json, :jsonb, :ltree, :citext, :point, :bit, :bit_varying, :money, ].uniq TYPES.each do |column_type| define_method column_type do |*args| options = args.extract_options! column_names = args column_names.each {|name| column(name, column_type, options) } end end ALIAS_TYPES = { # https://github.com/rails/rails/blob/v5.0.0.rc1/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb tinyblob: [:blob, {limit: 255}], mediumblob: [:binary, {limit: 16777215}], longblob: [:binary, {limit: 4294967295}], tinytext: [:text, {limit: 255}], mediumtext: [:text, {limit: 16777215}], longtext: [:text, {limit: 4294967295}], unsigned_integer: [:integer, {unsigned: true}], unsigned_bigint: [:bigint, {unsigned: true}], unsigned_float: [:float, {limit: 24, unsigned: true}], unsigned_decimal: [:decimal, {precision: 10, unsigned: true}], } # XXX: def blob(*args) options = args.extract_options! options = {limit: 65535}.merge(options) column_names = args column_names.each do |name| column_type = (0..0xff).include?(options[:limit]) ? :blob : :binary column(name, column_type, options) end end ALIAS_TYPES.each do |alias_type, (column_type, default_options)| define_method alias_type do |*args| options = args.extract_options! options = default_options.merge(options) column_names = args column_names.each {|name| column(name, column_type, options) } end end def index(name, options = {}) @base.add_index(@table_name, name, options) end def timestamps(*args) options = {:null => false}.merge(args.extract_options!) column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) type = options.delete(:type) || :integer args.each do |col| column("#{col}_id", type, options) column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic if index_options index("#{col}_id", index_options.is_a?(Hash) ? index_options : {}) index("#{col}_type", index_options.is_a?(Hash) ? index_options : {}) if polymorphic end end end alias :belongs_to :references end attr_reader :__definition attr_reader :__execute def initialize(opts = {}) @__working_dir = File.expand_path(opts[:path] ? File.dirname(opts[:path]) : Dir.pwd) @__definition = {} @__execute = [] end def self.eval(dsl, opts = {}) ctx = self.new(opts) if opts[:path] ctx.instance_eval(dsl, opts[:path]) else ctx.instance_eval(dsl) end [ctx.__definition, ctx.__execute] end def create_table(table_name, options = {}) table_name = table_name.to_s table_definition = TableDefinition.new(table_name, self) [:primary_key].each do |key| options[key] = options[key].to_s if options[key] end yield(table_definition) @__definition[table_name] ||= {} if @__definition[table_name][:definition] raise "Table `#{table_name}` already defined" end @__definition[table_name][:definition] = table_definition.__definition options.delete(:force) @__definition[table_name][:options] = options end def add_index(table_name, column_name, options = {}) table_name = table_name.to_s # Keep column_name for expression index support # https://github.com/rails/rails/pull/23393 unless column_name.is_a?(String) && /\W/ === column_name column_name = [column_name].flatten.map {|i| i.to_s } end options[:name] = options[:name].to_s if options[:name] @__definition[table_name] ||= {} @__definition[table_name][:indices] ||= {} idx = options[:name] || column_name if @__definition[table_name][:indices][idx] raise "Index `#{table_name}(#{idx})` already defined" end if options[:length].is_a?(Numeric) index_length = options[:length] options[:length] = {} column_name.each do |col| options[:length][col] = index_length end end @__definition[table_name][:indices][idx] = { :column_name => column_name, :options => options, } end def add_foreign_key(from_table, to_table, options = {}) unless options[:name] raise "Foreign key name in `#{from_table}` is undefined" end from_table = from_table.to_s to_table = to_table.to_s options[:name] = options[:name].to_s @__definition[from_table] ||= {} @__definition[from_table][:foreign_keys] ||= {} idx = options[:name] if @__definition[from_table][:foreign_keys][idx] raise "Foreign Key `#{from_table}(#{idx})` already defined" end @__definition[from_table][:foreign_keys][idx] = { :to_table => to_table, :options => options, } end def require(file) schemafile = (file =~ %r|\A/|) ? file : File.join(@__working_dir, file) if File.exist?(schemafile) instance_eval(File.read(schemafile), schemafile) elsif File.exist?(schemafile + '.rb') instance_eval(File.read(schemafile + '.rb'), schemafile + '.rb') else Kernel.require(file) end end def execute(sql, name = nil, &cond) @__execute << { :sql => sql, :condition => cond, } end end def initialize(options = {}) @options = options end def parse(dsl, opts = {}) definition, execute = Context.eval(dsl, opts) check_orphan_index(definition) check_orphan_foreign_key(definition) [definition, execute] end private def check_orphan_index(definition) definition.each do |table_name, attrs| if attrs[:indices] and not attrs[:definition] raise "Table `#{table_name}` to create the index is not defined: #{attrs[:indices].keys.join(',')}" end end end def check_orphan_foreign_key(definition) definition.each do |table_name, attrs| if attrs[:foreign_keys] and not attrs[:definition] raise "Table `#{table_name}` to create the foreign key is not defined: #{attrs[:foreign_keys].keys.join(',')}" end end end end