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.
- 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
|