sequel 3.33.0 → 3.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. data/CHANGELOG +140 -0
  2. data/Rakefile +7 -0
  3. data/bin/sequel +22 -2
  4. data/doc/dataset_basics.rdoc +1 -1
  5. data/doc/mass_assignment.rdoc +3 -1
  6. data/doc/querying.rdoc +28 -4
  7. data/doc/reflection.rdoc +23 -3
  8. data/doc/release_notes/3.34.0.txt +671 -0
  9. data/doc/schema_modification.rdoc +18 -2
  10. data/doc/virtual_rows.rdoc +49 -0
  11. data/lib/sequel/adapters/do/mysql.rb +0 -5
  12. data/lib/sequel/adapters/ibmdb.rb +9 -4
  13. data/lib/sequel/adapters/jdbc.rb +9 -4
  14. data/lib/sequel/adapters/jdbc/h2.rb +8 -2
  15. data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
  16. data/lib/sequel/adapters/jdbc/postgresql.rb +43 -0
  17. data/lib/sequel/adapters/jdbc/sqlite.rb +19 -0
  18. data/lib/sequel/adapters/mock.rb +24 -3
  19. data/lib/sequel/adapters/mysql.rb +29 -50
  20. data/lib/sequel/adapters/mysql2.rb +13 -28
  21. data/lib/sequel/adapters/oracle.rb +8 -2
  22. data/lib/sequel/adapters/postgres.rb +115 -20
  23. data/lib/sequel/adapters/shared/db2.rb +1 -1
  24. data/lib/sequel/adapters/shared/mssql.rb +14 -3
  25. data/lib/sequel/adapters/shared/mysql.rb +59 -11
  26. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
  27. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  28. data/lib/sequel/adapters/shared/postgres.rb +127 -30
  29. data/lib/sequel/adapters/shared/sqlite.rb +55 -38
  30. data/lib/sequel/adapters/sqlite.rb +9 -3
  31. data/lib/sequel/adapters/swift.rb +2 -2
  32. data/lib/sequel/adapters/swift/mysql.rb +0 -5
  33. data/lib/sequel/adapters/swift/postgres.rb +10 -0
  34. data/lib/sequel/ast_transformer.rb +4 -0
  35. data/lib/sequel/connection_pool.rb +8 -0
  36. data/lib/sequel/connection_pool/sharded_single.rb +5 -0
  37. data/lib/sequel/connection_pool/sharded_threaded.rb +17 -0
  38. data/lib/sequel/connection_pool/single.rb +5 -0
  39. data/lib/sequel/connection_pool/threaded.rb +14 -0
  40. data/lib/sequel/core.rb +24 -3
  41. data/lib/sequel/database/connecting.rb +24 -14
  42. data/lib/sequel/database/dataset_defaults.rb +1 -0
  43. data/lib/sequel/database/misc.rb +16 -25
  44. data/lib/sequel/database/query.rb +20 -2
  45. data/lib/sequel/database/schema_generator.rb +2 -2
  46. data/lib/sequel/database/schema_methods.rb +120 -23
  47. data/lib/sequel/dataset/actions.rb +91 -18
  48. data/lib/sequel/dataset/features.rb +5 -0
  49. data/lib/sequel/dataset/prepared_statements.rb +6 -2
  50. data/lib/sequel/dataset/sql.rb +68 -51
  51. data/lib/sequel/extensions/_pretty_table.rb +79 -0
  52. data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +18 -13
  53. data/lib/sequel/extensions/migration.rb +4 -0
  54. data/lib/sequel/extensions/null_dataset.rb +90 -0
  55. data/lib/sequel/extensions/pg_array.rb +460 -0
  56. data/lib/sequel/extensions/pg_array_ops.rb +220 -0
  57. data/lib/sequel/extensions/pg_auto_parameterize.rb +174 -0
  58. data/lib/sequel/extensions/pg_hstore.rb +296 -0
  59. data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
  60. data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
  61. data/lib/sequel/extensions/pretty_table.rb +5 -71
  62. data/lib/sequel/extensions/query_literals.rb +79 -0
  63. data/lib/sequel/extensions/schema_caching.rb +76 -0
  64. data/lib/sequel/extensions/schema_dumper.rb +227 -31
  65. data/lib/sequel/extensions/select_remove.rb +35 -0
  66. data/lib/sequel/extensions/sql_expr.rb +4 -110
  67. data/lib/sequel/extensions/to_dot.rb +1 -1
  68. data/lib/sequel/model.rb +11 -2
  69. data/lib/sequel/model/associations.rb +35 -7
  70. data/lib/sequel/model/base.rb +159 -36
  71. data/lib/sequel/no_core_ext.rb +2 -0
  72. data/lib/sequel/plugins/caching.rb +25 -18
  73. data/lib/sequel/plugins/composition.rb +1 -1
  74. data/lib/sequel/plugins/hook_class_methods.rb +1 -1
  75. data/lib/sequel/plugins/identity_map.rb +11 -3
  76. data/lib/sequel/plugins/instance_filters.rb +10 -0
  77. data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
  78. data/lib/sequel/plugins/nested_attributes.rb +4 -3
  79. data/lib/sequel/plugins/prepared_statements.rb +3 -1
  80. data/lib/sequel/plugins/prepared_statements_associations.rb +5 -1
  81. data/lib/sequel/plugins/schema.rb +7 -2
  82. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  83. data/lib/sequel/plugins/static_cache.rb +99 -0
  84. data/lib/sequel/plugins/validation_class_methods.rb +1 -1
  85. data/lib/sequel/sql.rb +417 -7
  86. data/lib/sequel/version.rb +1 -1
  87. data/spec/adapters/firebird_spec.rb +1 -1
  88. data/spec/adapters/mssql_spec.rb +12 -15
  89. data/spec/adapters/mysql_spec.rb +81 -23
  90. data/spec/adapters/postgres_spec.rb +444 -77
  91. data/spec/adapters/spec_helper.rb +2 -0
  92. data/spec/adapters/sqlite_spec.rb +8 -8
  93. data/spec/core/connection_pool_spec.rb +85 -0
  94. data/spec/core/database_spec.rb +29 -5
  95. data/spec/core/dataset_spec.rb +171 -3
  96. data/spec/core/expression_filters_spec.rb +364 -0
  97. data/spec/core/mock_adapter_spec.rb +17 -3
  98. data/spec/core/schema_spec.rb +133 -0
  99. data/spec/extensions/association_dependencies_spec.rb +13 -13
  100. data/spec/extensions/caching_spec.rb +26 -3
  101. data/spec/extensions/class_table_inheritance_spec.rb +2 -2
  102. data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +23 -94
  103. data/spec/extensions/force_encoding_spec.rb +4 -2
  104. data/spec/extensions/hook_class_methods_spec.rb +5 -2
  105. data/spec/extensions/identity_map_spec.rb +17 -0
  106. data/spec/extensions/instance_filters_spec.rb +1 -1
  107. data/spec/extensions/lazy_attributes_spec.rb +2 -2
  108. data/spec/extensions/list_spec.rb +4 -4
  109. data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
  110. data/spec/extensions/migration_spec.rb +6 -2
  111. data/spec/extensions/nested_attributes_spec.rb +20 -0
  112. data/spec/extensions/null_dataset_spec.rb +85 -0
  113. data/spec/extensions/optimistic_locking_spec.rb +2 -2
  114. data/spec/extensions/pg_array_ops_spec.rb +105 -0
  115. data/spec/extensions/pg_array_spec.rb +196 -0
  116. data/spec/extensions/pg_auto_parameterize_spec.rb +64 -0
  117. data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
  118. data/spec/extensions/pg_hstore_spec.rb +195 -0
  119. data/spec/extensions/pg_statement_cache_spec.rb +209 -0
  120. data/spec/extensions/prepared_statements_spec.rb +4 -0
  121. data/spec/extensions/pretty_table_spec.rb +6 -0
  122. data/spec/extensions/query_literals_spec.rb +168 -0
  123. data/spec/extensions/schema_caching_spec.rb +41 -0
  124. data/spec/extensions/schema_dumper_spec.rb +231 -11
  125. data/spec/extensions/schema_spec.rb +14 -2
  126. data/spec/extensions/select_remove_spec.rb +38 -0
  127. data/spec/extensions/sharding_spec.rb +6 -6
  128. data/spec/extensions/skip_create_refresh_spec.rb +1 -1
  129. data/spec/extensions/spec_helper.rb +2 -1
  130. data/spec/extensions/sql_expr_spec.rb +28 -19
  131. data/spec/extensions/static_cache_spec.rb +145 -0
  132. data/spec/extensions/touch_spec.rb +1 -1
  133. data/spec/extensions/typecast_on_load_spec.rb +9 -1
  134. data/spec/integration/associations_test.rb +6 -6
  135. data/spec/integration/database_test.rb +1 -1
  136. data/spec/integration/dataset_test.rb +89 -26
  137. data/spec/integration/migrator_test.rb +2 -3
  138. data/spec/integration/model_test.rb +3 -3
  139. data/spec/integration/plugin_test.rb +85 -22
  140. data/spec/integration/prepared_statement_test.rb +28 -8
  141. data/spec/integration/schema_test.rb +78 -7
  142. data/spec/integration/spec_helper.rb +1 -0
  143. data/spec/integration/timezone_test.rb +1 -1
  144. data/spec/integration/transaction_test.rb +4 -6
  145. data/spec/integration/type_test.rb +2 -2
  146. data/spec/model/associations_spec.rb +94 -8
  147. data/spec/model/base_spec.rb +4 -4
  148. data/spec/model/hooks_spec.rb +2 -2
  149. data/spec/model/model_spec.rb +19 -7
  150. data/spec/model/record_spec.rb +135 -58
  151. data/spec/model/spec_helper.rb +1 -0
  152. metadata +35 -7
@@ -3,80 +3,14 @@
3
3
  # tables.
4
4
 
5
5
  module Sequel
6
+ extension :_pretty_table
7
+
6
8
  class Dataset
7
9
  # Pretty prints the records in the dataset as plain-text table.
8
10
  def print(*cols)
9
- Sequel::PrettyTable.print(naked.all, cols.empty? ? columns : cols)
11
+ ds = naked
12
+ rows = ds.all
13
+ Sequel::PrettyTable.print(rows, cols.empty? ? ds.columns : cols)
10
14
  end
11
15
  end
12
-
13
- module PrettyTable
14
- # Prints nice-looking plain-text tables via puts
15
- #
16
- # +--+-------+
17
- # |id|name |
18
- # |--+-------|
19
- # |1 |fasdfas|
20
- # |2 |test |
21
- # +--+-------+
22
- def self.print(records, columns = nil) # records is an array of hashes
23
- columns ||= records.first.keys.sort_by{|x|x.to_s}
24
- sizes = column_sizes(records, columns)
25
- sep_line = separator_line(columns, sizes)
26
-
27
- puts sep_line
28
- puts header_line(columns, sizes)
29
- puts sep_line
30
- records.each {|r| puts data_line(columns, sizes, r)}
31
- puts sep_line
32
- end
33
-
34
- ### Private Module Methods ###
35
-
36
- # Hash of the maximum size of the value for each column
37
- def self.column_sizes(records, columns) # :nodoc:
38
- sizes = Hash.new {0}
39
- columns.each do |c|
40
- s = c.to_s.size
41
- sizes[c.to_sym] = s if s > sizes[c.to_sym]
42
- end
43
- records.each do |r|
44
- columns.each do |c|
45
- s = r[c].to_s.size
46
- sizes[c.to_sym] = s if s > sizes[c.to_sym]
47
- end
48
- end
49
- sizes
50
- end
51
-
52
- # String for each data line
53
- def self.data_line(columns, sizes, record) # :nodoc:
54
- '|' << columns.map {|c| format_cell(sizes[c], record[c])}.join('|') << '|'
55
- end
56
-
57
- # Format the value so it takes up exactly size characters
58
- def self.format_cell(size, v) # :nodoc:
59
- case v
60
- when Bignum, Fixnum
61
- "%#{size}d" % v
62
- when Float
63
- "%#{size}g" % v
64
- else
65
- "%-#{size}s" % v.to_s
66
- end
67
- end
68
-
69
- # String for header line
70
- def self.header_line(columns, sizes) # :nodoc:
71
- '|' << columns.map {|c| "%-#{sizes[c]}s" % c.to_s}.join('|') << '|'
72
- end
73
-
74
- # String for separtor line
75
- def self.separator_line(columns, sizes) # :nodoc:
76
- '+' << columns.map {|c| '-' * sizes[c]}.join('+') << '+'
77
- end
78
-
79
- private_class_method :column_sizes, :data_line, :format_cell, :header_line, :separator_line
80
- end
81
16
  end
82
-
@@ -0,0 +1,79 @@
1
+ # The query_literals extension changes Sequel's default behavior of
2
+ # the select, order and group methods so that if the first argument
3
+ # is a regular string, it is treated as a literal string, with the
4
+ # rest of the arguments (if any) treated as placeholder values. This
5
+ # allows you to write code such as:
6
+ #
7
+ # DB[:table].select('a, b, ?', 2).group('a, b').order('c')
8
+ #
9
+ # The default Sequel behavior would literalize that as:
10
+ #
11
+ # SELECT 'a, b, ?', 2 FROM table GROUP BY 'a, b' ORDER BY 'c'
12
+ #
13
+ # Using this extension changes the literalization to:
14
+ #
15
+ # SELECT a, b, 2, FROM table GROUP BY a, b ORDER BY c
16
+ #
17
+ # This extension makes select, group, and order methods operate
18
+ # like filter methods, which support the same interface.
19
+ #
20
+ # There are very few places where Sequel's default behavior is
21
+ # desirable in this area, but for backwards compatibility, the
22
+ # defaults won't be changed until the next major release.
23
+ #
24
+ # Loading this extension does nothing by default except make the
25
+ # Sequel::QueryLiterals module available. You can extend specific
26
+ # datasets with this module:
27
+ #
28
+ # ds = DB[:table]
29
+ # ds.extend(Sequel::QueryLiterals)
30
+ #
31
+ # Order you can extend all of a database's datasets with it, which
32
+ # is probably the desired behavior if you are using this extension:
33
+ #
34
+ # DB.extend_datasets(Sequel::QueryLiterals)
35
+
36
+ module Sequel
37
+ # The QueryLiterals module can be used to make select, group, and
38
+ # order methods operate similar to the filter methods if the first
39
+ # argument is a plain string, treating it like a literal string,
40
+ # with any remaining arguments treated as placeholder values.
41
+ #
42
+ # This adds such support to the following methods: select, select_append,
43
+ # select_group, select_more, group, group_and_count, order, order_append,
44
+ # and order_more.
45
+ #
46
+ # Note that if you pass a block to these methods, it will use the default
47
+ # implementation without the special literal handling.
48
+ module QueryLiterals
49
+ %w'select select_append select_group select_more group group_and_count order order_append order_more'.each do |m|
50
+ class_eval(<<-END, __FILE__, __LINE__ + 1)
51
+ def #{m}(*args)
52
+ if !block_given? && (l = query_literal(args))
53
+ super(l)
54
+ else
55
+ super
56
+ end
57
+ end
58
+ END
59
+ end
60
+
61
+ private
62
+
63
+ # If the first argument is a plain string, return a literal string
64
+ # if there are no additional args or a placeholder literal string with
65
+ # the remaining args. Otherwise, return nil.
66
+ def query_literal(args)
67
+ case (s = args[0])
68
+ when LiteralString, SQL::Blob
69
+ nil
70
+ when String
71
+ if args.length == 1
72
+ LiteralString.new(s)
73
+ else
74
+ SQL::PlaceholderLiteralString.new(s, args[1..-1])
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,76 @@
1
+ # The schema_caching extension adds a few methods to Sequel::Database
2
+ # that make it easy to dump the parsed schema information to a file,
3
+ # and load it from that file. Loading the schema information from a
4
+ # dumped file is faster than parsing it from the database, so this
5
+ # can save bootup time for applications with large numbers of models.
6
+ #
7
+ # Basic usage in application code:
8
+ #
9
+ # Sequel.extension :schema_caching
10
+ #
11
+ # DB = Sequel.connect('...')
12
+ #
13
+ # DB.load_schema_cache('/path/to/schema.dump')
14
+ #
15
+ # # load model files
16
+ #
17
+ # Then, whenever the database schema is modified, write a new cached
18
+ # file. You can do that with <tt>bin/sequel</tt>'s -S option:
19
+ #
20
+ # bin/sequel -S /path/to/schema.dump postgres://...
21
+ #
22
+ # Alternatively, if you don't want to dump the schema information for
23
+ # all tables, and you don't worry about race conditions, you can
24
+ # choose to use the following in your application code:
25
+ #
26
+ # Sequel.extension :schema_caching
27
+ #
28
+ # DB = Sequel.connect('...')
29
+ #
30
+ # DB.load_schema_cache?('/path/to/schema.dump')
31
+ #
32
+ # # load model files
33
+ #
34
+ # DB.dump_schema_cache?('/path/to/schema.dump')
35
+ #
36
+ # With this method, you just have to delete the schema dump file if
37
+ # the schema is modified, and the application will recreate it for you
38
+ # using just the tables that your models use.
39
+ #
40
+ # Note that it is up to the application to ensure that the dumped
41
+ # cached schema reflects the current state of the database. Sequel
42
+ # does no checking to ensure this, as checking would take time and the
43
+ # purpose of this code is to take a shortcut.
44
+ #
45
+ # The cached schema is dumped in Marshal format, since it is the fastest
46
+ # and it handles all ruby objects used in the schema hash. Because of this,
47
+ # you should not attempt to load the schema from a untrusted file.
48
+
49
+ module Sequel
50
+ class Database
51
+ # Dump the cached schema to the filename given in Marshal format.
52
+ def dump_schema_cache(file)
53
+ File.open(file, 'wb'){|f| f.write(Marshal.dump(@schemas))}
54
+ nil
55
+ end
56
+
57
+ # Dump the cached schema to the filename given unless the file
58
+ # already exists.
59
+ def dump_schema_cache?(file)
60
+ dump_schema_cache(file) unless File.exist?(file)
61
+ end
62
+
63
+ # Replace the schema cache with the data from the given file, which
64
+ # should be in Marshal format.
65
+ def load_schema_cache(file)
66
+ @schemas = Marshal.load(File.read(file))
67
+ nil
68
+ end
69
+
70
+ # Replace the schema cache with the data from the given file if the
71
+ # file exists.
72
+ def load_schema_cache?(file)
73
+ load_schema_cache(file) if File.exist?(file)
74
+ end
75
+ end
76
+ end
@@ -6,6 +6,24 @@
6
6
 
7
7
  module Sequel
8
8
  class Database
9
+ # Dump foreign key constraints for all tables as a migration. This complements
10
+ # the :foreign_keys=>false option to dump_schema_migration. This only dumps
11
+ # the constraints (not the columns) using alter_table/add_foreign_key with an
12
+ # array of columns.
13
+ #
14
+ # Note that the migration this produces does not have a down
15
+ # block, so you cannot reverse it.
16
+ def dump_foreign_key_migration(options={})
17
+ ts = tables(options)
18
+ <<END_MIG
19
+ Sequel.migration do
20
+ up do
21
+ #{ts.sort_by{|t| t.to_s}.map{|t| dump_table_foreign_keys(t)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
22
+ end
23
+ end
24
+ END_MIG
25
+ end
26
+
9
27
  # Dump indexes for all tables as a migration. This complements
10
28
  # the :indexes=>false option to dump_schema_migration. Options:
11
29
  # * :same_db - Create a dump for the same database type, so
@@ -19,7 +37,7 @@ Sequel.migration do
19
37
  end
20
38
 
21
39
  down do
22
- #{ts.sort_by{|t| t.to_s}.map{|t| dump_table_indexes(t, :drop_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
40
+ #{ts.sort_by{|t| t.to_s}.reverse.map{|t| dump_table_indexes(t, :drop_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
23
41
  end
24
42
  end
25
43
  END_MIG
@@ -32,18 +50,35 @@ END_MIG
32
50
  # ruby types, but there is no guarantee that the migration generated
33
51
  # will yield the same type. Without this set, types that aren't
34
52
  # recognized will be translated to a string-like type.
53
+ # * :foreign_keys - If set to false, don't dump foreign_keys
35
54
  # * :indexes - If set to false, don't dump indexes (they can be added
36
55
  # later via dump_index_migration).
37
56
  def dump_schema_migration(options={})
38
- ts = tables(options)
57
+ options = options.dup
58
+ if options[:indexes] == false && !options.has_key?(:foreign_keys)
59
+ # Unless foreign_keys option is specifically set, disable if indexes
60
+ # are disabled, as foreign keys that point to non-primary keys rely
61
+ # on unique indexes being created first
62
+ options[:foreign_keys] = false
63
+ end
64
+
65
+ ts = sort_dumped_tables(tables(options), options)
66
+ skipped_fks = if sfk = options[:skipped_foreign_keys]
67
+ # Handle skipped foreign keys by adding them at the end via
68
+ # alter_table/add_foreign_key. Note that skipped foreign keys
69
+ # probably result in a broken down migration.
70
+ sfka = sfk.sort_by{|table, fks| table.to_s}.map{|table, fks| dump_add_fk_constraints(table, fks.values)}
71
+ sfka.join("\n\n").gsub(/^/o, ' ') unless sfka.empty?
72
+ end
73
+
39
74
  <<END_MIG
40
75
  Sequel.migration do
41
76
  up do
42
- #{ts.sort_by{|t| t.to_s}.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, ' ')}
77
+ #{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, ' ')}#{"\n \n" if skipped_fks}#{skipped_fks}
43
78
  end
44
79
 
45
80
  down do
46
- drop_table(#{ts.sort_by{|t| t.to_s}.inspect[1...-1]})
81
+ drop_table(#{ts.reverse.inspect[1...-1]})
47
82
  end
48
83
  end
49
84
  END_MIG
@@ -53,24 +88,9 @@ END_MIG
53
88
  # table's schema. Takes the same options as dump_schema_migration.
54
89
  def dump_table_schema(table, options={})
55
90
  table = table.value.to_s if table.is_a?(SQL::Identifier)
56
- raise(Error, "must provide table as a Symbol, String, or Sequel::SQL::Identifier") unless [String, Symbol].any?{|c| table.is_a?(c)}
57
- s = schema(table).dup
58
- pks = s.find_all{|x| x.last[:primary_key] == true}.map{|x| x.first}
59
- options = options.merge(:single_pk=>true) if pks.length == 1
60
- m = method(:column_schema_to_generator_opts)
61
- im = method(:index_to_generator_opts)
62
- begin
63
- indexes = indexes(table).sort_by{|k,v| k.to_s} if options[:indexes] != false
64
- rescue Sequel::NotImplemented
65
- nil
66
- end
67
- gen = Schema::Generator.new(self) do
68
- s.each{|name, info| send(*m.call(name, info, options))}
69
- primary_key(pks) if !@primary_key && pks.length > 0
70
- indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} if indexes
71
- end
91
+ gen = dump_table_generator(table, options)
72
92
  commands = [gen.dump_columns, gen.dump_constraints, gen.dump_indexes].reject{|x| x == ''}.join("\n\n")
73
- "create_table(#{table.inspect}#{', :ignore_index_errors=>true' if !options[:same_db] && options[:indexes] != false && indexes && !indexes.empty?}) do\n#{commands.gsub(/^/o, ' ')}\nend"
93
+ "create_table(#{table.inspect}#{', :ignore_index_errors=>true' if !options[:same_db] && options[:indexes] != false && !gen.indexes.empty?}) do\n#{commands.gsub(/^/o, ' ')}\nend"
74
94
  end
75
95
 
76
96
  private
@@ -82,7 +102,7 @@ END_MIG
82
102
  if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback?
83
103
  default = default.to_s
84
104
  def default.inspect
85
- "#{super}.lit"
105
+ "#{super}.lit" # core_sql use
86
106
  end
87
107
  default
88
108
  end
@@ -93,6 +113,7 @@ END_MIG
93
113
  def column_schema_to_generator_opts(name, schema, options)
94
114
  if options[:single_pk] && schema_autoincrementing_primary_key?(schema)
95
115
  type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
116
+ [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]}
96
117
  if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"}
97
118
  [:primary_key, name]
98
119
  else
@@ -109,7 +130,12 @@ END_MIG
109
130
  end
110
131
  col_opts.delete(:default) if col_opts[:default].nil?
111
132
  col_opts[:null] = false if schema[:allow_null] == false
112
- [:column, name, type, col_opts]
133
+ if table = schema[:table]
134
+ [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]}
135
+ [:foreign_key, name, table, col_opts]
136
+ else
137
+ [:column, name, type, col_opts]
138
+ end
113
139
  end
114
140
  end
115
141
 
@@ -120,7 +146,7 @@ END_MIG
120
146
  case t = schema[:db_type].downcase
121
147
  when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?(?: unsigned)?\z/o
122
148
  {:type=>Integer}
123
- when /\Atinyint(?:\((\d+)\))?\z/o
149
+ when /\Atinyint(?:\((\d+)\))?(?: unsigned)?\z/o
124
150
  {:type =>schema[:type] == :boolean ? TrueClass : Integer}
125
151
  when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/o
126
152
  {:type=>Bignum}
@@ -156,12 +182,99 @@ END_MIG
156
182
  end
157
183
  end
158
184
 
185
+ # For the table and foreign key metadata array, return an alter_table
186
+ # string that would add the foreign keys if run in a migration.
187
+ def dump_add_fk_constraints(table, fks)
188
+ sfks = "alter_table(#{table.inspect}) do\n"
189
+ sfks << Schema::Generator.new(self) do
190
+ fks.sort_by{|fk| fk[:columns].map{|c| c.to_s}}.each do |fk|
191
+ foreign_key fk[:columns], fk
192
+ end
193
+ end.dump_constraints.gsub(/^foreign_key /, ' add_foreign_key ')
194
+ sfks << "\nend"
195
+ end
196
+
197
+ # For the table given, get the list of foreign keys and return an alter_table
198
+ # string that would add the foreign keys if run in a migration.
199
+ def dump_table_foreign_keys(table, options={})
200
+ begin
201
+ fks = foreign_key_list(table, options).sort_by{|fk| fk[:columns].map{|c| c.to_s}}
202
+ rescue Sequel::NotImplemented
203
+ return ''
204
+ end
205
+
206
+ if fks.empty?
207
+ ''
208
+ else
209
+ dump_add_fk_constraints(table, fks)
210
+ end
211
+ end
212
+
213
+ # Return a Schema::Generator object that will recreate the
214
+ # table's schema. Takes the same options as dump_schema_migration.
215
+ def dump_table_generator(table, options={})
216
+ table = table.value.to_s if table.is_a?(SQL::Identifier)
217
+ raise(Error, "must provide table as a Symbol, String, or Sequel::SQL::Identifier") unless [String, Symbol].any?{|c| table.is_a?(c)}
218
+ s = schema(table).dup
219
+ pks = s.find_all{|x| x.last[:primary_key] == true}.map{|x| x.first}
220
+ options = options.merge(:single_pk=>true) if pks.length == 1
221
+ m = method(:column_schema_to_generator_opts)
222
+ im = method(:index_to_generator_opts)
223
+
224
+ if options[:indexes] != false
225
+ begin
226
+ indexes = indexes(table).sort_by{|k,v| k.to_s}
227
+ rescue Sequel::NotImplemented
228
+ nil
229
+ end
230
+ end
231
+
232
+ if options[:foreign_keys] != false
233
+ begin
234
+ fk_list = foreign_key_list(table)
235
+
236
+ if (sfk = options[:skipped_foreign_keys]) && (sfkt = sfk[table])
237
+ fk_list.delete_if{|fk| sfkt.has_key?(fk[:columns])}
238
+ end
239
+
240
+ composite_fks, single_fks = fk_list.partition{|h| h[:columns].length > 1}
241
+ fk_hash = {}
242
+
243
+ single_fks.each do |fk|
244
+ column = fk.delete(:columns).first
245
+ fk.delete(:name)
246
+ fk_hash[column] = fk
247
+ end
248
+
249
+ s = s.map do |name, info|
250
+ if fk_info = fk_hash[name]
251
+ [name, fk_info.merge(info)]
252
+ else
253
+ [name, info]
254
+ end
255
+ end
256
+ rescue Sequel::NotImplemented
257
+ nil
258
+ end
259
+ end
260
+
261
+ Schema::Generator.new(self) do
262
+ s.each{|name, info| send(*m.call(name, info, options))}
263
+ primary_key(pks) if !@primary_key && pks.length > 0
264
+ indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} if indexes
265
+ composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks
266
+ end
267
+ end
268
+
159
269
  # Return a string that containing add_index/drop_index method calls for
160
270
  # creating the index migration.
161
271
  def dump_table_indexes(table, meth, options={})
162
- return '' unless respond_to?(:indexes)
272
+ begin
273
+ indexes = indexes(table).sort_by{|k,v| k.to_s}
274
+ rescue Sequel::NotImplemented
275
+ return ''
276
+ end
163
277
  im = method(:index_to_generator_opts)
164
- indexes = indexes(table).sort_by{|k,v| k.to_s}
165
278
  gen = Schema::Generator.new(self) do
166
279
  indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))}
167
280
  end
@@ -175,6 +288,70 @@ END_MIG
175
288
  h[:unique] = true if index_opts[:unique]
176
289
  h
177
290
  end
291
+
292
+ # Sort the tables so that referenced tables are created before tables that
293
+ # reference them, and then by name. If foreign keys are disabled, just sort by name.
294
+ def sort_dumped_tables(tables, options={})
295
+ sort_topologically = if options[:foreign_keys] != false
296
+ begin
297
+ foreign_key_list(:some_table_that_does_not_exist)
298
+ true
299
+ rescue Sequel::NotImplemented
300
+ false
301
+ rescue
302
+ true
303
+ end
304
+ end
305
+
306
+ if sort_topologically
307
+ table_fks = {}
308
+ tables.each{|t| table_fks[t] = foreign_key_list(t)}
309
+ # Remove self referential foreign keys, not important when sorting.
310
+ table_fks.each{|t, fks| fks.delete_if{|fk| fk[:table] == t}}
311
+ tables, skipped_foreign_keys = sort_dumped_tables_topologically(table_fks, [])
312
+ options[:skipped_foreign_keys] = skipped_foreign_keys
313
+ tables
314
+ else
315
+ tables.sort_by{|t| t.to_s}
316
+ end
317
+ end
318
+
319
+ # Do a topological sort of tables, so that referenced tables
320
+ # come before referencing tables. Returns an array of sorted
321
+ # tables and a hash of skipped foreign keys. The hash will be
322
+ # empty unless there are circular dependencies.
323
+ def sort_dumped_tables_topologically(table_fks, sorted_tables)
324
+ skipped_foreign_keys = {}
325
+
326
+ until table_fks.empty?
327
+ this_loop = []
328
+
329
+ table_fks.each do |table, fks|
330
+ fks.delete_if{|fk| !table_fks.has_key?(fk[:table])}
331
+ this_loop << table if fks.empty?
332
+ end
333
+
334
+ if this_loop.empty?
335
+ # No tables were changed this round, there must be a circular dependency.
336
+ # Break circular dependency by picking the table with the least number of
337
+ # outstanding foreign keys and skipping those foreign keys.
338
+ # The skipped foreign keys will be added at the end of the
339
+ # migration.
340
+ skip_table, skip_fks = table_fks.sort_by{|table, fks| [fks.length, table.to_s]}.first
341
+ skip_fks_hash = skipped_foreign_keys[skip_table] = {}
342
+ skip_fks.each{|fk| skip_fks_hash[fk[:columns]] = fk}
343
+ this_loop << skip_table
344
+ end
345
+
346
+ # Add sorted tables from this loop to the final list
347
+ sorted_tables.concat(this_loop.sort_by{|t| t.to_s})
348
+
349
+ # Remove tables that were handled this loop
350
+ this_loop.each{|t| table_fks.delete(t)}
351
+ end
352
+
353
+ [sorted_tables, skipped_foreign_keys]
354
+ end
178
355
 
179
356
  # Don't use the "...".lit fallback on MySQL, since the defaults it uses aren't
180
357
  # valid literal SQL values.
@@ -190,6 +367,10 @@ END_MIG
190
367
  def dump_columns
191
368
  strings = []
192
369
  cols = columns.dup
370
+ cols.each do |x|
371
+ x.delete(:on_delete) if x[:on_delete] == :no_action
372
+ x.delete(:on_update) if x[:on_update] == :no_action
373
+ end
193
374
  if pkn = primary_key_name
194
375
  cols.delete_if{|x| x[:name] == pkn}
195
376
  pk = @primary_key.dup
@@ -200,12 +381,17 @@ END_MIG
200
381
  cols.each do |c|
201
382
  c = c.dup
202
383
  name = c.delete(:name)
203
- type = c.delete(:type)
204
- opts = opts_inspect(c)
205
- strings << if type.is_a?(Class)
206
- "#{type.name} #{name.inspect}#{opts}"
384
+ strings << if table = c.delete(:table)
385
+ c.delete(:type) if c[:type] == Integer || c[:type] == 'integer'
386
+ "foreign_key #{name.inspect}, #{table.inspect}#{opts_inspect(c)}"
207
387
  else
208
- "column #{name.inspect}, #{type.inspect}#{opts}"
388
+ type = c.delete(:type)
389
+ opts = opts_inspect(c)
390
+ if type.is_a?(Class)
391
+ "#{type.name} #{name.inspect}#{opts}"
392
+ else
393
+ "column #{name.inspect}, #{type.inspect}#{opts}"
394
+ end
209
395
  end
210
396
  end
211
397
  strings.join("\n")
@@ -226,6 +412,13 @@ END_MIG
226
412
  else
227
413
  "#{name ? "constraint #{name.inspect}," : 'check'} #{c[:check].map{|x| x.inspect}.join(', ')}"
228
414
  end
415
+ when :foreign_key
416
+ c.delete(:on_delete) if c[:on_delete] == :no_action
417
+ c.delete(:on_update) if c[:on_update] == :no_action
418
+ c.delete(:deferrable) unless c[:deferrable]
419
+ cols = c.delete(:columns)
420
+ table = c.delete(:table)
421
+ "#{type} #{cols.inspect}, #{table.inspect}#{opts_inspect(c)}"
229
422
  else
230
423
  cols = c.delete(:columns)
231
424
  "#{type} #{cols.inspect}#{opts_inspect(c)}"
@@ -256,6 +449,9 @@ END_MIG
256
449
 
257
450
  private
258
451
 
452
+ # Return a string that converts the given options into one
453
+ # suitable for literal ruby code, handling default values
454
+ # that don't default to a literal interpretation.
259
455
  def opts_inspect(opts)
260
456
  if opts[:default]
261
457
  opts = opts.dup