Skip to content

Commit

Permalink
Do a much better job of parsing defaults from the database
Browse files Browse the repository at this point in the history
Parsing defaults from the database is an ugly job, but somebody's
got to do it.  I can't say I'm happy with this commit, but it's a
big improvement and should fix most of the issues people have.
I'm sure there are still corner cases that I didn't handle, but
I'll gladly take patches to improve support.

The default parsing works by looking at both the default string
value provided by the database as well as the type of value
that Sequel has already parsed (the symbol value used for
typecasting, not the class value parsed in the schema dumper).

It runs the string through some regexps and gsubs depending on
the database type, and then converts values to an appropriate
class.

For implementation reasons, the DateTime class is always used
default values for datetime types even if it isn't the
Sequel.datetime_class.

In Schema::Generator, recognize default values that can't be
represented correctly by inspect, and make sure to generate a
useable string.
  • Loading branch information
jeremyevans committed May 21, 2009
1 parent d89c15f commit ac87ba7
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
@@ -1,5 +1,7 @@
=== HEAD

* In the schema_dumper extension, do a much better job of parsing defaults from the database (jeremyevans)

* On PostgreSQL, assume the public schema if one is not given and there is no default in Database#tables (jeremyevans)

* Ignore a :default value if creating a String :text=>true or File column on MySQL, since it doesn't support defaults on text/blob columns (jeremyevans)
Expand Down
96 changes: 80 additions & 16 deletions lib/sequel/extensions/schema_dumper.rb
@@ -1,5 +1,9 @@
module Sequel
class Database
POSTGRES_DEFAULT_RE = /\A(?:B?('.*')::[^']+|\((-?\d+(?:\.\d+)?)\))\z/
MYSQL_TIMESTAMP_RE = /\ACURRENT_(?:DATE|TIMESTAMP)?\z/
STRING_DEFAULT_RE = /\A'(.*)'\z/

# Dump indexes for all tables as a migration. This complements
# the :indexes=>false option to dump_schema_migration.
def dump_indexes_migration
Expand Down Expand Up @@ -60,25 +64,70 @@ def dump_table_schema(table, options={})
end

private

# Convert the given default, which should be a database specific string, into
# a ruby object. If it can't be converted, return the string with the inspect
# method modified so that .lit is always appended after it.
# a ruby object.
def column_schema_to_ruby_default(default, type, options)
case default
when /false/
false
when 'true'
true
when /\A\d+\z/
default.to_i
else
if options[:same_db]
def default.inspect
"#{super}.lit"
return if default.nil?
orig_default = default
if database_type == :postgres and m = POSTGRES_DEFAULT_RE.match(default)
default = m[1] || m[2]
end
if [:string, :blob, :date, :datetime, :time].include?(type)
if database_type == :mysql
if [:date, :datetime, :time].include?(type) && MYSQL_TIMESTAMP_RE.match(default)
return column_schema_to_ruby_default_fallback(default, options)
end
orig_default = default = "'#{default.gsub("'", "''").gsub('\\', '\\\\')}'"
end
if m = STRING_DEFAULT_RE.match(default)
default = m[1].gsub("''", "'")
else
return column_schema_to_ruby_default_fallback(default, options)
end
end
res = begin
case type
when :boolean
case default
when /[f0]/i
false
when /[t1]/i
true
end
when :string
default
when :blob
Sequel::SQL::Blob.new(default)
when :integer
Integer(default)
when :float
Float(default)
when :date
Sequel.string_to_date(default)
when :datetime
DateTime.parse(default)
when :time
Sequel.string_to_time(default)
when :decimal
BigDecimal.new(default)
end
rescue
nil
end
res.nil? ? column_schema_to_ruby_default_fallback(orig_default, options) : res
end

# If the database default can't be converted, return the string with the inspect
# method modified so that .lit is always appended after it, only if the
# :same_db option is used.
def column_schema_to_ruby_default_fallback(default, options)
if options[:same_db]
default = default.to_s
def default.inspect
"#{super}.lit"
end
default
end
end

Expand All @@ -91,7 +140,7 @@ def column_schema_to_generator_opts(name, schema, options)
col_opts = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
type = col_opts.delete(:type)
col_opts.delete(:size) if col_opts[:size].nil?
default = column_schema_to_ruby_default(schema[:default], type, options) if schema[:default]
default = column_schema_to_ruby_default(schema[:default], schema[:type], options) if schema[:default]
col_opts[:default] = default unless default.nil?
col_opts[:null] = false if schema[:allow_null] == false
[:column, name, type, col_opts]
Expand Down Expand Up @@ -234,7 +283,22 @@ def dump_indexes(options={})
private

def opts_inspect(opts)
", #{opts.inspect[1...-1]}" if opts.length > 0
if opts[:default]
opts = opts.dup
de = case d = opts.delete(:default)
when BigDecimal, Sequel::SQL::Blob
"#{d.class.name}.new(#{d.to_s.inspect})"
when DateTime, Date
"#{d.class.name}.parse(#{d.to_s.inspect})"
when Time
"#{d.class.name}.parse(#{d.strftime('%H:%M:%S').inspect})"
else
d.inspect
end
", :default=>#{de}#{", #{opts.inspect[1...-1]}" if opts.length > 0}"
else
", #{opts.inspect[1...-1]}" if opts.length > 0
end
end
end
end
Expand Down
89 changes: 82 additions & 7 deletions spec/extensions/schema_dumper_spec.rb
Expand Up @@ -78,11 +78,6 @@
when :t3
[[:c1, {:db_type=>'date', :default=>"'now()'", :allow_null=>true}],
[:c2, {:db_type=>'datetime', :allow_null=>false}]]
when :t4
[[:c1, {:db_type=>'boolean', :default=>"false", :allow_null=>true}],
[:c2, {:db_type=>'boolean', :default=>"true", :allow_null=>true}],
[:c3, {:db_type=>'varchar', :default=>"'blah'", :allow_null=>true}],
[:c4, {:db_type=>'integer', :default=>"35", :allow_null=>true}]]
when :t5
[[:c1, {:db_type=>'blahblah', :allow_null=>true}]]
end
Expand Down Expand Up @@ -232,9 +227,89 @@ def down
it "should handle not null values and defaults" do
@d.dump_table_schema(:t3).should == "create_table(:t3) do\n Date :c1\n DateTime :c2, :null=>false\nend"
end


it "should handle converting many default formats" do
m = @d.method(:column_schema_to_ruby_default)
m.call("adf", :string, :same_db=>true).inspect.should == '"adf".lit'
p = lambda{|d,t| m.call(d,t,{})}
p[nil, :integer].should == nil
p['1', :integer].should == 1
p['-1', :integer].should == -1
p['1.0', :float].should == 1.0
p['-1.0', :float].should == -1.0
p['1.0', :decimal].should == BigDecimal.new('1.0')
p['-1.0', :decimal].should == BigDecimal.new('-1.0')
p['1', :boolean].should == true
p['0', :boolean].should == false
p['true', :boolean].should == true
p['false', :boolean].should == false
p["'t'", :boolean].should == true
p["'f'", :boolean].should == false
p["'a'", :string].should == 'a'
p["'a'", :blob].should == 'a'.to_sequel_blob
p["'a'", :blob].should be_a_kind_of(Sequel::SQL::Blob)
p["''", :string].should == ''
p["'\\a''b'", :string].should == "\\a'b"
p["'NULL'", :string].should == "NULL"
p["'2009-10-29'", :date].should == Date.new(2009,10,29)
p["CURRENT_TIMESTAMP", :date].should == nil
p["today()", :date].should == nil
p["'2009-10-29T10:20:30-07:00'", :datetime].should == DateTime.parse('2009-10-29T10:20:30-07:00')
p["'2009-10-29 10:20:30'", :datetime].should == DateTime.parse('2009-10-29 10:20:30')
p["'10:20:30'", :time].should == Time.parse('10:20:30')
end

it "should handle converting common defaults" do
@d.dump_table_schema(:t4).should == "create_table(:t4) do\n TrueClass :c1, :default=>false\n TrueClass :c2, :default=>true\n String :c3\n Integer :c4, :default=>35\nend"
@d.meta_def(:schema) do |t, *os|
[[:c1, {:db_type=>'boolean', :default=>"false", :type=>:boolean, :allow_null=>true}],
[:c2, {:db_type=>'varchar', :default=>"'blah'", :type=>:string, :allow_null=>true}],
[:c3, {:db_type=>'integer', :default=>"-1", :type=>:integer, :allow_null=>true}],
[:c4, {:db_type=>'float', :default=>"1.0", :type=>:float, :allow_null=>true}],
[:c5, {:db_type=>'decimal', :default=>"100.50", :type=>:decimal, :allow_null=>true}],
[:c6, {:db_type=>'blob', :default=>"'blah'", :type=>:blob, :allow_null=>true}],
[:c7, {:db_type=>'date', :default=>"'2008-10-29'", :type=>:date, :allow_null=>true}],
[:c8, {:db_type=>'datetime', :default=>"'2008-10-29 10:20:30'", :type=>:datetime, :allow_null=>true}],
[:c9, {:db_type=>'time', :default=>"'10:20:30'", :type=>:time, :allow_null=>true}],
[:c10, {:db_type=>'interval', :default=>"'6 weeks'", :type=>:interval, :allow_null=>true}]]
end
@d.dump_table_schema(:t4).gsub(/[+-]\d\d:\d\d"\)/, '")').should == "create_table(:t4) do\n TrueClass :c1, :default=>false\n String :c2, :default=>\"blah\"\n Integer :c3, :default=>-1\n Float :c4, :default=>1.0\n BigDecimal :c5, :default=>BigDecimal.new(\"0.1005E3\")\n File :c6, :default=>Sequel::SQL::Blob.new(\"blah\")\n Date :c7, :default=>Date.parse(\"2008-10-29\")\n DateTime :c8, :default=>DateTime.parse(\"2008-10-29T10:20:30\")\n Time :c9, :default=>Time.parse(\"10:20:30\"), :only_time=>true\n String :c10\nend"
@d.dump_table_schema(:t4, :same_db=>true).gsub(/[+-]\d\d:\d\d"\)/, '")').should == "create_table(:t4) do\n column :c1, \"boolean\", :default=>false\n column :c2, \"varchar\", :default=>\"blah\"\n column :c3, \"integer\", :default=>-1\n column :c4, \"float\", :default=>1.0\n column :c5, \"decimal\", :default=>BigDecimal.new(\"0.1005E3\")\n column :c6, \"blob\", :default=>Sequel::SQL::Blob.new(\"blah\")\n column :c7, \"date\", :default=>Date.parse(\"2008-10-29\")\n column :c8, \"datetime\", :default=>DateTime.parse(\"2008-10-29T10:20:30\")\n column :c9, \"time\", :default=>Time.parse(\"10:20:30\")\n column :c10, \"interval\", :default=>\"'6 weeks'\".lit\nend"
end

it "should handle converting PostgreSQL specific default formats" do
m = @d.method(:column_schema_to_ruby_default)
@d.meta_def(:database_type){:postgres}
p = lambda{|d,t| m.call(d,t,{})}
p["''::text", :string].should == ""
p["'\\a''b'::character varying", :string].should == "\\a'b"
p["'a'::bpchar", :string].should == "a"
p["(-1)", :integer].should == -1
p["(-1.0)", :float].should == -1.0
p['(-1.0)', :decimal].should == BigDecimal.new('-1.0')
p["'a'::bytea", :blob].should == 'a'.to_sequel_blob
p["'a'::bytea", :blob].should be_a_kind_of(Sequel::SQL::Blob)
p["'2009-10-29'::date", :date].should == Date.new(2009,10,29)
p["'2009-10-29 10:20:30.241343'::timestamp without time zone", :datetime].should == DateTime.parse('2009-10-29 10:20:30.241343')
p["'10:20:30'::time without time zone", :time].should == Time.parse('10:20:30')
end

it "should handle converting MySQL specific default formats" do
m = @d.method(:column_schema_to_ruby_default)
@d.meta_def(:database_type){:mysql}
p = lambda{|d,t| m.call(d,t,{})}
s = lambda{|d,t| m.call(d,t,{:same_db=>true})}
p["\\a'b", :string].should == "\\a'b"
p["a", :string].should == "a"
p["NULL", :string].should == "NULL"
p["-1", :float].should == -1.0
p['-1', :decimal].should == BigDecimal.new('-1.0')
p["2009-10-29", :date].should == Date.new(2009,10,29)
p["2009-10-29 10:20:30", :datetime].should == DateTime.parse('2009-10-29 10:20:30')
p["10:20:30", :time].should == Time.parse('10:20:30')
p["CURRENT_DATE", :date].should == nil
p["CURRENT_TIMESTAMP", :datetime].should == nil
s["CURRENT_DATE", :date].inspect.should == "\"CURRENT_DATE\".lit"
s["CURRENT_TIMESTAMP", :datetime].inspect.should == "\"CURRENT_TIMESTAMP\".lit"
end

it "should convert unknown database types to strings" do
Expand Down

0 comments on commit ac87ba7

Please sign in to comment.