sequel 3.33.0 → 3.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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