sequel 2.12.0 → 3.0.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 +62 -0
- data/README.rdoc +3 -3
- data/Rakefile +7 -0
- data/doc/advanced_associations.rdoc +44 -0
- data/doc/release_notes/3.0.0.txt +221 -0
- data/lib/sequel/adapters/amalgalite.rb +208 -0
- data/lib/sequel/adapters/db2.rb +3 -0
- data/lib/sequel/adapters/dbi.rb +9 -0
- data/lib/sequel/adapters/do.rb +0 -4
- data/lib/sequel/adapters/firebird.rb +16 -18
- data/lib/sequel/adapters/informix.rb +5 -3
- data/lib/sequel/adapters/jdbc.rb +24 -20
- data/lib/sequel/adapters/jdbc/h2.rb +15 -4
- data/lib/sequel/adapters/mysql.rb +4 -8
- data/lib/sequel/adapters/odbc.rb +0 -4
- data/lib/sequel/adapters/oracle.rb +0 -4
- data/lib/sequel/adapters/shared/mssql.rb +16 -5
- data/lib/sequel/adapters/shared/mysql.rb +87 -86
- data/lib/sequel/adapters/shared/oracle.rb +92 -3
- data/lib/sequel/adapters/shared/postgres.rb +85 -29
- data/lib/sequel/adapters/shared/progress.rb +8 -3
- data/lib/sequel/adapters/shared/sqlite.rb +53 -23
- data/lib/sequel/adapters/sqlite.rb +4 -7
- data/lib/sequel/adapters/utils/unsupported.rb +3 -3
- data/lib/sequel/connection_pool.rb +18 -25
- data/lib/sequel/core.rb +2 -21
- data/lib/sequel/database.rb +60 -44
- data/lib/sequel/database/schema_generator.rb +26 -31
- data/lib/sequel/database/schema_methods.rb +8 -3
- data/lib/sequel/database/schema_sql.rb +114 -28
- data/lib/sequel/dataset.rb +14 -41
- data/lib/sequel/dataset/convenience.rb +31 -54
- data/lib/sequel/dataset/graph.rb +7 -13
- data/lib/sequel/dataset/sql.rb +43 -54
- data/lib/sequel/extensions/inflector.rb +0 -5
- data/lib/sequel/extensions/schema_dumper.rb +238 -0
- data/lib/sequel/metaprogramming.rb +0 -20
- data/lib/sequel/model.rb +1 -2
- data/lib/sequel/model/base.rb +18 -16
- data/lib/sequel/model/inflections.rb +6 -9
- data/lib/sequel/plugins/caching.rb +0 -6
- data/lib/sequel/plugins/hook_class_methods.rb +1 -1
- data/lib/sequel/sql.rb +2 -0
- data/lib/sequel/version.rb +2 -2
- data/spec/adapters/firebird_spec.rb +35 -8
- data/spec/adapters/mysql_spec.rb +173 -266
- data/spec/adapters/oracle_spec.rb +13 -0
- data/spec/adapters/postgres_spec.rb +127 -227
- data/spec/adapters/sqlite_spec.rb +13 -171
- data/spec/core/connection_pool_spec.rb +15 -4
- data/spec/core/core_sql_spec.rb +14 -170
- data/spec/core/database_spec.rb +50 -132
- data/spec/core/dataset_spec.rb +47 -930
- data/spec/core/expression_filters_spec.rb +12 -0
- data/spec/core/schema_generator_spec.rb +37 -45
- data/spec/core/schema_spec.rb +26 -16
- data/spec/core/spec_helper.rb +0 -25
- data/spec/extensions/inflector_spec.rb +0 -3
- data/spec/extensions/schema_dumper_spec.rb +292 -0
- data/spec/extensions/serialization_spec.rb +9 -0
- data/spec/extensions/single_table_inheritance_spec.rb +6 -1
- data/spec/extensions/spec_helper.rb +1 -3
- data/spec/extensions/validation_helpers_spec.rb +4 -4
- data/spec/integration/database_test.rb +18 -0
- data/spec/integration/dataset_test.rb +112 -1
- data/spec/integration/eager_loader_test.rb +70 -9
- data/spec/integration/prepared_statement_test.rb +2 -2
- data/spec/integration/schema_test.rb +76 -27
- data/spec/integration/spec_helper.rb +0 -14
- data/spec/integration/transaction_test.rb +27 -0
- data/spec/model/associations_spec.rb +0 -36
- data/spec/model/base_spec.rb +18 -123
- data/spec/model/hooks_spec.rb +2 -235
- data/spec/model/inflector_spec.rb +15 -115
- data/spec/model/model_spec.rb +0 -120
- data/spec/model/plugins_spec.rb +0 -70
- data/spec/model/record_spec.rb +35 -93
- data/spec/model/spec_helper.rb +0 -27
- data/spec/model/validations_spec.rb +0 -931
- metadata +9 -14
- data/lib/sequel/deprecated.rb +0 -593
- data/lib/sequel/deprecated_migration.rb +0 -91
- data/lib/sequel/model/deprecated.rb +0 -204
- data/lib/sequel/model/deprecated_hooks.rb +0 -103
- data/lib/sequel/model/deprecated_inflector.rb +0 -335
- data/lib/sequel/model/deprecated_validations.rb +0 -388
- data/spec/core/core_ext_spec.rb +0 -156
- data/spec/core/migration_spec.rb +0 -263
- data/spec/core/pretty_table_spec.rb +0 -58
- data/spec/model/caching_spec.rb +0 -217
- data/spec/model/schema_spec.rb +0 -92
data/lib/sequel/dataset/graph.rb
CHANGED
@@ -17,8 +17,7 @@ module Sequel
|
|
17
17
|
# the tables are combined in the single return hash. You can get around that by
|
18
18
|
# using .select with correct aliases for all of the columns, but it is simpler to
|
19
19
|
# use graph and have the result set split for you. In addition, graph respects
|
20
|
-
# any row_proc
|
21
|
-
# you use with graph.
|
20
|
+
# any row_proc of the current dataset and the datasets you use with graph.
|
22
21
|
#
|
23
22
|
# If you are graphing a table and all columns for that table are nil, this
|
24
23
|
# indicates that no matching rows existed in the table, so graph will return nil
|
@@ -186,7 +185,7 @@ module Sequel
|
|
186
185
|
# Fetch the rows, split them into component table parts,
|
187
186
|
# tranform and run the row_proc on each part (if applicable),
|
188
187
|
# and yield a hash of the parts.
|
189
|
-
def graph_each
|
188
|
+
def graph_each
|
190
189
|
# Reject tables with nil datasets, as they are excluded from
|
191
190
|
# the result set
|
192
191
|
datasets = @opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
|
@@ -194,13 +193,11 @@ module Sequel
|
|
194
193
|
table_aliases = datasets.collect{|ta,ds| ta}
|
195
194
|
# Get an array of arrays, one for each dataset, with
|
196
195
|
# the necessary information about each dataset, for speed
|
197
|
-
datasets = datasets.collect
|
198
|
-
[ta, ds, ds.instance_variable_get(:@transform), ds.row_proc]
|
199
|
-
end
|
196
|
+
datasets = datasets.collect{|ta, ds| [ta, ds, ds.row_proc]}
|
200
197
|
# Use the manually set graph aliases, if any, otherwise
|
201
198
|
# use the ones automatically created by .graph
|
202
199
|
column_aliases = @opts[:graph_aliases] || @opts[:graph][:column_aliases]
|
203
|
-
fetch_rows(
|
200
|
+
fetch_rows(select_sql) do |r|
|
204
201
|
graph = {}
|
205
202
|
# Create the sub hashes, one per table
|
206
203
|
table_aliases.each{|ta| graph[ta]={}}
|
@@ -211,14 +208,11 @@ module Sequel
|
|
211
208
|
ta, column = tc
|
212
209
|
graph[ta][column] = r[col_alias]
|
213
210
|
end
|
214
|
-
# For each dataset
|
215
|
-
|
216
|
-
datasets.each do |ta,ds,tr,rp|
|
211
|
+
# For each dataset run the row_proc if applicable
|
212
|
+
datasets.each do |ta,ds,rp|
|
217
213
|
g = graph[ta]
|
218
214
|
graph[ta] = if g.values.any?{|x| !x.nil?}
|
219
|
-
|
220
|
-
g = rp[g] if rp
|
221
|
-
g
|
215
|
+
rp ? rp.call(g) : g
|
222
216
|
else
|
223
217
|
nil
|
224
218
|
end
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -12,7 +12,7 @@ module Sequel
|
|
12
12
|
N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
|
13
13
|
NULL = "NULL".freeze
|
14
14
|
QUESTION_MARK = '?'.freeze
|
15
|
-
STOCK_COUNT_OPTS = {:select => [LiteralString.new("COUNT(*)").freeze], :order => nil}.freeze
|
15
|
+
STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
|
16
16
|
SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having compounds order limit'.freeze
|
17
17
|
TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
|
18
18
|
WILDCARD = '*'.freeze
|
@@ -49,7 +49,7 @@ module Sequel
|
|
49
49
|
|
50
50
|
# SQL fragment for the SQL CAST expression.
|
51
51
|
def cast_sql(expr, type)
|
52
|
-
"CAST(#{literal(expr)} AS #{db.
|
52
|
+
"CAST(#{literal(expr)} AS #{db.cast_type_literal(type)})"
|
53
53
|
end
|
54
54
|
|
55
55
|
# SQL fragment for specifying all columns in a given table.
|
@@ -87,9 +87,8 @@ module Sequel
|
|
87
87
|
#
|
88
88
|
# dataset.filter{|o| o.price >= 100}.delete_sql #=>
|
89
89
|
# "DELETE FROM items WHERE (price >= 100)"
|
90
|
-
def delete_sql
|
91
|
-
|
92
|
-
opts = opts ? @opts.merge(opts) : @opts
|
90
|
+
def delete_sql
|
91
|
+
opts = @opts
|
93
92
|
|
94
93
|
return static_sql(opts[:sql]) if opts[:sql]
|
95
94
|
|
@@ -136,7 +135,6 @@ module Sequel
|
|
136
135
|
def exclude(*cond, &block)
|
137
136
|
clause = (@opts[:having] ? :having : :where)
|
138
137
|
cond = cond.first if cond.size == 1
|
139
|
-
cond = SQL::BooleanExpression.from_value_pairs(cond, :OR) if Sequel.condition_specifier?(cond)
|
140
138
|
cond = filter_expr(cond, &block)
|
141
139
|
cond = SQL::BooleanExpression.invert(cond)
|
142
140
|
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
@@ -147,9 +145,8 @@ module Sequel
|
|
147
145
|
#
|
148
146
|
# DB.select(1).where(DB[:items].exists).sql
|
149
147
|
# #=> "SELECT 1 WHERE EXISTS (SELECT * FROM items)"
|
150
|
-
def exists
|
151
|
-
|
152
|
-
LiteralString.new("EXISTS (#{defarg ? select_sql : select_sql(opts)})")
|
148
|
+
def exists
|
149
|
+
LiteralString.new("EXISTS (#{select_sql})")
|
153
150
|
end
|
154
151
|
|
155
152
|
# Returns a copy of the dataset with the given conditions imposed upon it.
|
@@ -223,10 +220,11 @@ module Sequel
|
|
223
220
|
|
224
221
|
# Returns a copy of the dataset with the source changed.
|
225
222
|
#
|
223
|
+
# dataset.from # SQL: SELECT *
|
226
224
|
# dataset.from(:blah) # SQL: SELECT * FROM blah
|
227
225
|
# dataset.from(:blah, :foo) # SQL: SELECT * FROM blah, foo
|
228
226
|
def from(*source)
|
229
|
-
clone(:from
|
227
|
+
clone(:from=>source.empty? ? nil : source)
|
230
228
|
end
|
231
229
|
|
232
230
|
# Returns a dataset selecting from the current dataset.
|
@@ -324,7 +322,6 @@ module Sequel
|
|
324
322
|
when Hash
|
325
323
|
values = @opts[:defaults].merge(values) if @opts[:defaults]
|
326
324
|
values = values.merge(@opts[:overrides]) if @opts[:overrides]
|
327
|
-
values = transform_save(values) if @transform
|
328
325
|
if values.empty?
|
329
326
|
insert_default_values_sql
|
330
327
|
else
|
@@ -362,11 +359,6 @@ module Sequel
|
|
362
359
|
clone(o)
|
363
360
|
end
|
364
361
|
|
365
|
-
# SQL fragment specifying an Irregular (cast/extract) SQL function call
|
366
|
-
def irregular_function_sql(f)
|
367
|
-
"#{f.f}(#{literal(f.arg1)} #{f.joiner} #{literal(f.arg2)})"
|
368
|
-
end
|
369
|
-
|
370
362
|
# SQL fragment specifying a JOIN clause without ON or USING.
|
371
363
|
def join_clause_sql(jc)
|
372
364
|
table = jc.table
|
@@ -608,6 +600,7 @@ module Sequel
|
|
608
600
|
# quote the name with quoted_identifier.
|
609
601
|
def quote_identifier(name)
|
610
602
|
return name if name.is_a?(LiteralString)
|
603
|
+
name = name.value if name.is_a?(SQL::Identifier)
|
611
604
|
name = input_identifier(name)
|
612
605
|
name = quoted_identifier(name) if quote_identifiers?
|
613
606
|
name
|
@@ -683,24 +676,21 @@ module Sequel
|
|
683
676
|
# Formats a SELECT statement
|
684
677
|
#
|
685
678
|
# dataset.select_sql # => "SELECT * FROM items"
|
686
|
-
def select_sql
|
687
|
-
|
688
|
-
opts = opts ? @opts.merge(opts) : @opts
|
689
|
-
return static_sql(opts[:sql]) if opts[:sql]
|
679
|
+
def select_sql
|
680
|
+
return static_sql(@opts[:sql]) if @opts[:sql]
|
690
681
|
sql = 'SELECT'
|
691
|
-
select_clause_order.each{|x| send("select_#{x}_sql", sql
|
682
|
+
select_clause_order.each{|x| send(:"select_#{x}_sql", sql)}
|
692
683
|
sql
|
693
684
|
end
|
694
685
|
|
695
686
|
# Same as select_sql, not aliased directly to make subclassing simpler.
|
696
|
-
def sql
|
697
|
-
|
698
|
-
defarg ? select_sql : select_sql(opts)
|
687
|
+
def sql
|
688
|
+
select_sql
|
699
689
|
end
|
700
690
|
|
701
691
|
# SQL fragment for specifying subscripts (SQL arrays)
|
702
692
|
def subscript_sql(s)
|
703
|
-
"#{s.f}[#{s.sub.join(COMMA_SEPARATOR)}]"
|
693
|
+
"#{literal(s.f)}[#{s.sub.join(COMMA_SEPARATOR)}]"
|
704
694
|
end
|
705
695
|
|
706
696
|
# Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
|
@@ -733,9 +723,8 @@ module Sequel
|
|
733
723
|
#
|
734
724
|
# Raises an error if the dataset is grouped or includes more
|
735
725
|
# than one table.
|
736
|
-
def update_sql(values = {}
|
737
|
-
|
738
|
-
opts = opts ? @opts.merge(opts) : @opts
|
726
|
+
def update_sql(values = {})
|
727
|
+
opts = @opts
|
739
728
|
|
740
729
|
return static_sql(opts[:sql]) if opts[:sql]
|
741
730
|
|
@@ -750,7 +739,6 @@ module Sequel
|
|
750
739
|
values = opts[:defaults].merge(values) if opts[:defaults]
|
751
740
|
values = values.merge(opts[:overrides]) if opts[:overrides]
|
752
741
|
# get values from hash
|
753
|
-
values = transform_save(values) if @transform
|
754
742
|
values.map do |k, v|
|
755
743
|
"#{[String, Symbol].any?{|c| k.is_a?(c)} ? quote_identifier(k) : literal(k)} = #{literal(v)}"
|
756
744
|
end.join(COMMA_SEPARATOR)
|
@@ -775,7 +763,7 @@ module Sequel
|
|
775
763
|
end
|
776
764
|
|
777
765
|
# Returns a copy of the dataset with the static SQL used. This is useful if you want
|
778
|
-
# to keep the same row_proc/
|
766
|
+
# to keep the same row_proc/graph, but change the SQL used to custom SQL.
|
779
767
|
#
|
780
768
|
# dataset.with_sql('SELECT * FROM foo') # SELECT * FROM foo
|
781
769
|
def with_sql(sql, *args)
|
@@ -802,7 +790,6 @@ module Sequel
|
|
802
790
|
# Internal filter method so it works on either the having or where clauses.
|
803
791
|
def _filter(clause, *cond, &block)
|
804
792
|
cond = cond.first if cond.size == 1
|
805
|
-
cond = transform_save(cond) if @transform if cond.is_a?(Hash)
|
806
793
|
cond = filter_expr(cond, &block)
|
807
794
|
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
808
795
|
clone(clause => cond)
|
@@ -851,8 +838,10 @@ module Sequel
|
|
851
838
|
when Array
|
852
839
|
if String === expr[0]
|
853
840
|
SQL::PlaceholderLiteralString.new(expr.shift, expr, true)
|
854
|
-
|
841
|
+
elsif Sequel.condition_specifier?(expr)
|
855
842
|
SQL::BooleanExpression.from_value_pairs(expr)
|
843
|
+
else
|
844
|
+
SQL::BooleanExpression.new(:AND, *expr.map{|x| filter_expr(x)})
|
856
845
|
end
|
857
846
|
when Proc
|
858
847
|
filter_expr(virtual_row_block_call(expr))
|
@@ -1021,13 +1010,13 @@ module Sequel
|
|
1021
1010
|
end
|
1022
1011
|
|
1023
1012
|
# Modify the sql to add the columns selected
|
1024
|
-
def select_columns_sql(sql
|
1025
|
-
sql << " #{column_list(opts[:select])}"
|
1013
|
+
def select_columns_sql(sql)
|
1014
|
+
sql << " #{column_list(@opts[:select])}"
|
1026
1015
|
end
|
1027
1016
|
|
1028
1017
|
# Modify the sql to add the DISTINCT modifier
|
1029
|
-
def select_distinct_sql(sql
|
1030
|
-
if distinct = opts[:distinct]
|
1018
|
+
def select_distinct_sql(sql)
|
1019
|
+
if distinct = @opts[:distinct]
|
1031
1020
|
sql << " DISTINCT#{" ON (#{expression_list(distinct)})" unless distinct.empty?}"
|
1032
1021
|
end
|
1033
1022
|
end
|
@@ -1035,9 +1024,9 @@ module Sequel
|
|
1035
1024
|
# Modify the sql to add a dataset to the via an EXCEPT, INTERSECT, or UNION clause.
|
1036
1025
|
# This uses a subselect for the compound datasets used, because using parantheses doesn't
|
1037
1026
|
# work on all databases. I consider this an ugly hack, but can't I think of a better default.
|
1038
|
-
def select_compounds_sql(sql
|
1039
|
-
return unless opts[:compounds]
|
1040
|
-
opts[:compounds].each do |type, dataset, all|
|
1027
|
+
def select_compounds_sql(sql)
|
1028
|
+
return unless @opts[:compounds]
|
1029
|
+
@opts[:compounds].each do |type, dataset, all|
|
1041
1030
|
compound_sql = subselect_sql(dataset)
|
1042
1031
|
compound_sql = "SELECT * FROM (#{compound_sql})" if dataset.opts[:compounds]
|
1043
1032
|
sql.replace("#{sql} #{type.to_s.upcase}#{' ALL' if all} #{compound_sql}")
|
@@ -1045,39 +1034,39 @@ module Sequel
|
|
1045
1034
|
end
|
1046
1035
|
|
1047
1036
|
# Modify the sql to add the list of tables to select FROM
|
1048
|
-
def select_from_sql(sql
|
1049
|
-
sql << " FROM #{source_list(opts[:from])}" if opts[:from]
|
1037
|
+
def select_from_sql(sql)
|
1038
|
+
sql << " FROM #{source_list(@opts[:from])}" if @opts[:from]
|
1050
1039
|
end
|
1051
1040
|
|
1052
1041
|
# Modify the sql to add the expressions to GROUP BY
|
1053
|
-
def select_group_sql(sql
|
1054
|
-
sql << " GROUP BY #{expression_list(opts[:group])}" if opts[:group]
|
1042
|
+
def select_group_sql(sql)
|
1043
|
+
sql << " GROUP BY #{expression_list(@opts[:group])}" if @opts[:group]
|
1055
1044
|
end
|
1056
1045
|
|
1057
1046
|
# Modify the sql to add the filter criteria in the HAVING clause
|
1058
|
-
def select_having_sql(sql
|
1059
|
-
sql << " HAVING #{literal(opts[:having])}" if opts[:having]
|
1047
|
+
def select_having_sql(sql)
|
1048
|
+
sql << " HAVING #{literal(@opts[:having])}" if @opts[:having]
|
1060
1049
|
end
|
1061
1050
|
|
1062
1051
|
# Modify the sql to add the list of tables to JOIN to
|
1063
|
-
def select_join_sql(sql
|
1064
|
-
opts[:join].each{|j| sql << literal(j)} if opts[:join]
|
1052
|
+
def select_join_sql(sql)
|
1053
|
+
@opts[:join].each{|j| sql << literal(j)} if @opts[:join]
|
1065
1054
|
end
|
1066
1055
|
|
1067
1056
|
# Modify the sql to limit the number of rows returned and offset
|
1068
|
-
def select_limit_sql(sql
|
1069
|
-
sql << " LIMIT #{opts[:limit]}" if opts[:limit]
|
1070
|
-
sql << " OFFSET #{opts[:offset]}" if opts[:offset]
|
1057
|
+
def select_limit_sql(sql)
|
1058
|
+
sql << " LIMIT #{@opts[:limit]}" if @opts[:limit]
|
1059
|
+
sql << " OFFSET #{@opts[:offset]}" if @opts[:offset]
|
1071
1060
|
end
|
1072
1061
|
|
1073
1062
|
# Modify the sql to add the expressions to ORDER BY
|
1074
|
-
def select_order_sql(sql
|
1075
|
-
sql << " ORDER BY #{expression_list(opts[:order])}" if opts[:order]
|
1063
|
+
def select_order_sql(sql)
|
1064
|
+
sql << " ORDER BY #{expression_list(@opts[:order])}" if @opts[:order]
|
1076
1065
|
end
|
1077
1066
|
|
1078
1067
|
# Modify the sql to add the filter criteria in the WHERE clause
|
1079
|
-
def select_where_sql(sql
|
1080
|
-
sql << " WHERE #{literal(opts[:where])}" if opts[:where]
|
1068
|
+
def select_where_sql(sql)
|
1069
|
+
sql << " WHERE #{literal(@opts[:where])}" if @opts[:where]
|
1081
1070
|
end
|
1082
1071
|
|
1083
1072
|
# Converts an array of source names into into a comma separated list.
|
@@ -32,7 +32,6 @@ class String
|
|
32
32
|
# clear :all
|
33
33
|
# clear :plurals
|
34
34
|
def self.clear(scope = :all)
|
35
|
-
Sequel::Inflections.clear(scope)
|
36
35
|
case scope
|
37
36
|
when :all
|
38
37
|
@plurals, @singulars, @uncountables = [], [], []
|
@@ -48,7 +47,6 @@ class String
|
|
48
47
|
# irregular 'octopus', 'octopi'
|
49
48
|
# irregular 'person', 'people'
|
50
49
|
def self.irregular(singular, plural)
|
51
|
-
Sequel::Inflections.irregular(singular, plural)
|
52
50
|
plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
|
53
51
|
singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
|
54
52
|
end
|
@@ -59,7 +57,6 @@ class String
|
|
59
57
|
# Example:
|
60
58
|
# plural(/(x|ch|ss|sh)$/i, '\1es')
|
61
59
|
def self.plural(rule, replacement)
|
62
|
-
Sequel::Inflections.plural(rule, replacement)
|
63
60
|
@plurals.insert(0, [rule, replacement])
|
64
61
|
end
|
65
62
|
|
@@ -69,7 +66,6 @@ class String
|
|
69
66
|
# Example:
|
70
67
|
# singular(/([^aeiouy]|qu)ies$/i, '\1y')
|
71
68
|
def self.singular(rule, replacement)
|
72
|
-
Sequel::Inflections.singular(rule, replacement)
|
73
69
|
@singulars.insert(0, [rule, replacement])
|
74
70
|
end
|
75
71
|
|
@@ -80,7 +76,6 @@ class String
|
|
80
76
|
# uncountable "money", "information"
|
81
77
|
# uncountable %w( money information rice )
|
82
78
|
def self.uncountable(*words)
|
83
|
-
Sequel::Inflections.uncountable(*words)
|
84
79
|
(@uncountables << words).flatten!
|
85
80
|
end
|
86
81
|
|
@@ -0,0 +1,238 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Database
|
3
|
+
# Dump indexes for all tables as a migration. This complements
|
4
|
+
# the :indexes=>false option to dump_schema_migration.
|
5
|
+
def dump_indexes_migration
|
6
|
+
ts = tables
|
7
|
+
<<END_MIG
|
8
|
+
Class.new(Sequel::Migration) do
|
9
|
+
def up
|
10
|
+
#{ts.map{|t| dump_table_indexes(t, :add_index)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
|
11
|
+
end
|
12
|
+
|
13
|
+
def down
|
14
|
+
#{ts.map{|t| dump_table_indexes(t, :drop_index)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
END_MIG
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return a string that contains a Sequel::Migration subclass that when
|
21
|
+
# run would recreate the database structure. Options:
|
22
|
+
# * :same_db - Don't attempt to translate database types to ruby types.
|
23
|
+
# If this isn't set to true, all database types will be translated to
|
24
|
+
# ruby types, but there is no guarantee that the migration generated
|
25
|
+
# will yield the same type. Without this set, types that aren't
|
26
|
+
# recognized will be translated to a string-like type.
|
27
|
+
# * :indexes - If set to false, don't dump indexes (they can be added
|
28
|
+
# later via dump_index_migration).
|
29
|
+
def dump_schema_migration(options={})
|
30
|
+
ts = tables
|
31
|
+
<<END_MIG
|
32
|
+
Class.new(Sequel::Migration) do
|
33
|
+
def up
|
34
|
+
#{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, ' ')}
|
35
|
+
end
|
36
|
+
|
37
|
+
def down
|
38
|
+
drop_table(#{ts.inspect[1...-1]})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
END_MIG
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return a string with a create table block that will recreate the given
|
45
|
+
# table's schema. Takes the same options as dump_schema_migration.
|
46
|
+
def dump_table_schema(table, options={})
|
47
|
+
s = schema(table).dup
|
48
|
+
pks = s.find_all{|x| x.last[:primary_key] == true}.map{|x| x.first}
|
49
|
+
options = options.merge(:single_pk=>true) if pks.length == 1
|
50
|
+
m = method(:column_schema_to_generator_opts)
|
51
|
+
im = method(:index_to_generator_opts)
|
52
|
+
indexes = indexes(table).sort_by{|k,v| k.to_s} if options[:indexes] != false and respond_to?(:indexes)
|
53
|
+
gen = Schema::Generator.new(self) do
|
54
|
+
s.each{|name, info| send(*m.call(name, info, options))}
|
55
|
+
primary_key(pks) if !@primary_key && pks.length > 0
|
56
|
+
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} if indexes
|
57
|
+
end
|
58
|
+
commands = [gen.dump_columns, gen.dump_constraints, gen.dump_indexes].reject{|x| x == ''}.join("\n\n")
|
59
|
+
"create_table(#{table.inspect}) do\n#{commands.gsub(/^/o, ' ')}\nend"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Convert the given default, which should be a database specific string, into
|
65
|
+
# a ruby object. If it can't be converted, return the string with the inspect
|
66
|
+
# method modified so that .lit is always appended after it.
|
67
|
+
def column_schema_to_ruby_default(default, type)
|
68
|
+
case default
|
69
|
+
when /false/
|
70
|
+
false
|
71
|
+
when 'true'
|
72
|
+
true
|
73
|
+
when /\A\d+\z/
|
74
|
+
default.to_i
|
75
|
+
else
|
76
|
+
def default.inspect
|
77
|
+
"#{super}.lit"
|
78
|
+
end
|
79
|
+
default
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Convert the given name and parsed database schema into an array with a method
|
84
|
+
# name and arguments to it to pass to a Schema::Generator to recreate the column.
|
85
|
+
def column_schema_to_generator_opts(name, schema, options)
|
86
|
+
if options[:single_pk] && schema_autoincrementing_primary_key?(schema)
|
87
|
+
[:primary_key, name]
|
88
|
+
else
|
89
|
+
col_opts = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
|
90
|
+
type = col_opts.delete(:type)
|
91
|
+
col_opts.delete(:size) if col_opts[:size].nil?
|
92
|
+
col_opts[:default] = column_schema_to_ruby_default(schema[:default], type) if schema[:default]
|
93
|
+
col_opts[:null] = false if schema[:allow_null] == false
|
94
|
+
[:column, name, type, col_opts]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Convert the column schema information to a hash of column options, one of which must
|
99
|
+
# be :type. The other options added should modify that type (e.g. :size). If a
|
100
|
+
# database type is not recognized, return it as a String type.
|
101
|
+
def column_schema_to_ruby_type(schema)
|
102
|
+
case t = schema[:db_type].downcase
|
103
|
+
when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?\z/o
|
104
|
+
{:type=>Integer}
|
105
|
+
when /\Atinyint(?:\((?:\d+)\))?\z/o
|
106
|
+
{:type=>(Sequel.convert_tinyint_to_bool ? TrueClass : Integer)}
|
107
|
+
when /\Abigint(?:\((?:\d+)\))?\z/o
|
108
|
+
{:type=>Bignum}
|
109
|
+
when /\A(?:real|float|double(?: precision)?)\z/o
|
110
|
+
{:type=>Float}
|
111
|
+
when 'boolean'
|
112
|
+
{:type=>TrueClass}
|
113
|
+
when /\A(?:(?:tiny|medium|long)?text|clob)\z/o
|
114
|
+
{:type=>String, :text=>true}
|
115
|
+
when 'date'
|
116
|
+
{:type=>Date}
|
117
|
+
when 'datetime'
|
118
|
+
{:type=>DateTime}
|
119
|
+
when /\Atimestamp(?: with(?:out)? time zone)?\z/o
|
120
|
+
{:type=>DateTime}
|
121
|
+
when /\Atime(?: with(?:out)? time zone)?\z/o
|
122
|
+
{:type=>Time, :only_time=>true}
|
123
|
+
when /\Achar(?:acter)?(?:\((\d+)\))?\z/o
|
124
|
+
{:type=>String, :size=>($1.to_i if $1), :fixed=>true}
|
125
|
+
when /\A(?:varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/o
|
126
|
+
s = ($1.to_i if $1)
|
127
|
+
{:type=>String, :size=>(s == 255 ? nil : s)}
|
128
|
+
when 'money'
|
129
|
+
{:type=>BigDecimal, :size=>[19,2]}
|
130
|
+
when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/o
|
131
|
+
s = [($1.to_i if $1), ($2.to_i if $2)].compact
|
132
|
+
{:type=>BigDecimal, :size=>(s.empty? ? nil : s)}
|
133
|
+
when /\A(?:bytea|(?:tiny|medium|long)?blob|(?:var)?binary)(?:\((\d+)\))?\z/o
|
134
|
+
{:type=>File, :size=>($1.to_i if $1)}
|
135
|
+
when 'year'
|
136
|
+
{:type=>Integer}
|
137
|
+
else
|
138
|
+
{:type=>String}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return a string that containing add_index/drop_index method calls for
|
143
|
+
# creating the index migration.
|
144
|
+
def dump_table_indexes(table, meth)
|
145
|
+
return '' unless respond_to?(:indexes)
|
146
|
+
im = method(:index_to_generator_opts)
|
147
|
+
indexes = indexes(table).sort_by{|k,v| k.to_s}
|
148
|
+
gen = Schema::Generator.new(self) do
|
149
|
+
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))}
|
150
|
+
end
|
151
|
+
gen.dump_indexes(meth=>table)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Convert the parsed index information into options to the Generators index method.
|
155
|
+
def index_to_generator_opts(table, name, index_opts)
|
156
|
+
h = {}
|
157
|
+
h[:name] = name unless default_index_name(table, index_opts[:columns]) == name.to_s
|
158
|
+
h[:unique] = true if index_opts[:unique]
|
159
|
+
h
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
module Schema
|
164
|
+
class Generator
|
165
|
+
# Dump this generator's columns to a string that could be evaled inside
|
166
|
+
# another instance to represent the same columns
|
167
|
+
def dump_columns
|
168
|
+
strings = []
|
169
|
+
cols = columns.dup
|
170
|
+
if pkn = primary_key_name
|
171
|
+
cols.delete_if{|x| x[:name] == pkn}
|
172
|
+
pk = @primary_key.dup
|
173
|
+
pkname = pk.delete(:name)
|
174
|
+
@db.serial_primary_key_options.each{|k,v| pk.delete(k) if v == pk[k]}
|
175
|
+
strings << "primary_key #{pkname.inspect}#{opts_inspect(pk)}"
|
176
|
+
end
|
177
|
+
cols.each do |c|
|
178
|
+
c = c.dup
|
179
|
+
name = c.delete(:name)
|
180
|
+
type = c.delete(:type)
|
181
|
+
opts = opts_inspect(c)
|
182
|
+
strings << if type.is_a?(Class)
|
183
|
+
"#{type.name} #{name.inspect}#{opts}"
|
184
|
+
else
|
185
|
+
"column #{name.inspect}, #{type.inspect}#{opts}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
strings.join("\n")
|
189
|
+
end
|
190
|
+
|
191
|
+
# Dump this generator's constraints to a string that could be evaled inside
|
192
|
+
# another instance to represent the same constraints
|
193
|
+
def dump_constraints
|
194
|
+
constraints.map do |c|
|
195
|
+
c = c.dup
|
196
|
+
type = c.delete(:type)
|
197
|
+
case type
|
198
|
+
when :check
|
199
|
+
raise(Error, "can't dump check/constraint specified with Proc") if c[:check].is_a?(Proc)
|
200
|
+
name = c.delete(:name)
|
201
|
+
if !name and c[:check].length == 1 and c[:check].first.is_a?(Hash)
|
202
|
+
"check #{c[:check].first.inspect[1...-1]}"
|
203
|
+
else
|
204
|
+
"#{name ? "constraint #{name.inspect}," : 'check'} #{c[:check].map{|x| x.inspect}.join(', ')}"
|
205
|
+
end
|
206
|
+
else
|
207
|
+
cols = c.delete(:columns)
|
208
|
+
"#{type} #{cols.inspect}#{opts_inspect(c)}"
|
209
|
+
end
|
210
|
+
end.join("\n")
|
211
|
+
end
|
212
|
+
|
213
|
+
# Dump this generator's indexes to a string that could be evaled inside
|
214
|
+
# another instance to represent the same indexes. Options:
|
215
|
+
# * :add_index - Use add_index instead of index, so the methods
|
216
|
+
# can be called outside of a generator but inside a migration.
|
217
|
+
# The value of this option should be the table name to use.
|
218
|
+
# * :drop_index - Same as add_index, but create drop_index statements.
|
219
|
+
def dump_indexes(options={})
|
220
|
+
indexes.map do |c|
|
221
|
+
c = c.dup
|
222
|
+
cols = c.delete(:columns)
|
223
|
+
if table = options[:add_index] || options[:drop_index]
|
224
|
+
"#{options[:drop_index] ? 'drop' : 'add'}_index #{table.inspect}, #{cols.inspect}#{opts_inspect(c)}"
|
225
|
+
else
|
226
|
+
"index #{cols.inspect}#{opts_inspect(c)}"
|
227
|
+
end
|
228
|
+
end.join("\n")
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def opts_inspect(opts)
|
234
|
+
", #{opts.inspect[1...-1]}" if opts.length > 0
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|