sequel 3.33.0 → 3.34.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +140 -0
- data/Rakefile +7 -0
- data/bin/sequel +22 -2
- data/doc/dataset_basics.rdoc +1 -1
- data/doc/mass_assignment.rdoc +3 -1
- data/doc/querying.rdoc +28 -4
- data/doc/reflection.rdoc +23 -3
- data/doc/release_notes/3.34.0.txt +671 -0
- data/doc/schema_modification.rdoc +18 -2
- data/doc/virtual_rows.rdoc +49 -0
- data/lib/sequel/adapters/do/mysql.rb +0 -5
- data/lib/sequel/adapters/ibmdb.rb +9 -4
- data/lib/sequel/adapters/jdbc.rb +9 -4
- data/lib/sequel/adapters/jdbc/h2.rb +8 -2
- data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
- data/lib/sequel/adapters/jdbc/postgresql.rb +43 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +19 -0
- data/lib/sequel/adapters/mock.rb +24 -3
- data/lib/sequel/adapters/mysql.rb +29 -50
- data/lib/sequel/adapters/mysql2.rb +13 -28
- data/lib/sequel/adapters/oracle.rb +8 -2
- data/lib/sequel/adapters/postgres.rb +115 -20
- data/lib/sequel/adapters/shared/db2.rb +1 -1
- data/lib/sequel/adapters/shared/mssql.rb +14 -3
- data/lib/sequel/adapters/shared/mysql.rb +59 -11
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
- data/lib/sequel/adapters/shared/oracle.rb +1 -1
- data/lib/sequel/adapters/shared/postgres.rb +127 -30
- data/lib/sequel/adapters/shared/sqlite.rb +55 -38
- data/lib/sequel/adapters/sqlite.rb +9 -3
- data/lib/sequel/adapters/swift.rb +2 -2
- data/lib/sequel/adapters/swift/mysql.rb +0 -5
- data/lib/sequel/adapters/swift/postgres.rb +10 -0
- data/lib/sequel/ast_transformer.rb +4 -0
- data/lib/sequel/connection_pool.rb +8 -0
- data/lib/sequel/connection_pool/sharded_single.rb +5 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +17 -0
- data/lib/sequel/connection_pool/single.rb +5 -0
- data/lib/sequel/connection_pool/threaded.rb +14 -0
- data/lib/sequel/core.rb +24 -3
- data/lib/sequel/database/connecting.rb +24 -14
- data/lib/sequel/database/dataset_defaults.rb +1 -0
- data/lib/sequel/database/misc.rb +16 -25
- data/lib/sequel/database/query.rb +20 -2
- data/lib/sequel/database/schema_generator.rb +2 -2
- data/lib/sequel/database/schema_methods.rb +120 -23
- data/lib/sequel/dataset/actions.rb +91 -18
- data/lib/sequel/dataset/features.rb +5 -0
- data/lib/sequel/dataset/prepared_statements.rb +6 -2
- data/lib/sequel/dataset/sql.rb +68 -51
- data/lib/sequel/extensions/_pretty_table.rb +79 -0
- data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +18 -13
- data/lib/sequel/extensions/migration.rb +4 -0
- data/lib/sequel/extensions/null_dataset.rb +90 -0
- data/lib/sequel/extensions/pg_array.rb +460 -0
- data/lib/sequel/extensions/pg_array_ops.rb +220 -0
- data/lib/sequel/extensions/pg_auto_parameterize.rb +174 -0
- data/lib/sequel/extensions/pg_hstore.rb +296 -0
- data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
- data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
- data/lib/sequel/extensions/pretty_table.rb +5 -71
- data/lib/sequel/extensions/query_literals.rb +79 -0
- data/lib/sequel/extensions/schema_caching.rb +76 -0
- data/lib/sequel/extensions/schema_dumper.rb +227 -31
- data/lib/sequel/extensions/select_remove.rb +35 -0
- data/lib/sequel/extensions/sql_expr.rb +4 -110
- data/lib/sequel/extensions/to_dot.rb +1 -1
- data/lib/sequel/model.rb +11 -2
- data/lib/sequel/model/associations.rb +35 -7
- data/lib/sequel/model/base.rb +159 -36
- data/lib/sequel/no_core_ext.rb +2 -0
- data/lib/sequel/plugins/caching.rb +25 -18
- data/lib/sequel/plugins/composition.rb +1 -1
- data/lib/sequel/plugins/hook_class_methods.rb +1 -1
- data/lib/sequel/plugins/identity_map.rb +11 -3
- data/lib/sequel/plugins/instance_filters.rb +10 -0
- data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
- data/lib/sequel/plugins/nested_attributes.rb +4 -3
- data/lib/sequel/plugins/prepared_statements.rb +3 -1
- data/lib/sequel/plugins/prepared_statements_associations.rb +5 -1
- data/lib/sequel/plugins/schema.rb +7 -2
- data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
- data/lib/sequel/plugins/static_cache.rb +99 -0
- data/lib/sequel/plugins/validation_class_methods.rb +1 -1
- data/lib/sequel/sql.rb +417 -7
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/firebird_spec.rb +1 -1
- data/spec/adapters/mssql_spec.rb +12 -15
- data/spec/adapters/mysql_spec.rb +81 -23
- data/spec/adapters/postgres_spec.rb +444 -77
- data/spec/adapters/spec_helper.rb +2 -0
- data/spec/adapters/sqlite_spec.rb +8 -8
- data/spec/core/connection_pool_spec.rb +85 -0
- data/spec/core/database_spec.rb +29 -5
- data/spec/core/dataset_spec.rb +171 -3
- data/spec/core/expression_filters_spec.rb +364 -0
- data/spec/core/mock_adapter_spec.rb +17 -3
- data/spec/core/schema_spec.rb +133 -0
- data/spec/extensions/association_dependencies_spec.rb +13 -13
- data/spec/extensions/caching_spec.rb +26 -3
- data/spec/extensions/class_table_inheritance_spec.rb +2 -2
- data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +23 -94
- data/spec/extensions/force_encoding_spec.rb +4 -2
- data/spec/extensions/hook_class_methods_spec.rb +5 -2
- data/spec/extensions/identity_map_spec.rb +17 -0
- data/spec/extensions/instance_filters_spec.rb +1 -1
- data/spec/extensions/lazy_attributes_spec.rb +2 -2
- data/spec/extensions/list_spec.rb +4 -4
- data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
- data/spec/extensions/migration_spec.rb +6 -2
- data/spec/extensions/nested_attributes_spec.rb +20 -0
- data/spec/extensions/null_dataset_spec.rb +85 -0
- data/spec/extensions/optimistic_locking_spec.rb +2 -2
- data/spec/extensions/pg_array_ops_spec.rb +105 -0
- data/spec/extensions/pg_array_spec.rb +196 -0
- data/spec/extensions/pg_auto_parameterize_spec.rb +64 -0
- data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
- data/spec/extensions/pg_hstore_spec.rb +195 -0
- data/spec/extensions/pg_statement_cache_spec.rb +209 -0
- data/spec/extensions/prepared_statements_spec.rb +4 -0
- data/spec/extensions/pretty_table_spec.rb +6 -0
- data/spec/extensions/query_literals_spec.rb +168 -0
- data/spec/extensions/schema_caching_spec.rb +41 -0
- data/spec/extensions/schema_dumper_spec.rb +231 -11
- data/spec/extensions/schema_spec.rb +14 -2
- data/spec/extensions/select_remove_spec.rb +38 -0
- data/spec/extensions/sharding_spec.rb +6 -6
- data/spec/extensions/skip_create_refresh_spec.rb +1 -1
- data/spec/extensions/spec_helper.rb +2 -1
- data/spec/extensions/sql_expr_spec.rb +28 -19
- data/spec/extensions/static_cache_spec.rb +145 -0
- data/spec/extensions/touch_spec.rb +1 -1
- data/spec/extensions/typecast_on_load_spec.rb +9 -1
- data/spec/integration/associations_test.rb +6 -6
- data/spec/integration/database_test.rb +1 -1
- data/spec/integration/dataset_test.rb +89 -26
- data/spec/integration/migrator_test.rb +2 -3
- data/spec/integration/model_test.rb +3 -3
- data/spec/integration/plugin_test.rb +85 -22
- data/spec/integration/prepared_statement_test.rb +28 -8
- data/spec/integration/schema_test.rb +78 -7
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/integration/timezone_test.rb +1 -1
- data/spec/integration/transaction_test.rb +4 -6
- data/spec/integration/type_test.rb +2 -2
- data/spec/model/associations_spec.rb +94 -8
- data/spec/model/base_spec.rb +4 -4
- data/spec/model/hooks_spec.rb +2 -2
- data/spec/model/model_spec.rb +19 -7
- data/spec/model/record_spec.rb +135 -58
- data/spec/model/spec_helper.rb +1 -0
- 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
|
-
|
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
|
-
|
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.
|
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.
|
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
|
-
|
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 &&
|
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
|
-
|
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
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
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
|