sequel 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/CHANGELOG +1551 -4
  2. data/README +306 -19
  3. data/Rakefile +84 -56
  4. data/bin/sequel +106 -0
  5. data/doc/cheat_sheet.rdoc +225 -0
  6. data/doc/dataset_filtering.rdoc +182 -0
  7. data/lib/sequel_core.rb +136 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
  9. data/lib/sequel_core/adapters/ado.rb +80 -0
  10. data/lib/sequel_core/adapters/db2.rb +148 -0
  11. data/lib/sequel_core/adapters/dbi.rb +117 -0
  12. data/lib/sequel_core/adapters/informix.rb +78 -0
  13. data/lib/sequel_core/adapters/jdbc.rb +186 -0
  14. data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
  15. data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
  16. data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
  17. data/lib/sequel_core/adapters/mysql.rb +231 -0
  18. data/lib/sequel_core/adapters/odbc.rb +155 -0
  19. data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/openbase.rb +64 -0
  21. data/lib/sequel_core/adapters/oracle.rb +170 -0
  22. data/lib/sequel_core/adapters/postgres.rb +199 -0
  23. data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
  24. data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
  25. data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
  26. data/lib/sequel_core/adapters/sqlite.rb +138 -0
  27. data/lib/sequel_core/connection_pool.rb +194 -0
  28. data/lib/sequel_core/core_ext.rb +203 -0
  29. data/lib/sequel_core/core_sql.rb +184 -0
  30. data/lib/sequel_core/database.rb +471 -0
  31. data/lib/sequel_core/database/schema.rb +156 -0
  32. data/lib/sequel_core/dataset.rb +457 -0
  33. data/lib/sequel_core/dataset/callback.rb +13 -0
  34. data/lib/sequel_core/dataset/convenience.rb +245 -0
  35. data/lib/sequel_core/dataset/pagination.rb +96 -0
  36. data/lib/sequel_core/dataset/query.rb +41 -0
  37. data/lib/sequel_core/dataset/schema.rb +15 -0
  38. data/lib/sequel_core/dataset/sql.rb +889 -0
  39. data/lib/sequel_core/deprecated.rb +26 -0
  40. data/lib/sequel_core/exceptions.rb +42 -0
  41. data/lib/sequel_core/migration.rb +187 -0
  42. data/lib/sequel_core/object_graph.rb +216 -0
  43. data/lib/sequel_core/pretty_table.rb +71 -0
  44. data/lib/sequel_core/schema.rb +2 -0
  45. data/lib/sequel_core/schema/generator.rb +239 -0
  46. data/lib/sequel_core/schema/sql.rb +325 -0
  47. data/lib/sequel_core/sql.rb +812 -0
  48. data/lib/sequel_model.rb +5 -1
  49. data/lib/sequel_model/association_reflection.rb +3 -8
  50. data/lib/sequel_model/base.rb +15 -10
  51. data/lib/sequel_model/inflector.rb +3 -5
  52. data/lib/sequel_model/plugins.rb +1 -1
  53. data/lib/sequel_model/record.rb +11 -3
  54. data/lib/sequel_model/schema.rb +4 -4
  55. data/lib/sequel_model/validations.rb +6 -1
  56. data/spec/adapters/ado_spec.rb +17 -0
  57. data/spec/adapters/informix_spec.rb +96 -0
  58. data/spec/adapters/mysql_spec.rb +764 -0
  59. data/spec/adapters/oracle_spec.rb +222 -0
  60. data/spec/adapters/postgres_spec.rb +441 -0
  61. data/spec/adapters/spec_helper.rb +7 -0
  62. data/spec/adapters/sqlite_spec.rb +400 -0
  63. data/spec/integration/dataset_test.rb +51 -0
  64. data/spec/integration/eager_loader_test.rb +702 -0
  65. data/spec/integration/schema_test.rb +102 -0
  66. data/spec/integration/spec_helper.rb +44 -0
  67. data/spec/integration/type_test.rb +43 -0
  68. data/spec/rcov.opts +2 -0
  69. data/spec/sequel_core/connection_pool_spec.rb +363 -0
  70. data/spec/sequel_core/core_ext_spec.rb +156 -0
  71. data/spec/sequel_core/core_sql_spec.rb +427 -0
  72. data/spec/sequel_core/database_spec.rb +964 -0
  73. data/spec/sequel_core/dataset_spec.rb +2977 -0
  74. data/spec/sequel_core/expression_filters_spec.rb +346 -0
  75. data/spec/sequel_core/migration_spec.rb +261 -0
  76. data/spec/sequel_core/object_graph_spec.rb +234 -0
  77. data/spec/sequel_core/pretty_table_spec.rb +58 -0
  78. data/spec/sequel_core/schema_generator_spec.rb +122 -0
  79. data/spec/sequel_core/schema_spec.rb +497 -0
  80. data/spec/sequel_core/spec_helper.rb +51 -0
  81. data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
  82. data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
  83. data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
  84. data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
  85. data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
  86. data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
  87. data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
  88. data/spec/sequel_model/inflector_spec.rb +119 -0
  89. data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
  90. data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
  91. data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
  92. data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
  93. data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
  94. data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
  95. data/spec/spec_config.rb +9 -0
  96. data/spec/spec_config.rb.example +10 -0
  97. metadata +110 -37
  98. data/spec/inflector_spec.rb +0 -34
@@ -0,0 +1,13 @@
1
+ module Sequel
2
+ class Dataset
3
+ private
4
+ # This is run inside .all, after all
5
+ # of the records have been loaded
6
+ # via .each, but before any block passed
7
+ # to all is called. It is called with
8
+ # a single argument, an array of all
9
+ # returned records.
10
+ def post_load(all_records)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,245 @@
1
+ module Sequel
2
+ class Dataset
3
+ COMMA_SEPARATOR = ', '.freeze
4
+ COUNT_OF_ALL_AS_COUNT = :count['*'.lit].as(:count)
5
+
6
+ # Returns the first record matching the conditions.
7
+ def [](*conditions)
8
+ first(*conditions)
9
+ end
10
+
11
+ # Update all records matching the conditions
12
+ # with the values specified.
13
+ def []=(conditions, values)
14
+ filter(conditions).update(values)
15
+ end
16
+
17
+ # Returns the average value for the given column.
18
+ def avg(column)
19
+ get(:avg[column])
20
+ end
21
+
22
+ # Returns true if no records exists in the dataset
23
+ def empty?
24
+ get(1).nil?
25
+ end
26
+
27
+ # Returns the first record in the dataset. If a numeric argument is
28
+ # given, it is interpreted as a limit, and then returns all
29
+ # matching records up to that limit. If no argument is passed,
30
+ # it returns the first matching record. If any other type of
31
+ # argument(s) is passed, it is given to filter and the
32
+ # first matching record is returned. If a block is given, it is used
33
+ # to filter the dataset before returning anything.
34
+ #
35
+ # Examples:
36
+ #
37
+ # ds.first => {:id=>7}
38
+ # ds.first(2) => [{:id=>6}, {:id=>4}]
39
+ # ds.order(:id).first(2) => [{:id=>1}, {:id=>2}]
40
+ # ds.first(:id=>2) => {:id=>2}
41
+ # ds.first("id = 3") => {:id=>3}
42
+ # ds.first("id = ?", 4) => {:id=>4}
43
+ # ds.first{:id > 2} => {:id=>5}
44
+ # ds.order(:id).first{:id > 2} => {:id=>3}
45
+ # ds.first{:id > 2} => {:id=>5}
46
+ # ds.first("id > ?", 4){:id < 6) => {:id=>5}
47
+ # ds.order(:id).first(2){:id < 2} => [{:id=>1}]
48
+ def first(*args, &block)
49
+ ds = block ? filter(&block) : self
50
+
51
+ if args.empty?
52
+ ds.single_record
53
+ else
54
+ args = (args.size == 1) ? args.first : args
55
+ if Integer === args
56
+ ds.limit(args).all
57
+ else
58
+ ds.filter(args).single_record
59
+ end
60
+ end
61
+ end
62
+
63
+ # Return the column value for the first matching record in the dataset.
64
+ def get(column)
65
+ select(column).single_value
66
+ end
67
+
68
+ # Returns a dataset grouped by the given column with count by group.
69
+ def group_and_count(*columns)
70
+ group(*columns).select(*(columns + [COUNT_OF_ALL_AS_COUNT])).order(:count)
71
+ end
72
+
73
+ # Returns the interval between minimum and maximum values for the given
74
+ # column.
75
+ def interval(column)
76
+ get("(max(#{literal(column)}) - min(#{literal(column)}))".lit)
77
+ end
78
+
79
+ # Reverses the order and then runs first. Note that this
80
+ # will not necessarily give you the last record in the dataset,
81
+ # unless you have an unambiguous order. If there is not
82
+ # currently an order for this dataset, raises an Error.
83
+ def last(*args, &block)
84
+ raise(Error, 'No order specified') unless @opts[:order]
85
+ reverse.first(*args, &block)
86
+ end
87
+
88
+ # Maps column values for each record in the dataset (if a column name is
89
+ # given), or performs the stock mapping functionality of Enumerable.
90
+ def map(column_name = nil, &block)
91
+ if column_name
92
+ super() {|r| r[column_name]}
93
+ else
94
+ super(&block)
95
+ end
96
+ end
97
+
98
+ # Returns the maximum value for the given column.
99
+ def max(column)
100
+ get(:max[column])
101
+ end
102
+
103
+ # Returns the minimum value for the given column.
104
+ def min(column)
105
+ get(:min[column])
106
+ end
107
+
108
+ # Inserts multiple records into the associated table. This method can be
109
+ # to efficiently insert a large amounts of records into a table. Inserts
110
+ # are automatically wrapped in a transaction.
111
+ #
112
+ # This method should be called with a columns array and an array of value arrays:
113
+ #
114
+ # dataset.multi_insert([:x, :y], [[1, 2], [3, 4]])
115
+ #
116
+ # This method can also be called with an array of hashes:
117
+ #
118
+ # dataset.multi_insert({:x => 1}, {:x => 2})
119
+ #
120
+ # Be aware that all hashes should have the same keys if you use this calling method,
121
+ # otherwise some columns could be missed or set to null instead of to default
122
+ # values.
123
+ #
124
+ # The method also accepts a :slice or :commit_every option that specifies
125
+ # the number of records to insert per transaction. This is useful especially
126
+ # when inserting a large number of records, e.g.:
127
+ #
128
+ # # this will commit every 50 records
129
+ # dataset.multi_insert(lots_of_records, :slice => 50)
130
+ def multi_insert(*args)
131
+ if args.empty?
132
+ return
133
+ elsif args[0].is_a?(Array) && args[1].is_a?(Array)
134
+ columns, values, opts = *args
135
+ elsif args[0].is_a?(Array) && args[1].is_a?(Dataset)
136
+ table = @opts[:from].first
137
+ columns, dataset = *args
138
+ sql = "INSERT INTO #{quote_identifier(table)} #{literal(columns)} VALUES #{literal(dataset)}"
139
+ return @db.transaction {@db.execute_dui sql}
140
+ else
141
+ # we assume that an array of hashes is given
142
+ hashes, opts = *args
143
+ return if hashes.empty?
144
+ columns = hashes.first.keys
145
+ # convert the hashes into arrays
146
+ values = hashes.map {|h| columns.map {|c| h[c]}}
147
+ end
148
+ # make sure there's work to do
149
+ return if columns.empty? || values.empty?
150
+
151
+ slice_size = opts && (opts[:commit_every] || opts[:slice])
152
+
153
+ if slice_size
154
+ values.each_slice(slice_size) do |slice|
155
+ statements = multi_insert_sql(columns, slice)
156
+ @db.transaction {statements.each {|st| @db.execute_dui(st)}}
157
+ end
158
+ else
159
+ statements = multi_insert_sql(columns, values)
160
+ @db.transaction {statements.each {|st| @db.execute_dui(st)}}
161
+ end
162
+ end
163
+ alias_method :import, :multi_insert
164
+
165
+ # Pretty prints the records in the dataset as plain-text table.
166
+ def print(*cols)
167
+ Sequel::PrettyTable.print(naked.all, cols.empty? ? columns : cols)
168
+ end
169
+
170
+ # Returns a Range object made from the minimum and maximum values for the
171
+ # given column.
172
+ def range(column)
173
+ if r = select(:min[column].as(:v1), :max[column].as(:v2)).first
174
+ (r[:v1]..r[:v2])
175
+ end
176
+ end
177
+
178
+ # Returns the first record in the dataset.
179
+ def single_record(opts = nil)
180
+ each((opts||{}).merge(:limit=>1)){|r| return r}
181
+ nil
182
+ end
183
+
184
+ # Returns the first value of the first record in the dataset.
185
+ # Returns nil if dataset is empty.
186
+ def single_value(opts = nil)
187
+ if r = naked.single_record(opts)
188
+ r.values.first
189
+ end
190
+ end
191
+
192
+ # Returns the sum for the given column.
193
+ def sum(column)
194
+ get(:sum[column])
195
+ end
196
+
197
+ # Returns true if the table exists. Will raise an error
198
+ # if the dataset has fixed SQL or selects from another dataset
199
+ # or more than one table.
200
+ def table_exists?
201
+ if @opts[:sql]
202
+ raise Sequel::Error, "this dataset has fixed SQL"
203
+ end
204
+
205
+ if @opts[:from].size != 1
206
+ raise Sequel::Error, "this dataset selects from multiple sources"
207
+ end
208
+
209
+ t = @opts[:from].first
210
+ if t.is_a?(Dataset)
211
+ raise Sequel::Error, "this dataset selects from a sub query"
212
+ end
213
+
214
+ @db.table_exists?(t.to_sym)
215
+ end
216
+
217
+ # Returns a string in CSV format containing the dataset records. By
218
+ # default the CSV representation includes the column titles in the
219
+ # first line. You can turn that off by passing false as the
220
+ # include_column_titles argument.
221
+ #
222
+ # This does not use a CSV library or handle quoting of values in
223
+ # any way. If any values in any of the rows could include commas or line
224
+ # endings, you probably shouldn't use this.
225
+ def to_csv(include_column_titles = true)
226
+ n = naked
227
+ cols = n.columns
228
+ csv = ''
229
+ csv << "#{cols.join(COMMA_SEPARATOR)}\r\n" if include_column_titles
230
+ n.each{|r| csv << "#{cols.collect{|c| r[c]}.join(COMMA_SEPARATOR)}\r\n"}
231
+ csv
232
+ end
233
+
234
+ # Returns a hash with one column used as key and another used as value.
235
+ # If rows have duplicate values for the key column, the latter row(s)
236
+ # will overwrite the value of the previous row(s). If the value_column
237
+ # is not given or nil, uses the entire hash as the value.
238
+ def to_hash(key_column, value_column = nil)
239
+ inject({}) do |m, r|
240
+ m[r[key_column]] = value_column ? r[value_column] : r
241
+ m
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,96 @@
1
+ module Sequel
2
+ class Dataset
3
+ # Returns a paginated dataset. The returned dataset is limited to
4
+ # the page size at the correct offset, and extended with the Pagination
5
+ # module. If a record count is not provided, does a count of total
6
+ # number of records for this dataset.
7
+ def paginate(page_no, page_size, record_count=nil)
8
+ raise(Error, "You cannot paginate a dataset that already has a limit") if @opts[:limit]
9
+ paginated = limit(page_size, (page_no - 1) * page_size)
10
+ paginated.extend(Pagination)
11
+ paginated.set_pagination_info(page_no, page_size, record_count || count)
12
+ end
13
+
14
+ # Yields a paginated dataset for each page and returns the receiver. Does
15
+ # a count to find the total number of records for this dataset.
16
+ def each_page(page_size, &block)
17
+ raise(Error, "You cannot paginate a dataset that already has a limit") if @opts[:limit]
18
+ record_count = count
19
+ total_pages = (record_count / page_size.to_f).ceil
20
+ (1..total_pages).each{|page_no| yield paginate(page_no, page_size, record_count)}
21
+ self
22
+ end
23
+
24
+ # Holds methods that only relate to paginated datasets. Paginated dataset
25
+ # have pages starting at 1 (page 1 is offset 0, page 1 is offset page_size).
26
+ module Pagination
27
+ # The number of records per page (the final page may have fewer than
28
+ # this number of records).
29
+ attr_accessor :page_size
30
+
31
+ # The number of pages in the dataset before pagination, of which
32
+ # this paginated dataset is one.
33
+ attr_accessor :page_count
34
+
35
+ # The current page of the dataset, starting at 1 and not 0.
36
+ attr_accessor :current_page
37
+
38
+ # The total number of records in the dataset before pagination.
39
+ attr_accessor :pagination_record_count
40
+
41
+ # Returns the record range for the current page
42
+ def current_page_record_range
43
+ return (0..0) if @current_page > @page_count
44
+
45
+ a = 1 + (@current_page - 1) * @page_size
46
+ b = a + @page_size - 1
47
+ b = @pagination_record_count if b > @pagination_record_count
48
+ a..b
49
+ end
50
+
51
+ # Returns the number of records in the current page
52
+ def current_page_record_count
53
+ return 0 if @current_page > @page_count
54
+
55
+ a = 1 + (@current_page - 1) * @page_size
56
+ b = a + @page_size - 1
57
+ b = @pagination_record_count if b > @pagination_record_count
58
+ b - a + 1
59
+ end
60
+
61
+ # Returns true if the current page is the first page
62
+ def first_page?
63
+ @current_page == 1
64
+ end
65
+
66
+ # Returns true if the current page is the last page
67
+ def last_page?
68
+ @current_page == @page_count
69
+ end
70
+
71
+ # Returns the next page number or nil if the current page is the last page
72
+ def next_page
73
+ current_page < page_count ? (current_page + 1) : nil
74
+ end
75
+
76
+ # Returns the page range
77
+ def page_range
78
+ 1..page_count
79
+ end
80
+
81
+ # Returns the previous page number or nil if the current page is the first
82
+ def prev_page
83
+ current_page > 1 ? (current_page - 1) : nil
84
+ end
85
+
86
+ # Sets the pagination info for this paginated dataset, and returns self.
87
+ def set_pagination_info(page_no, page_size, record_count)
88
+ @current_page = page_no
89
+ @page_size = page_size
90
+ @pagination_record_count = record_count
91
+ @page_count = (record_count / page_size.to_f).ceil
92
+ self
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,41 @@
1
+ module Sequel
2
+ class Dataset
3
+ # Translates a query block into a dataset. Query blocks can be useful
4
+ # when expressing complex SELECT statements, e.g.:
5
+ #
6
+ # dataset = DB[:items].query do
7
+ # select :x, :y, :z
8
+ # filter((:x > 1) & (:y > 2))
9
+ # order :z.desc
10
+ # end
11
+ #
12
+ # Which is the same as:
13
+ #
14
+ # dataset = DB[:items].select(:x, :y, :z).filter((:x > 1) & (:y > 2)).order(:z.desc)
15
+ #
16
+ # Note that inside a call to query, you cannot call each, insert, update,
17
+ # or delete (or any method that calls those), or Sequel will raise an
18
+ # error.
19
+ def query(&block)
20
+ copy = clone({})
21
+ copy.extend(QueryBlockCopy)
22
+ copy.instance_eval(&block)
23
+ clone(copy.opts)
24
+ end
25
+
26
+ # Module used by Dataset#query that has the effect of making all
27
+ # dataset methods into !-style methods that modify the receiver.
28
+ module QueryBlockCopy
29
+ %w'each insert update delete'.each do |meth|
30
+ define_method(meth){|*args| raise Error, "##{meth} cannot be invoked inside a query block."}
31
+ end
32
+
33
+ # Merge the given options into the receiver's options and return the receiver
34
+ # instead of cloning the receiver.
35
+ def clone(opts = nil)
36
+ @opts.merge!(opts)
37
+ self
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Sequel
2
+ class Dataset
3
+ # Creates a view in the database with the given named based
4
+ # on the current dataset.
5
+ def create_view(name)
6
+ @db.create_view(name, self)
7
+ end
8
+
9
+ # Creates or replaces a view in the database with the given
10
+ # named based on the current dataset.
11
+ def create_or_replace_view(name)
12
+ @db.create_or_replace_view(name, self)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,889 @@
1
+ module Sequel
2
+ class Dataset
3
+ AND_SEPARATOR = " AND ".freeze
4
+ BOOL_FALSE = "'f'".freeze
5
+ BOOL_TRUE = "'t'".freeze
6
+ COLUMN_REF_RE1 = /\A([\w ]+)__([\w ]+)___([\w ]+)\z/.freeze
7
+ COLUMN_REF_RE2 = /\A([\w ]+)___([\w ]+)\z/.freeze
8
+ COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
9
+ COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit]
10
+ DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
11
+ N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
12
+ NULL = "NULL".freeze
13
+ QUESTION_MARK = '?'.freeze
14
+ STOCK_COUNT_OPTS = {:select => ["COUNT(*)".lit], :order => nil}.freeze
15
+ TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
16
+ TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
17
+ WILDCARD = '*'.freeze
18
+
19
+ # Adds an further filter to an existing filter using AND. If no filter
20
+ # exists an error is raised. This method is identical to #filter except
21
+ # it expects an existing filter.
22
+ def and(*cond, &block)
23
+ raise(Error::NoExistingFilter, "No existing filter found.") unless @opts[:having] || @opts[:where]
24
+ filter(*cond, &block)
25
+ end
26
+
27
+ # SQL fragment for the aliased expression
28
+ def aliased_expression_sql(ae)
29
+ "#{literal(ae.expression)} AS #{quote_identifier(ae.aliaz)}"
30
+ end
31
+
32
+ # SQL fragment for specifying given CaseExpression.
33
+ def case_expression_sql(ce)
34
+ "(CASE #{ce.conditions.collect{|c,r| "WHEN #{literal(c)} THEN #{literal(r)} "}.join}ELSE #{literal(ce.default)} END)"
35
+ end
36
+
37
+ # SQL fragment for specifying all columns in a given table.
38
+ def column_all_sql(ca)
39
+ "#{quote_identifier(ca.table)}.*"
40
+ end
41
+
42
+ # SQL fragment for complex expressions
43
+ def complex_expression_sql(op, args)
44
+ case op
45
+ when *TWO_ARITY_OPERATORS
46
+ "(#{literal(args.at(0))} #{op} #{literal(args.at(1))})"
47
+ when *N_ARITY_OPERATORS
48
+ "(#{args.collect{|a| literal(a)}.join(" #{op} ")})"
49
+ when :NOT
50
+ "NOT #{literal(args.at(0))}"
51
+ when :NOOP
52
+ literal(args.at(0))
53
+ when :'B~'
54
+ "~#{literal(args.at(0))}"
55
+ else
56
+ raise(Sequel::Error, "invalid operator #{op}")
57
+ end
58
+ end
59
+
60
+ # Returns the number of records in the dataset.
61
+ def count
62
+ options_overlap(COUNT_FROM_SELF_OPTS) ? from_self.count : single_value(STOCK_COUNT_OPTS).to_i
63
+ end
64
+ alias_method :size, :count
65
+
66
+ # Formats a DELETE statement using the given options and dataset options.
67
+ #
68
+ # dataset.filter(:price >= 100).delete_sql #=>
69
+ # "DELETE FROM items WHERE (price >= 100)"
70
+ def delete_sql(opts = nil)
71
+ opts = opts ? @opts.merge(opts) : @opts
72
+
73
+ if opts[:group]
74
+ raise Error::InvalidOperation, "Grouped datasets cannot be deleted from"
75
+ elsif opts[:from].is_a?(Array) && opts[:from].size > 1
76
+ raise Error::InvalidOperation, "Joined datasets cannot be deleted from"
77
+ end
78
+
79
+ sql = "DELETE FROM #{source_list(opts[:from])}"
80
+
81
+ if where = opts[:where]
82
+ sql << " WHERE #{literal(where)}"
83
+ end
84
+
85
+ sql
86
+ end
87
+
88
+ # Adds an EXCEPT clause using a second dataset object. If all is true the
89
+ # clause used is EXCEPT ALL, which may return duplicate rows.
90
+ #
91
+ # DB[:items].except(DB[:other_items]).sql
92
+ # #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
93
+ def except(dataset, all = false)
94
+ clone(:except => dataset, :except_all => all)
95
+ end
96
+
97
+ # Performs the inverse of Dataset#filter.
98
+ #
99
+ # dataset.exclude(:category => 'software').sql #=>
100
+ # "SELECT * FROM items WHERE (category != 'software')"
101
+ def exclude(*cond, &block)
102
+ clause = (@opts[:having] ? :having : :where)
103
+ cond = cond.first if cond.size == 1
104
+ cond = cond.sql_or if (Hash === cond) || ((Array === cond) && (cond.all_two_pairs?))
105
+ cond = filter_expr(cond, &block)
106
+ cond = SQL::BooleanExpression.invert(cond)
107
+ cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
108
+ clone(clause => cond)
109
+ end
110
+
111
+ # Returns an EXISTS clause for the dataset.
112
+ #
113
+ # DB.select(1).where(DB[:items].exists).sql
114
+ # #=> "SELECT 1 WHERE EXISTS (SELECT * FROM items)"
115
+ def exists(opts = nil)
116
+ "EXISTS (#{select_sql(opts)})"
117
+ end
118
+
119
+ # Returns a copy of the dataset with the given conditions imposed upon it.
120
+ # If the query has been grouped, then the conditions are imposed in the
121
+ # HAVING clause. If not, then they are imposed in the WHERE clause. Filter
122
+ #
123
+ # filter accepts the following argument types:
124
+ #
125
+ # * Hash - list of equality expressions
126
+ # * Array - depends:
127
+ # * If first member is a string, assumes the rest of the arguments
128
+ # are parameters and interpolates them into the string.
129
+ # * If all members are arrays of length two, treats the same way
130
+ # as a hash, except it allows for duplicate keys to be
131
+ # specified.
132
+ # * String - taken literally
133
+ # * Symbol - taken as a boolean column argument (e.g. WHERE active)
134
+ # * Sequel::SQL::BooleanExpression - an existing condition expression,
135
+ # probably created using the Sequel blockless filter DSL.
136
+ #
137
+ # filter also takes a block, which should return one of the above argument
138
+ # types, and is treated the same way. If both a block and regular argument
139
+ # are provided, they get ANDed together.
140
+ #
141
+ # Examples:
142
+ #
143
+ # dataset.filter(:id => 3).sql #=>
144
+ # "SELECT * FROM items WHERE (id = 3)"
145
+ # dataset.filter('price < ?', 100).sql #=>
146
+ # "SELECT * FROM items WHERE price < 100"
147
+ # dataset.filter([[:id, (1,2,3)], [:id, 0..10]]).sql #=>
148
+ # "SELECT * FROM items WHERE ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10)))"
149
+ # dataset.filter('price < 100').sql #=>
150
+ # "SELECT * FROM items WHERE price < 100"
151
+ # dataset.filter(:active).sql #=>
152
+ # "SELECT * FROM items WHERE :active
153
+ # dataset.filter(:price < 100).sql #=>
154
+ # "SELECT * FROM items WHERE (price < 100)"
155
+ #
156
+ # Multiple filter calls can be chained for scoping:
157
+ #
158
+ # software = dataset.filter(:category => 'software')
159
+ # software.filter(price < 100).sql #=>
160
+ # "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
161
+ #
162
+ # See doc/dataset_filters.rdoc for more examples and details.
163
+ def filter(*cond, &block)
164
+ clause = (@opts[:having] ? :having : :where)
165
+ cond = cond.first if cond.size == 1
166
+ cond = transform_save(cond) if @transform if cond.is_a?(Hash)
167
+ cond = filter_expr(cond, &block)
168
+ cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause] && !@opts[clause].blank?
169
+ clone(clause => cond)
170
+ end
171
+ alias_method :where, :filter
172
+
173
+ # The first source (primary table) for this dataset. If the dataset doesn't
174
+ # have a table, raises an error. If the table is aliased, returns the actual
175
+ # table name, not the alias.
176
+ def first_source
177
+ source = @opts[:from]
178
+ if source.nil? || source.empty?
179
+ raise Error, 'No source specified for query'
180
+ end
181
+ case s = source.first
182
+ when Hash
183
+ s.values.first
184
+ when Symbol
185
+ sch, table, aliaz = split_symbol(s)
186
+ aliaz ? aliaz.to_sym : s
187
+ else
188
+ s
189
+ end
190
+ end
191
+
192
+ # Returns a copy of the dataset with the source changed.
193
+ def from(*source)
194
+ clone(:from => source)
195
+ end
196
+
197
+ # Returns a dataset selecting from the current dataset.
198
+ #
199
+ # ds = DB[:items].order(:name)
200
+ # ds.sql #=> "SELECT * FROM items ORDER BY name"
201
+ # ds.from_self.sql #=> "SELECT * FROM (SELECT * FROM items ORDER BY name)"
202
+ def from_self
203
+ fs = {}
204
+ @opts.keys.each{|k| fs[k] = nil}
205
+ fs[:from] = [self]
206
+ clone(fs)
207
+ end
208
+
209
+ # SQL fragment specifying an SQL function call
210
+ def function_sql(f)
211
+ args = f.args
212
+ "#{f.f}#{args.empty? ? '()' : literal(args)}"
213
+ end
214
+
215
+ # Pattern match any of the columns to any of the terms. The terms can be
216
+ # strings (which use LIKE) or regular expressions (which are only supported
217
+ # in some databases). See Sequel::SQL::StringExpression.like. Note that the
218
+ # total number of pattern matches will be cols.length * terms.length,
219
+ # which could cause performance issues.
220
+ def grep(cols, terms)
221
+ filter(SQL::BooleanExpression.new(:OR, *Array(cols).collect{|c| SQL::StringExpression.like(c, *terms)}))
222
+ end
223
+
224
+ # Returns a copy of the dataset with the results grouped by the value of
225
+ # the given columns
226
+ def group(*columns)
227
+ clone(:group => columns)
228
+ end
229
+ alias_method :group_by, :group
230
+
231
+ # Returns a copy of the dataset with the having conditions changed. Raises
232
+ # an error if the dataset has not been grouped. See also #filter.
233
+ def having(*cond, &block)
234
+ raise(Error::InvalidOperation, "Can only specify a HAVING clause on a grouped dataset") unless @opts[:group]
235
+ clone(:having=>{}).filter(*cond, &block)
236
+ end
237
+
238
+ # Inserts multiple values. If a block is given it is invoked for each
239
+ # item in the given array before inserting it. See #multi_insert as
240
+ # a possible faster version that inserts multiple records in one
241
+ # SQL statement.
242
+ def insert_multiple(array, &block)
243
+ if block
244
+ array.each {|i| insert(block[i])}
245
+ else
246
+ array.each {|i| insert(i)}
247
+ end
248
+ end
249
+
250
+ # Formats an INSERT statement using the given values. If a hash is given,
251
+ # the resulting statement includes column names. If no values are given,
252
+ # the resulting statement includes a DEFAULT VALUES clause.
253
+ #
254
+ # dataset.insert_sql() #=> 'INSERT INTO items DEFAULT VALUES'
255
+ # dataset.insert_sql(1,2,3) #=> 'INSERT INTO items VALUES (1, 2, 3)'
256
+ # dataset.insert_sql(:a => 1, :b => 2) #=>
257
+ # 'INSERT INTO items (a, b) VALUES (1, 2)'
258
+ def insert_sql(*values)
259
+ if values.empty?
260
+ insert_default_values_sql
261
+ else
262
+ values = values[0] if values.size == 1
263
+
264
+ # if hash or array with keys we need to transform the values
265
+ if @transform && (values.is_a?(Hash) || (values.is_a?(Array) && values.keys))
266
+ values = transform_save(values)
267
+ end
268
+ from = source_list(@opts[:from])
269
+
270
+ case values
271
+ when Array
272
+ if values.empty?
273
+ insert_default_values_sql
274
+ else
275
+ "INSERT INTO #{from} VALUES #{literal(values)}"
276
+ end
277
+ when Hash
278
+ if values.empty?
279
+ insert_default_values_sql
280
+ else
281
+ fl, vl = [], []
282
+ values.each {|k, v| fl << literal(k.is_a?(String) ? k.to_sym : k); vl << literal(v)}
283
+ "INSERT INTO #{from} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
284
+ end
285
+ when Dataset
286
+ "INSERT INTO #{from} #{literal(values)}"
287
+ else
288
+ if values.respond_to?(:values)
289
+ insert_sql(values.values)
290
+ else
291
+ "INSERT INTO #{from} VALUES (#{literal(values)})"
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ # Adds an INTERSECT clause using a second dataset object. If all is true
298
+ # the clause used is INTERSECT ALL, which may return duplicate rows.
299
+ #
300
+ # DB[:items].intersect(DB[:other_items]).sql
301
+ # #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
302
+ def intersect(dataset, all = false)
303
+ clone(:intersect => dataset, :intersect_all => all)
304
+ end
305
+
306
+ # Inverts the current filter
307
+ #
308
+ # dataset.filter(:category => 'software').invert.sql #=>
309
+ # "SELECT * FROM items WHERE (category != 'software')"
310
+ def invert
311
+ having, where = @opts[:having], @opts[:where]
312
+ raise(Error, "No current filter") unless having || where
313
+ o = {}
314
+ o[:having] = SQL::BooleanExpression.invert(having) if having
315
+ o[:where] = SQL::BooleanExpression.invert(where) if where
316
+ clone(o)
317
+ end
318
+
319
+ # SQL fragment specifying an Irregular (cast/extract) SQL function call
320
+ def irregular_function_sql(f)
321
+ "#{f.f}(#{literal(f.arg1)} #{f.joiner} #{literal(f.arg2)})"
322
+ end
323
+
324
+ # SQL fragment specifying a JOIN clause without ON or USING.
325
+ def join_clause_sql(jc)
326
+ table = jc.table
327
+ table_alias = jc.table_alias
328
+ table_alias = nil if table == table_alias
329
+ " #{join_type_sql(jc.join_type)} #{table_ref(table)}" \
330
+ "#{" AS #{quote_identifier(jc.table_alias)}" if table_alias}"
331
+ end
332
+
333
+ # SQL fragment specifying a JOIN clause with ON.
334
+ def join_on_clause_sql(jc)
335
+ "#{join_clause_sql(jc)} ON #{literal(filter_expr(jc.on))}"
336
+ end
337
+
338
+ # SQL fragment specifying a JOIN clause with USING.
339
+ def join_using_clause_sql(jc)
340
+ "#{join_clause_sql(jc)} USING (#{column_list(jc.using)})"
341
+ end
342
+
343
+ # Returns a joined dataset. Uses the following arguments:
344
+ #
345
+ # * type - The type of join to do (:inner, :left_outer, :right_outer, :full)
346
+ # * table - Depends on type:
347
+ # * Dataset - a subselect is performed with an alias of tN for some value of N
348
+ # * Model (or anything responding to :table_name) - table.table_name
349
+ # * String, Symbol: table
350
+ # * expr - specifies conditions, depends on type:
351
+ # * Hash, Array with all two pairs - Assumes key (1st arg) is column of joined table (unless already
352
+ # qualified), and value (2nd arg) is column of the last joined or primary table.
353
+ # To specify multiple conditions on a single joined table column, you must use an array.
354
+ # Uses a JOIN with an ON clause.
355
+ # * Array - If all members of the array are symbols, considers them as columns and
356
+ # uses a JOIN with a USING clause. Most databases will remove duplicate columns from
357
+ # the result set if this is used.
358
+ # * nil - If a block is not given, doesn't use ON or USING, so the JOIN should be a NATURAL
359
+ # or CROSS join. If a block is given, uses a ON clause based on the block, see below.
360
+ # * Everything else - pretty much the same as a using the argument in a call to filter,
361
+ # so strings are considered literal, symbols specify boolean columns, and blockless
362
+ # filter expressions can be used. Uses a JOIN with an ON clause.
363
+ # * table_alias - the name of the table's alias when joining, necessary for joining
364
+ # to the same table more than once. No alias is used by default.
365
+ # * block - The block argument should only be given if a JOIN with an ON clause is used,
366
+ # in which case it yields the table alias/name for the table currently being joined,
367
+ # the table alias/name for the last joined (or first table), and an array of previous
368
+ # SQL::JoinClause.
369
+ def join_table(type, table, expr=nil, table_alias=nil, &block)
370
+ if Dataset === table
371
+ if table_alias.nil?
372
+ table_alias_num = (@opts[:num_dataset_sources] || 0) + 1
373
+ table_alias = "t#{table_alias_num}"
374
+ end
375
+ table_name = table_alias
376
+ else
377
+ table = table.table_name if table.respond_to?(:table_name)
378
+ table_name = table_alias || table
379
+ end
380
+
381
+ join = if expr.nil? and !block_given?
382
+ SQL::JoinClause.new(type, table, table_alias)
383
+ elsif Array === expr and !expr.empty? and expr.all?{|x| Symbol === x}
384
+ raise(Sequel::Error, "can't use a block if providing an array of symbols as expr") if block_given?
385
+ SQL::JoinUsingClause.new(expr, type, table, table_alias)
386
+ else
387
+ last_alias = @opts[:last_joined_table] || first_source
388
+ if Hash === expr or (Array === expr and expr.all_two_pairs?)
389
+ expr = expr.collect do |k, v|
390
+ k = qualified_column_name(k, table_name) if k.is_a?(Symbol)
391
+ v = qualified_column_name(v, last_alias) if v.is_a?(Symbol)
392
+ [k,v]
393
+ end
394
+ end
395
+ if block_given?
396
+ expr2 = yield(table_name, last_alias, @opts[:join] || [])
397
+ expr = expr ? SQL::BooleanExpression.new(:AND, expr, expr2) : expr2
398
+ end
399
+ SQL::JoinOnClause.new(expr, type, table, table_alias)
400
+ end
401
+
402
+ opts = {:join => (@opts[:join] || []) + [join], :last_joined_table => table_name}
403
+ opts[:num_dataset_sources] = table_alias_num if table_alias_num
404
+ clone(opts)
405
+ end
406
+
407
+ # If given an integer, the dataset will contain only the first l results.
408
+ # If given a range, it will contain only those at offsets within that
409
+ # range. If a second argument is given, it is used as an offset.
410
+ def limit(l, o = nil)
411
+ return from_self.limit(l, o) if @opts[:sql]
412
+
413
+ if Range === l
414
+ o = l.first
415
+ l = l.interval + 1
416
+ end
417
+ l = l.to_i
418
+ raise(Error, 'Limits must be greater than or equal to 1') unless l >= 1
419
+ opts = {:limit => l}
420
+ if o
421
+ o = o.to_i
422
+ raise(Error, 'Offsets must be greater than or equal to 0') unless o >= 0
423
+ opts[:offset] = o
424
+ end
425
+ clone(opts)
426
+ end
427
+
428
+ # Returns a literal representation of a value to be used as part
429
+ # of an SQL expression.
430
+ #
431
+ # dataset.literal("abc'def\\") #=> "'abc''def\\\\'"
432
+ # dataset.literal(:items__id) #=> "items.id"
433
+ # dataset.literal([1, 2, 3]) => "(1, 2, 3)"
434
+ # dataset.literal(DB[:items]) => "(SELECT * FROM items)"
435
+ # dataset.literal(:x + 1 > :y) => "((x + 1) > y)"
436
+ #
437
+ # If an unsupported object is given, an exception is raised.
438
+ def literal(v)
439
+ case v
440
+ when LiteralString
441
+ v
442
+ when String
443
+ "'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
444
+ when Integer, Float
445
+ v.to_s
446
+ when BigDecimal
447
+ v.to_s("F")
448
+ when NilClass
449
+ NULL
450
+ when TrueClass
451
+ BOOL_TRUE
452
+ when FalseClass
453
+ BOOL_FALSE
454
+ when Symbol
455
+ symbol_to_column_ref(v)
456
+ when ::Sequel::SQL::Expression
457
+ v.to_s(self)
458
+ when Array
459
+ v.all_two_pairs? ? literal(v.sql_expr) : (v.empty? ? '(NULL)' : "(#{v.collect{|i| literal(i)}.join(COMMA_SEPARATOR)})")
460
+ when Hash
461
+ literal(v.sql_expr)
462
+ when Time, DateTime
463
+ v.strftime(TIMESTAMP_FORMAT)
464
+ when Date
465
+ v.strftime(DATE_FORMAT)
466
+ when Dataset
467
+ "(#{v.sql})"
468
+ else
469
+ raise Error, "can't express #{v.inspect} as a SQL literal"
470
+ end
471
+ end
472
+
473
+ # Returns an array of insert statements for inserting multiple records.
474
+ # This method is used by #multi_insert to format insert statements and
475
+ # expects a keys array and and an array of value arrays.
476
+ #
477
+ # This method should be overridden by descendants if the support
478
+ # inserting multiple records in a single SQL statement.
479
+ def multi_insert_sql(columns, values)
480
+ table = quote_identifier(@opts[:from].first)
481
+ columns = literal(columns)
482
+ values.map do |r|
483
+ "INSERT INTO #{table} #{columns} VALUES #{literal(r)}"
484
+ end
485
+ end
486
+
487
+ # Adds an alternate filter to an existing filter using OR. If no filter
488
+ # exists an error is raised.
489
+ def or(*cond, &block)
490
+ clause = (@opts[:having] ? :having : :where)
491
+ cond = cond.first if cond.size == 1
492
+ if @opts[clause]
493
+ clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(cond, &block)))
494
+ else
495
+ raise Error::NoExistingFilter, "No existing filter found."
496
+ end
497
+ end
498
+
499
+ # Returns a copy of the dataset with the order changed. If a nil is given
500
+ # the returned dataset has no order. This can accept multiple arguments
501
+ # of varying kinds, and even SQL functions.
502
+ #
503
+ # ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
504
+ # ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
505
+ # ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
506
+ # ds.order(:a + :b).sql #=> 'SELECT * FROM items ORDER BY (a + b)'
507
+ # ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
508
+ # ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
509
+ # ds.order(:arr|1).sql #=> 'SELECT * FROM items ORDER BY arr[1]'
510
+ # ds.order(nil).sql #=> 'SELECT * FROM items'
511
+ def order(*order)
512
+ clone(:order => (order.compact.empty?) ? nil : order)
513
+ end
514
+ alias_method :order_by, :order
515
+
516
+ # Returns a copy of the dataset with the order columns added
517
+ # to the existing order.
518
+ def order_more(*order)
519
+ order(*((@opts[:order] || []) + order))
520
+ end
521
+
522
+ # SQL fragment for the ordered expression, used in the ORDER BY
523
+ # clause.
524
+ def ordered_expression_sql(oe)
525
+ "#{literal(oe.expression)} #{oe.descending ? 'DESC' : 'ASC'}"
526
+ end
527
+
528
+ # SQL fragment for the qualifed identifier, specifying
529
+ # a table and a column (or schema and table).
530
+ def qualified_identifier_sql(qcr)
531
+ [qcr.table, qcr.column].map{|x| x.is_one_of?(SQL::QualifiedIdentifier, SQL::Identifier) ? literal(x) : quote_identifier(x)}.join('.')
532
+ end
533
+
534
+ # Adds quoting to identifiers (columns and tables). If identifiers are not
535
+ # being quoted, returns name as a string. If identifiers are being quoted
536
+ # quote the name with quoted_identifier.
537
+ def quote_identifier(name)
538
+ quote_identifiers? ? quoted_identifier(name) : name.to_s
539
+ end
540
+ alias_method :quote_column_ref, :quote_identifier
541
+
542
+ # This method quotes the given name with the SQL standard double quote.
543
+ # It uppercases the name given to conform with the SQL standard. This
544
+ # should be overridden by subclasses to provide quoting not matching the
545
+ # SQL standard, such as backtick (used by MySQL and SQLite), or where
546
+ # lowercase is the default for unquoted identifiers (PostgreSQL).
547
+ #
548
+ # If you are using a database such as Oracle that defaults to uppercase
549
+ # but you are using lower case identifiers, you should override this
550
+ # method to not upcase the name for those identifiers.
551
+ def quoted_identifier(name)
552
+ "\"#{name.to_s.upcase}\""
553
+ end
554
+
555
+ # Returns a copy of the dataset with the order reversed. If no order is
556
+ # given, the existing order is inverted.
557
+ def reverse_order(*order)
558
+ order(*invert_order(order.empty? ? @opts[:order] : order))
559
+ end
560
+ alias_method :reverse, :reverse_order
561
+
562
+ # Returns a copy of the dataset with the columns selected changed
563
+ # to the given columns.
564
+ def select(*columns)
565
+ clone(:select => columns)
566
+ end
567
+
568
+ # Returns a copy of the dataset selecting the wildcard.
569
+ def select_all
570
+ clone(:select => nil)
571
+ end
572
+
573
+ # Returns a copy of the dataset with the given columns added
574
+ # to the existing selected columns.
575
+ def select_more(*columns)
576
+ select(*((@opts[:select] || []) + columns))
577
+ end
578
+
579
+ # Formats a SELECT statement using the given options and the dataset
580
+ # options.
581
+ def select_sql(opts = nil)
582
+ opts = opts ? @opts.merge(opts) : @opts
583
+
584
+ if sql = opts[:sql]
585
+ return sql
586
+ end
587
+
588
+ columns = opts[:select]
589
+ select_columns = columns ? column_list(columns) : WILDCARD
590
+
591
+ if distinct = opts[:distinct]
592
+ distinct_clause = distinct.empty? ? "DISTINCT" : "DISTINCT ON (#{expression_list(distinct)})"
593
+ sql = "SELECT #{distinct_clause} #{select_columns}"
594
+ else
595
+ sql = "SELECT #{select_columns}"
596
+ end
597
+
598
+ if opts[:from]
599
+ sql << " FROM #{source_list(opts[:from])}"
600
+ end
601
+
602
+ if join = opts[:join]
603
+ join.each{|j| sql << literal(j)}
604
+ end
605
+
606
+ if where = opts[:where]
607
+ sql << " WHERE #{literal(where)}"
608
+ end
609
+
610
+ if group = opts[:group]
611
+ sql << " GROUP BY #{expression_list(group)}"
612
+ end
613
+
614
+ if having = opts[:having]
615
+ sql << " HAVING #{literal(having)}"
616
+ end
617
+
618
+ if order = opts[:order]
619
+ sql << " ORDER BY #{expression_list(order)}"
620
+ end
621
+
622
+ if limit = opts[:limit]
623
+ sql << " LIMIT #{limit}"
624
+ if offset = opts[:offset]
625
+ sql << " OFFSET #{offset}"
626
+ end
627
+ end
628
+
629
+ if union = opts[:union]
630
+ sql << (opts[:union_all] ? \
631
+ " UNION ALL #{union.sql}" : " UNION #{union.sql}")
632
+ elsif intersect = opts[:intersect]
633
+ sql << (opts[:intersect_all] ? \
634
+ " INTERSECT ALL #{intersect.sql}" : " INTERSECT #{intersect.sql}")
635
+ elsif except = opts[:except]
636
+ sql << (opts[:except_all] ? \
637
+ " EXCEPT ALL #{except.sql}" : " EXCEPT #{except.sql}")
638
+ end
639
+
640
+ sql
641
+ end
642
+ alias_method :sql, :select_sql
643
+
644
+ # SQL fragment for specifying subscripts (SQL arrays)
645
+ def subscript_sql(s)
646
+ "#{s.f}[#{s.sub.join(COMMA_SEPARATOR)}]"
647
+ end
648
+
649
+ # Converts a symbol into a column name. This method supports underscore
650
+ # notation in order to express qualified (two underscores) and aliased
651
+ # (three underscores) columns:
652
+ #
653
+ # ds = DB[:items]
654
+ # :abc.to_column_ref(ds) #=> "abc"
655
+ # :abc___a.to_column_ref(ds) #=> "abc AS a"
656
+ # :items__abc.to_column_ref(ds) #=> "items.abc"
657
+ # :items__abc___a.to_column_ref(ds) #=> "items.abc AS a"
658
+ #
659
+ def symbol_to_column_ref(sym)
660
+ c_table, column, c_alias = split_symbol(sym)
661
+ "#{"#{quote_identifier(c_table)}." if c_table}#{quote_identifier(column)}#{" AS #{quote_identifier(c_alias)}" if c_alias}"
662
+ end
663
+
664
+ # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
665
+ def unfiltered
666
+ clone(:where => nil, :having => nil)
667
+ end
668
+
669
+ # Adds a UNION clause using a second dataset object. If all is true the
670
+ # clause used is UNION ALL, which may return duplicate rows.
671
+ #
672
+ # DB[:items].union(DB[:other_items]).sql
673
+ # #=> "SELECT * FROM items UNION SELECT * FROM other_items"
674
+ def union(dataset, all = false)
675
+ clone(:union => dataset, :union_all => all)
676
+ end
677
+
678
+ # Returns a copy of the dataset with the distinct option.
679
+ def uniq(*args)
680
+ clone(:distinct => args)
681
+ end
682
+ alias_method :distinct, :uniq
683
+
684
+ # Returns a copy of the dataset with no order.
685
+ def unordered
686
+ order(nil)
687
+ end
688
+
689
+ # Formats an UPDATE statement using the given values.
690
+ #
691
+ # dataset.update_sql(:price => 100, :category => 'software') #=>
692
+ # "UPDATE items SET price = 100, category = 'software'"
693
+ #
694
+ # Accepts a block, but such usage is discouraged.
695
+ #
696
+ # Raises an error if the dataset is grouped or includes more
697
+ # than one table.
698
+ def update_sql(values = {}, opts = nil)
699
+ opts = opts ? @opts.merge(opts) : @opts
700
+
701
+ if opts[:group]
702
+ raise Error::InvalidOperation, "A grouped dataset cannot be updated"
703
+ elsif (opts[:from].size > 1) or opts[:join]
704
+ raise Error::InvalidOperation, "A joined dataset cannot be updated"
705
+ end
706
+
707
+ sql = "UPDATE #{source_list(@opts[:from])} SET "
708
+ set = if values.is_a?(Hash)
709
+ # get values from hash
710
+ values = transform_save(values) if @transform
711
+ values.map do |k, v|
712
+ # convert string key into symbol
713
+ k = k.to_sym if String === k
714
+ "#{literal(k)} = #{literal(v)}"
715
+ end.join(COMMA_SEPARATOR)
716
+ else
717
+ # copy values verbatim
718
+ values
719
+ end
720
+ sql << set
721
+ if where = opts[:where]
722
+ sql << " WHERE #{literal(where)}"
723
+ end
724
+
725
+ sql
726
+ end
727
+
728
+ [:inner, :full_outer, :right_outer, :left_outer].each do |jtype|
729
+ class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end")
730
+ end
731
+ alias_method :join, :inner_join
732
+
733
+ protected
734
+
735
+ # Returns a table reference for use in the FROM clause. Returns an SQL subquery
736
+ # frgament with an optional table alias.
737
+ def to_table_reference(table_alias=nil)
738
+ "(#{sql})#{" #{quote_identifier(table_alias)}" if table_alias}"
739
+ end
740
+
741
+ private
742
+
743
+ # Converts an array of column names into a comma seperated string of
744
+ # column names. If the array is empty, a wildcard (*) is returned.
745
+ def column_list(columns)
746
+ if columns.empty?
747
+ WILDCARD
748
+ else
749
+ m = columns.map do |i|
750
+ i.is_a?(Hash) ? i.map{|k, v| "#{literal(k)} AS #{quote_identifier(v)}"} : literal(i)
751
+ end
752
+ m.join(COMMA_SEPARATOR)
753
+ end
754
+ end
755
+
756
+ # Converts an array of expressions into a comma separated string of
757
+ # expressions.
758
+ def expression_list(columns)
759
+ columns.map{|i| literal(i)}.join(COMMA_SEPARATOR)
760
+ end
761
+
762
+ # SQL fragment based on the expr type. See #filter.
763
+ def filter_expr(expr = nil, &block)
764
+ expr = nil if expr == []
765
+ if expr && block
766
+ return SQL::BooleanExpression.new(:AND, filter_expr(expr), filter_expr(block))
767
+ elsif block
768
+ expr = block
769
+ end
770
+ case expr
771
+ when Hash
772
+ SQL::BooleanExpression.from_value_pairs(expr)
773
+ when Array
774
+ if String === expr[0]
775
+ filter_expr(expr.shift.gsub(QUESTION_MARK){literal(expr.shift)}.lit)
776
+ else
777
+ SQL::BooleanExpression.from_value_pairs(expr)
778
+ end
779
+ when Proc
780
+ filter_expr(expr.call(SQL::VirtualRow.new))
781
+ when SQL::NumericExpression, SQL::StringExpression
782
+ raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
783
+ when Symbol, SQL::Expression
784
+ expr
785
+ when TrueClass, FalseClass
786
+ SQL::BooleanExpression.new(:NOOP, expr)
787
+ when String
788
+ "(#{expr})".lit
789
+ else
790
+ raise(Error, 'Invalid filter argument')
791
+ end
792
+ end
793
+
794
+ # SQL statement for formatting an insert statement with default values
795
+ def insert_default_values_sql
796
+ "INSERT INTO #{source_list(@opts[:from])} DEFAULT VALUES"
797
+ end
798
+
799
+ # Inverts the given order by breaking it into a list of column references
800
+ # and inverting them.
801
+ #
802
+ # dataset.invert_order([:id.desc]]) #=> [:id]
803
+ # dataset.invert_order(:category, :price.desc]) #=>
804
+ # [:category.desc, :price]
805
+ def invert_order(order)
806
+ return nil unless order
807
+ new_order = []
808
+ order.map do |f|
809
+ case f
810
+ when SQL::OrderedExpression
811
+ SQL::OrderedExpression.new(f.expression, !f.descending)
812
+ else
813
+ SQL::OrderedExpression.new(f)
814
+ end
815
+ end
816
+ end
817
+
818
+ # SQL fragment specifying a JOIN type, converts underscores to
819
+ # spaces and upcases.
820
+ def join_type_sql(join_type)
821
+ "#{join_type.to_s.gsub('_', ' ').upcase} JOIN"
822
+ end
823
+
824
+ # Returns a qualified column name (including a table name) if the column
825
+ # name isn't already qualified.
826
+ def qualified_column_name(column, table)
827
+ if Symbol === column
828
+ c_table, column, c_alias = split_symbol(column)
829
+ schema, table, t_alias = split_symbol(table) if Symbol === table
830
+ c_table ||= t_alias || table
831
+ ::Sequel::SQL::QualifiedIdentifier.new(c_table, column)
832
+ else
833
+ column
834
+ end
835
+ end
836
+
837
+ # Converts an array of source names into into a comma separated list.
838
+ def source_list(source)
839
+ if source.nil? || source.empty?
840
+ raise Error, 'No source specified for query'
841
+ end
842
+ auto_alias_count = @opts[:num_dataset_sources] || 0
843
+ m = source.map do |s|
844
+ case s
845
+ when Dataset
846
+ auto_alias_count += 1
847
+ s.to_table_reference("t#{auto_alias_count}")
848
+ else
849
+ table_ref(s)
850
+ end
851
+ end
852
+ m.join(COMMA_SEPARATOR)
853
+ end
854
+
855
+ # Splits the symbol into three parts. Each part will
856
+ # either be a string or nil.
857
+ #
858
+ # For columns, these parts are the table, column, and alias.
859
+ # For tables, these parts are the schema, table, and alias.
860
+ def split_symbol(sym)
861
+ s = sym.to_s
862
+ if m = COLUMN_REF_RE1.match(s)
863
+ m[1..3]
864
+ elsif m = COLUMN_REF_RE2.match(s)
865
+ [nil, m[1], m[2]]
866
+ elsif m = COLUMN_REF_RE3.match(s)
867
+ [m[1], m[2], nil]
868
+ else
869
+ [nil, s, nil]
870
+ end
871
+ end
872
+
873
+ # SQL fragement specifying a table name.
874
+ def table_ref(t)
875
+ case t
876
+ when Dataset
877
+ t.to_table_reference
878
+ when Hash
879
+ t.map {|k, v| "#{table_ref(k)} #{table_ref(v)}"}.join(COMMA_SEPARATOR)
880
+ when Symbol
881
+ symbol_to_column_ref(t)
882
+ when String
883
+ quote_identifier(t)
884
+ else
885
+ literal(t)
886
+ end
887
+ end
888
+ end
889
+ end