sequel 2.12.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|