sequel-impala 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/LICENSE +462 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +39 -0
  6. data/lib/driver/commons-logging-1.2.jar +0 -0
  7. data/lib/driver/hadoop-common-2.6.0.jar +0 -0
  8. data/lib/driver/hadoop-core-2.6.0.jar +0 -0
  9. data/lib/driver/hive-exec-1.1.0.jar +0 -0
  10. data/lib/driver/hive-jdbc-1.1.0.jar +0 -0
  11. data/lib/driver/hive-metastore-1.1.0.jar +0 -0
  12. data/lib/driver/hive-service-1.1.0.jar +0 -0
  13. data/lib/driver/httpclient-4.3.jar +0 -0
  14. data/lib/driver/httpcore-4.3.jar +0 -0
  15. data/lib/driver/libfb303-0.9.0.jar +0 -0
  16. data/lib/driver/slf4j-api-1.7.5.jar +0 -0
  17. data/lib/impala.rb +47 -0
  18. data/lib/impala/connection.rb +117 -0
  19. data/lib/impala/cursor.rb +157 -0
  20. data/lib/impala/protocol.rb +8 -0
  21. data/lib/impala/protocol/beeswax_constants.rb +15 -0
  22. data/lib/impala/protocol/beeswax_service.rb +766 -0
  23. data/lib/impala/protocol/beeswax_types.rb +193 -0
  24. data/lib/impala/protocol/cli_service_constants.rb +60 -0
  25. data/lib/impala/protocol/cli_service_types.rb +1452 -0
  26. data/lib/impala/protocol/facebook_service.rb +706 -0
  27. data/lib/impala/protocol/fb303_constants.rb +15 -0
  28. data/lib/impala/protocol/fb303_types.rb +25 -0
  29. data/lib/impala/protocol/hive_metastore_constants.rb +53 -0
  30. data/lib/impala/protocol/hive_metastore_types.rb +698 -0
  31. data/lib/impala/protocol/impala_hive_server2_service.rb +29 -0
  32. data/lib/impala/protocol/impala_service.rb +377 -0
  33. data/lib/impala/protocol/impala_service_constants.rb +13 -0
  34. data/lib/impala/protocol/impala_service_types.rb +90 -0
  35. data/lib/impala/protocol/status_constants.rb +13 -0
  36. data/lib/impala/protocol/status_types.rb +46 -0
  37. data/lib/impala/protocol/t_c_l_i_service.rb +948 -0
  38. data/lib/impala/protocol/thrift_hive_metastore.rb +4707 -0
  39. data/lib/impala/version.rb +3 -0
  40. data/lib/jdbc/hive2.rb +46 -0
  41. data/lib/sequel/adapters/impala.rb +123 -0
  42. data/lib/sequel/adapters/jdbc/hive2.rb +26 -0
  43. data/lib/sequel/adapters/shared/impala.rb +635 -0
  44. data/lib/sequel/extensions/csv_to_parquet.rb +112 -0
  45. data/spec/database_test.rb +56 -0
  46. data/spec/dataset_test.rb +1268 -0
  47. data/spec/files/bad_down_migration/001_create_alt_basic.rb +4 -0
  48. data/spec/files/bad_down_migration/002_create_alt_advanced.rb +4 -0
  49. data/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  50. data/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb +9 -0
  51. data/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb +3 -0
  52. data/spec/files/bad_up_migration/001_create_alt_basic.rb +4 -0
  53. data/spec/files/bad_up_migration/002_create_alt_advanced.rb +3 -0
  54. data/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb +9 -0
  55. data/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb +9 -0
  56. data/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb +4 -0
  57. data/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb +9 -0
  58. data/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb +9 -0
  59. data/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  60. data/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb +9 -0
  61. data/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb +4 -0
  62. data/spec/files/integer_migrations/001_create_sessions.rb +9 -0
  63. data/spec/files/integer_migrations/002_create_nodes.rb +9 -0
  64. data/spec/files/integer_migrations/003_3_create_users.rb +4 -0
  65. data/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  66. data/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb +9 -0
  67. data/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb +9 -0
  68. data/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb +9 -0
  69. data/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb +4 -0
  70. data/spec/files/reversible_migrations/001_reversible.rb +5 -0
  71. data/spec/files/reversible_migrations/002_reversible.rb +5 -0
  72. data/spec/files/reversible_migrations/003_reversible.rb +5 -0
  73. data/spec/files/reversible_migrations/004_reversible.rb +5 -0
  74. data/spec/files/reversible_migrations/005_reversible.rb +10 -0
  75. data/spec/files/timestamped_migrations/1273253849_create_sessions.rb +9 -0
  76. data/spec/files/timestamped_migrations/1273253851_create_nodes.rb +9 -0
  77. data/spec/files/timestamped_migrations/1273253853_3_create_users.rb +4 -0
  78. data/spec/impala_test.rb +285 -0
  79. data/spec/migrator_test.rb +240 -0
  80. data/spec/plugin_test.rb +91 -0
  81. data/spec/prepared_statement_test.rb +327 -0
  82. data/spec/schema_test.rb +356 -0
  83. data/spec/spec_helper.rb +15 -0
  84. data/spec/timezone_test.rb +86 -0
  85. data/spec/type_test.rb +99 -0
  86. metadata +239 -0
@@ -0,0 +1,112 @@
1
+ require 'securerandom'
2
+ require 'shellwords'
3
+
4
+ module Sequel::CsvToParquet
5
+ # Load a CSV file into an existing parquet table. By default,
6
+ # assumes the CSV file has headers that match the column names
7
+ # in the parquet table. If this isn't true, the :headers or
8
+ # :mapping option should be specified.
9
+ #
10
+ # This works by adding the CSV file to HDFS via hdfs -put, then
11
+ # creating an external CSV table in Impala, then inserting into
12
+ # parquet table from the CSV table.
13
+ #
14
+ # Options:
15
+ # :empty_null :: Convert empty CSV cells to \N when adding to HDFS,
16
+ # so Impala will treat them as NULL instead of the
17
+ # empty string.
18
+ # :headers :: Specify the headers to use in the CSV file, assuming the
19
+ # csv file does not contain headers. If :skip_headers is set
20
+ # to true, this will ignore the existing headers in the file.
21
+ # :hdfs_tmp_dir :: The temporary HDFS directory to use when uploading.
22
+ # :mapping :: Override the mapping of the CSV columns to the parquet table
23
+ # columns. By default, assumes the CSV header names are the
24
+ # same as the parquet table columns, and uses both. If specified
25
+ # this should be a hash with parquet column symbol keys, with the
26
+ # value being the value to insert into the parquet table. This
27
+ # can be used to transform the data from the CSV table when loading
28
+ # it into the parquet table.
29
+ # :overwrite :: Set to true to overwrite existing data in the parquet table
30
+ # with the information from the CSV file. The default is to
31
+ # append the data to the existing parquet table.
32
+ # :skip_header :: Specifies that the first row contains headers and should
33
+ # be skipped when copying the CSV file to HDFS. If not
34
+ # specified, headers are skipped unless the :headers option
35
+ # is given.
36
+ # :tmp_table :: The temporary table name to use for the CSV table.
37
+ # :types :: Specify the types to use for the temporary CSV table. By default,
38
+ # it introspects the parquet table to get the type information, and
39
+ # uses the type for the matching column name.
40
+ def load_csv(local_csv_path, into_table, opts={})
41
+ tmp_num = SecureRandom.hex(8)
42
+ hdfs_tmp_dir = opts[:hdfs_tmp_dir] || "/tmp/cvs-#{tmp_num}"
43
+ hdfs_tmp_file = "#{hdfs_tmp_dir}/#{File.basename(local_csv_path)}"
44
+ tmp_table = opts[:tmp_table] || "csv_#{tmp_num}"
45
+
46
+ skip_header = opts.fetch(:skip_header, !opts.has_key?(:headers))
47
+ mapping = opts[:mapping]
48
+ overwrite = opts[:overwrite]
49
+
50
+ if columns = opts[:headers]
51
+ columns = columns.split(',') if columns.is_a?(String)
52
+ else
53
+ columns = File.open(local_csv_path).readline.chomp.split(',').map(&:downcase).map(&:to_sym)
54
+ end
55
+
56
+ into_table_columns = describe(into_table) rescue nil
57
+
58
+ if types = opts[:types]
59
+ types = types.split(',') if types.is_a?(String)
60
+ elsif (into_table_columns)
61
+ sch = Hash[into_table_columns.map { |h| [h[:name].downcase.to_sym, h[:type]]}]
62
+ types = columns.map { |col| sch[col] || "string" }
63
+ else
64
+ types = ["string"] * columns.length
65
+ end
66
+
67
+ unless types.length == columns.length
68
+ raise ArgumentError, "number of types doesn't match number of columns"
69
+ end
70
+
71
+ system("hdfs", "dfs", "-mkdir", hdfs_tmp_dir)
72
+
73
+ pipeline = if skip_header
74
+ "tail -n +2 #{Shellwords.shellescape(local_csv_path)}"
75
+ else
76
+ "cat #{Shellwords.shellescape(local_csv_path)}"
77
+ end
78
+
79
+ case opts[:empty_null]
80
+ when nil, false
81
+ when :perl
82
+ pipeline << ' | perl -p -e \'s/(^|,)(?=,|$)/\\1\\\\N/g\''
83
+ else
84
+ pipeline << (' | sed -r \'s/(^|,)(,|$)/\\1\\\\N\\2/g\'' * 2 )
85
+ end
86
+
87
+ system("#{pipeline} | hdfs dfs -put - #{Shellwords.shellescape(hdfs_tmp_file)}")
88
+
89
+ create_table(tmp_table, :external=>true, :field_term=>',', :location=>hdfs_tmp_dir) do
90
+ columns.zip(types) do |c, t|
91
+ column c, t
92
+ end
93
+ end
94
+
95
+ ds = from(into_table)
96
+ ds = ds.insert_overwrite if overwrite
97
+
98
+ if mapping
99
+ table_columns, csv_columns = mapping.to_a.transpose
100
+ else
101
+ table_columns = csv_columns = into_table_columns.map { |h| h[:name].to_sym }
102
+ end
103
+ ds.insert(table_columns, from(tmp_table).select(*csv_columns))
104
+
105
+ ensure
106
+ system("hdfs", "dfs", "-rm", hdfs_tmp_file)
107
+ system("hdfs", "dfs", "-rmdir", hdfs_tmp_dir)
108
+ drop_table?(tmp_table)
109
+ end
110
+ end
111
+
112
+ Sequel::Database.register_extension(:csv_to_parquet, Sequel::CsvToParquet)
@@ -0,0 +1,56 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')
2
+
3
+ describe Sequel::Database do
4
+ before do
5
+ @db = DB
6
+ end
7
+
8
+ it "should provide disconnect functionality" do
9
+ @db.disconnect
10
+ @db.pool.size.must_equal 0
11
+ @db.test_connection
12
+ @db.pool.size.must_equal 1
13
+ end
14
+
15
+ it "should provide disconnect functionality after preparing a statement" do
16
+ @db.create_table!(:items){Integer :i}
17
+ @db[:items].prepare(:first, :a).call
18
+ @db.disconnect
19
+ @db.pool.size.must_equal 0
20
+ @db.drop_table?(:items)
21
+ end
22
+
23
+ it "should raise Sequel::DatabaseError on invalid SQL" do
24
+ proc{@db << "S"}.must_raise(Sequel::DatabaseError)
25
+ end
26
+
27
+ it "should store underlying wrapped exception in Sequel::DatabaseError" do
28
+ begin
29
+ @db << "SELECT"
30
+ rescue Sequel::DatabaseError=>e
31
+ if defined?(Java::JavaLang::Exception)
32
+ (e.wrapped_exception.is_a?(Exception) || e.wrapped_exception.is_a?(Java::JavaLang::Exception)).must_equal true
33
+ else
34
+ e.wrapped_exception.must_be_kind_of(Exception)
35
+ end
36
+ end
37
+ end
38
+
39
+ it "should not have the connection pool swallow non-StandardError based exceptions" do
40
+ proc{@db.pool.hold{raise Interrupt, "test"}}.must_raise(Interrupt)
41
+ end
42
+
43
+ it "should be able to disconnect connections more than once without exceptions" do
44
+ conn = @db.synchronize{|c| c}
45
+ @db.disconnect
46
+ @db.disconnect_connection(conn)
47
+ @db.disconnect_connection(conn)
48
+ end
49
+
50
+ it "should provide ability to check connections for validity" do
51
+ conn = @db.synchronize{|c| c}
52
+ @db.valid_connection?(conn).must_equal true
53
+ @db.disconnect
54
+ @db.valid_connection?(conn).must_equal false
55
+ end
56
+ end
@@ -0,0 +1,1268 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')
2
+
3
+ describe "Simple Dataset operations" do
4
+ before(:all) do
5
+ @db = DB
6
+ @ds = @db[:items]
7
+ end
8
+ after do
9
+ @db.drop_table?(:items)
10
+ end
11
+
12
+ it "should support sequential primary keys with a Bignum" do
13
+ @db.create_table!(:items) do
14
+ primary_key :id, :type=>Bignum
15
+ Integer :number
16
+ end
17
+ @ds << {:id=>1, :number=>20}
18
+ @ds << {:id=>2, :number=>30}
19
+ @ds.order(:number).all.must_equal [{:id => 1, :number=>20}, {:id => 2, :number=>30}]
20
+ end
21
+
22
+ it "should have insert work correctly with static SQL" do
23
+ @db.create_table!(:items) do
24
+ primary_key :id
25
+ Integer :number
26
+ end
27
+ @db["INSERT INTO #{@ds.literal(:items)} (id, number) VALUES (2, 30)"].insert
28
+ @ds.all.must_equal [{:id => 2, :number=>30}]
29
+ end
30
+
31
+ it "should have insert work correctly when inserting a row with all NULL values" do
32
+ @db.create_table!(:items) do
33
+ Integer :id
34
+ Integer :number
35
+ end
36
+ @ds.insert
37
+ @ds.all.must_equal [{:id=>nil, :number=>nil}]
38
+ end
39
+
40
+ it "should support iterating over large numbers of records with paged_each" do
41
+ @db.create_table!(:items) do
42
+ Integer :id
43
+ Integer :number
44
+ end
45
+ @ds.import([:id, :number], (1..10).map{|i| [i, i*10]})
46
+
47
+ [:offset, :filter].each do |strategy|
48
+ rows = []
49
+ @ds.order(:number).paged_each(:rows_per_fetch=>5, :strategy=>strategy){|row| rows << row}
50
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}})
51
+
52
+ rows = []
53
+ @ds.order(:number).paged_each(:rows_per_fetch=>3, :strategy=>strategy){|row| rows << row}
54
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}})
55
+
56
+ rows = []
57
+ @ds.order(:number, :id).paged_each(:rows_per_fetch=>5, :strategy=>strategy){|row| rows << row}
58
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}})
59
+
60
+ rows = []
61
+ @ds.reverse_order(:number).paged_each(:rows_per_fetch=>5, :strategy=>strategy){|row| rows << row}
62
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}}.reverse)
63
+
64
+ rows = []
65
+ @ds.order(Sequel.desc(:number), :id).paged_each(:rows_per_fetch=>5, :strategy=>strategy){|row| rows << row}
66
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}}.reverse)
67
+ end
68
+
69
+ rows = []
70
+ @ds.order(:number).limit(5, 2).paged_each(:rows_per_fetch=>3){|row| rows << row}
71
+ rows.must_equal((3..7).map{|i| {:id=>i, :number=>i*10}})
72
+
73
+ rows = []
74
+ @ds.order(Sequel.*(:number, 2)).paged_each(:rows_per_fetch=>5){|row| rows << row}
75
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}})
76
+
77
+ rows = []
78
+ @ds.order(Sequel.*(:number, 2)).paged_each(:rows_per_fetch=>5, :strategy=>:filter, :filter_values=>proc{|row, _| [row[:number] * 2]}){|row| rows << row}
79
+ rows.must_equal((1..10).map{|i| {:id=>i, :number=>i*10}})
80
+
81
+ if RUBY_ENGINE == 'jruby'
82
+ # check retrival with varying fetch sizes
83
+ array = (1..10).to_a
84
+ [1, 2, 5, 10, 20].each do |i|
85
+ @ds.with_fetch_size(i).select_order_map(:id).must_equal array
86
+ end
87
+ end
88
+ end
89
+
90
+ it "should fetch correctly with a limit and offset for different combinations of from and join tables" do
91
+ @db.create_table!(:items) do
92
+ Integer :id
93
+ Integer :number
94
+ end
95
+ @ds.insert(:id=>1, :number=>10)
96
+ @db.create_table!(:items2){primary_key :id2; Integer :number2}
97
+ @db[:items2].insert(:id2=>1, :number2=>10)
98
+ @ds.from(:items, :items2).order(:id).limit(2, 0).all.must_equal [{:id=>1, :number=>10, :id2=>1, :number2=>10}]
99
+ @ds.from(:items___i, :items2___i2).order(:id).limit(2, 0).all.must_equal [{:id=>1, :number=>10, :id2=>1, :number2=>10}]
100
+ @ds.cross_join(:items2).order(:id).limit(2, 0).all.must_equal [{:id=>1, :number=>10, :id2=>1, :number2=>10}]
101
+ @ds.from(:items___i).cross_join(:items2___i2).order(:id).limit(2, 0).all.must_equal [{:id=>1, :number=>10, :id2=>1, :number2=>10}]
102
+ @ds.cross_join(:items2___i).cross_join(@db[:items2].select(:id2___id3, :number2___number3)).order(:id).limit(2, 0).all.must_equal [{:id=>1, :number=>10, :id2=>1, :number2=>10, :id3=>1, :number3=>10}]
103
+
104
+ @ds.from(:items, :items2).order(:id).limit(2, 1).all.must_equal []
105
+ @ds.from(:items___i, :items2___i2).order(:id).limit(2, 1).all.must_equal []
106
+ @ds.cross_join(:items2).order(:id).limit(2, 1).all.must_equal []
107
+ @ds.from(:items___i).cross_join(:items2___i2).order(:id).limit(2, 1).all.must_equal []
108
+ @ds.cross_join(:items2___i).cross_join(@db[:items2].select(:id2___id3, :number2___number3)).order(:id).limit(2, 1).all.must_equal []
109
+ @db.drop_table(:items2)
110
+ end
111
+
112
+ end
113
+
114
+ describe "Simple Dataset operations" do
115
+ before(:all) do
116
+ @db = DB
117
+ @db.create_table!(:items) do
118
+ primary_key :id
119
+ Integer :number
120
+ end
121
+ @ds = @db[:items]
122
+ @ds.insert(:id=>1, :number=>10)
123
+ end
124
+ after(:all) do
125
+ @db.drop_table?(:items)
126
+ end
127
+
128
+ it "should join correctly" do
129
+ @ds.join(:items___b, :id=>:id).select_all(:items).all.must_equal [{:id=>1, :number=>10}]
130
+ @ds.join(:items___b, [:id]).select_all(:items).all.must_equal [{:id=>1, :number=>10}]
131
+ end
132
+
133
+ it "should correctly handle subqueries" do
134
+ @ds.from_self(:alias=>:a).all.must_equal [{:id=>1, :number=>10}]
135
+ @ds.join(@ds.as(:a), :id=>:id).select_all(:a).all.must_equal [{:id=>1, :number=>10}]
136
+ end
137
+
138
+ it "should graph correctly" do
139
+ a = [{:items=>{:id=>1, :number=>10}, :b=>{:id=>1, :number=>10}}]
140
+ pr = proc{|t| @ds.graph(t, {:id=>:id}, :table_alias=>:b).extension(:graph_each).all.must_equal a}
141
+ pr[:items]
142
+ pr[:items___foo]
143
+ pr[Sequel.identifier(:items)]
144
+ pr[Sequel.identifier('items')]
145
+ pr[Sequel.as(:items, :foo)]
146
+ pr[Sequel.as(Sequel.identifier('items'), 'foo')]
147
+ end
148
+
149
+ it "should graph correctly with a subselect" do
150
+ @ds.from_self(:alias=>:items).graph(@ds.from_self, {:id=>:id}, :table_alias=>:b).extension(:graph_each).all.must_equal [{:items=>{:id=>1, :number=>10}, :b=>{:id=>1, :number=>10}}]
151
+ end
152
+
153
+ it "should iterate over records as they come in" do
154
+ called = false
155
+ @ds.each{|row| called = true; row.must_equal(:id=>1, :number=>10)}
156
+ called.must_equal true
157
+ end
158
+
159
+ it "should fetch all results correctly" do
160
+ @ds.all.must_equal [{:id=>1, :number=>10}]
161
+ end
162
+
163
+ it "should fetch a single row correctly" do
164
+ @ds.first.must_equal(:id=>1, :number=>10)
165
+ @ds.single_record.must_equal(:id=>1, :number=>10)
166
+ @ds.single_record!.must_equal(:id=>1, :number=>10)
167
+ end
168
+
169
+ it "should work correctly when returning from each without iterating over the whole result set" do
170
+ @ds.order(:id).each{|v| break v}.must_equal(:id=>1, :number=>10)
171
+ @ds.reverse(:id).each{|v| break v}.must_equal(:id=>1, :number=>10)
172
+ end
173
+
174
+ it "should fetch a single value correctly" do
175
+ @ds.get(:id).must_equal 1
176
+ @ds.select(:id).single_value.must_equal 1
177
+ @ds.select(:id).single_value!.must_equal 1
178
+ end
179
+
180
+ it "should have distinct work with limit" do
181
+ @ds.limit(1).distinct.all.must_equal [{:id=>1, :number=>10}]
182
+ end
183
+
184
+ it "should fetch correctly with a limit" do
185
+ @ds.order(:id).limit(1).all.must_equal [{:id=>1, :number=>10}]
186
+ end
187
+
188
+ it "should fetch correctly with a limit and offset" do
189
+ @ds.order(:id).limit(1, 0).all.must_equal [{:id=>1, :number=>10}]
190
+ @ds.order(:id).limit(1, 1).all.must_equal []
191
+ end
192
+
193
+ it "should fetch correctly with just offset" do
194
+ @ds.order(:id).offset(0).all.must_equal [{:id=>1, :number=>10}]
195
+ @ds.order(:id).offset(1).all.must_equal []
196
+ end
197
+
198
+ it "should fetch correctly with a limit and offset using seperate methods" do
199
+ @ds.order(:id).limit(1).offset(0).all.must_equal [{:id=>1, :number=>10}]
200
+ @ds.order(:id).limit(1).offset(1).all.must_equal []
201
+ end
202
+
203
+ it "should provide correct columns when using a limit and offset" do
204
+ ds = @ds.order(:id).limit(1, 1)
205
+ ds.all
206
+ ds.columns.must_equal [:id, :number]
207
+ @ds.order(:id).limit(1, 1).columns.must_equal [:id, :number]
208
+ end
209
+
210
+ it "should fetch correctly with a limit and offset without an order" do
211
+ ds = @ds.order(1)
212
+ ds.limit(2, 1).all.must_equal []
213
+ ds.join(:items___i, :id=>:id).select(:items__id___s, :i__id___id2).limit(2, 1).all.must_equal []
214
+ ds.join(:items___i, :id=>:id).select(:items__id).limit(2, 1).all.must_equal []
215
+ ds.join(:items___i, :id=>:id).select(Sequel.qualify(:items, :id)).limit(2, 1).all.must_equal []
216
+ ds.join(:items___i, :id=>:id).select(Sequel.qualify(:items, :id).as(:s)).limit(2, 1).all.must_equal []
217
+ end
218
+
219
+ it "should be orderable by column number" do
220
+ @ds.order(2, 1).select_map([:id, :number]).must_equal [[1, 10]]
221
+ end
222
+
223
+ it "should fetch correctly with a limit in an IN subselect" do
224
+ @ds.where(:id=>@ds.select(:id).order(:id).limit(1)).all.must_equal [{:id=>1, :number=>10}]
225
+ end
226
+
227
+ it "should fetch correctly with a limit and offset in an IN subselect" do
228
+ @ds.where(:id=>@ds.select(:id).order(:id).limit(2, 0)).all.must_equal [{:id=>1, :number=>10}]
229
+ @ds.where(:id=>@ds.select(:id).order(:id).limit(2, 1)).all.must_equal []
230
+ end
231
+
232
+ it "should fetch correctly when using limit and offset in a from_self" do
233
+ ds = @ds.order(:id).limit(1, 1).from_self
234
+ ds.all.must_equal []
235
+ ds.columns.must_equal [:id, :number]
236
+ @ds.order(:id).limit(1, 1).columns.must_equal [:id, :number]
237
+ end
238
+
239
+ it "should fetch correctly when using nested limit and offset in a from_self" do
240
+ ds = @ds.order(:id).limit(1, 0).from_self.reverse_order(:number).limit(1, 0)
241
+ ds.all.must_equal [{:number=>10, :id=>1}]
242
+ ds.columns.must_equal [:id, :number]
243
+ @ds.order(:id).limit(1, 0).from_self.reverse_order(:number).limit(1, 0).columns.must_equal [:id, :number]
244
+ end
245
+
246
+ it "should alias columns correctly" do
247
+ @ds.select(:id___x, :number___n).first.must_equal(:x=>1, :n=>10)
248
+ end
249
+
250
+ it "should handle true/false properly" do
251
+ @ds.filter(Sequel::TRUE).select_map(:number).must_equal [10]
252
+ @ds.filter(Sequel::FALSE).select_map(:number).must_equal []
253
+ @ds.filter(true).select_map(:number).must_equal [10]
254
+ @ds.filter(false).select_map(:number).must_equal []
255
+ @ds.filter({:id=>1}=>true).select_map(:number).must_equal [10]
256
+ @ds.filter({:id=>1}=>false).select_map(:number).must_equal []
257
+ @ds.literal(true)
258
+ @ds.literal(false)
259
+ end
260
+ end
261
+
262
+ describe Sequel::Dataset do
263
+ before(:all) do
264
+ DB.create_table!(:test) do
265
+ String :name
266
+ Integer :value
267
+ end
268
+ @d = DB[:test]
269
+ @d.multi_insert([{:name => 'abc', :value => 123}, {:name => 'abc', :value => 456}, {:name => 'def', :value => 789}])
270
+ end
271
+ after(:all) do
272
+ DB.drop_table?(:test)
273
+ end
274
+
275
+ it "should correctly return avg" do
276
+ @d.avg(:value).to_i.must_equal 456
277
+ end
278
+
279
+ it "should correctly return sum" do
280
+ @d.sum(:value).to_i.must_equal 1368
281
+ end
282
+
283
+ it "should correctly return max" do
284
+ @d.max(:value).to_i.must_equal 789
285
+ end
286
+
287
+ it "should correctly return min" do
288
+ @d.min(:value).to_i.must_equal 123
289
+ end
290
+
291
+ it "should return the correct record count" do
292
+ @d.count.must_equal 3
293
+ end
294
+
295
+ it "should handle functions with identifier names correctly" do
296
+ @d.get{sum.function(:value)}.must_equal 1368
297
+ end
298
+
299
+ it "should handle aggregate methods on limited datasets correctly" do
300
+ @d = @d.order(:name).limit(2)
301
+ @d.count.must_equal 2
302
+ @d.avg(:value).to_i.must_equal 289
303
+ @d.min(:value).to_i.must_equal 123
304
+ @d.reverse(:value).min(:value).to_i.must_equal 456
305
+ @d.max(:value).to_i.must_equal 456
306
+ @d.sum(:value).to_i.must_equal 579
307
+ @d.interval(:value).to_i.must_equal 333
308
+ end
309
+
310
+ it "should return the correct records" do
311
+ @d.order(:value).to_a.must_equal [{:name => 'abc', :value => 123}, {:name => 'abc', :value => 456}, {:name => 'def', :value => 789}]
312
+ end
313
+ end
314
+
315
+ describe Sequel::Database do
316
+ it "should correctly escape strings" do
317
+ ["\\\n",
318
+ "\\\\\n",
319
+ "\\\r\n",
320
+ "\\\\\r\n",
321
+ "\\\\\n\n",
322
+ "\\\\\r\n\r\n",
323
+ "\b\a'\0\3",
324
+ #"\t\b\a'\0\3",
325
+ "\\dingo",
326
+ "\\'dingo",
327
+ "\\\\''dingo",
328
+ ].each do |str|
329
+ DB.get(Sequel.cast(str, String)).must_equal str
330
+ str = "1#{str}1"
331
+ DB.get(Sequel.cast(str, String)).must_equal str
332
+ str = "#{str}#{str}"
333
+ DB.get(Sequel.cast(str, String)).must_equal str
334
+ end
335
+ end
336
+
337
+ it "should have a working table_exists?" do
338
+ t = :basdfdsafsaddsaf
339
+ DB.drop_table?(t)
340
+ DB.table_exists?(t).must_equal false
341
+ DB.create_table(t){Integer :a}
342
+ begin
343
+ DB.table_exists?(t).must_equal true
344
+ ensure
345
+ DB.drop_table(t)
346
+ end
347
+ end
348
+ end
349
+
350
+ describe "Simple Dataset operations" do
351
+ before do
352
+ DB.create_table!(:items) do
353
+ Integer :number
354
+ TrueClass :flag
355
+ end
356
+ @ds = DB[:items]
357
+ end
358
+ after do
359
+ DB.drop_table?(:items)
360
+ end
361
+
362
+ it "should deal with boolean conditions correctly" do
363
+ @ds.insert(:number=>1, :flag=>true)
364
+ @ds.insert(:number=>2, :flag=>false)
365
+ @ds.insert(:number=>3, :flag=>nil)
366
+ @ds.order!(:number)
367
+ @ds.filter(:flag=>true).map(:number).must_equal [1]
368
+ @ds.filter(:flag=>false).map(:number).must_equal [2]
369
+ @ds.filter(:flag=>nil).map(:number).must_equal [3]
370
+ @ds.exclude(:flag=>true).map(:number).must_equal [2, 3]
371
+ @ds.exclude(:flag=>false).map(:number).must_equal [1, 3]
372
+ @ds.exclude(:flag=>nil).map(:number).must_equal [1, 2]
373
+ end
374
+ end
375
+
376
+ describe "Dataset UNION, EXCEPT, and INTERSECT" do
377
+ before(:all) do
378
+ DB.create_table!(:i1){integer :number}
379
+ DB.create_table!(:i2){integer :number}
380
+ DB.create_table!(:i3){integer :number}
381
+ @ds1 = DB[:i1]
382
+ @ds1.insert(:number=>8)
383
+ @ds1.insert(:number=>10)
384
+ @ds1.insert(:number=>20)
385
+ @ds1.insert(:number=>38)
386
+ @ds2 = DB[:i2]
387
+ @ds2.insert(:number=>9)
388
+ @ds2.insert(:number=>10)
389
+ @ds2.insert(:number=>30)
390
+ @ds2.insert(:number=>39)
391
+ @ds3 = DB[:i3]
392
+ @ds3.insert(:number=>10)
393
+ @ds3.insert(:number=>40)
394
+ end
395
+ after(:all) do
396
+ DB.drop_table?(:i1, :i2, :i3)
397
+ end
398
+
399
+ it "should give the correct results for simple UNION" do
400
+ @ds1.union(@ds2).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 20 30 38 39'
401
+ end
402
+
403
+ it "should give the correct results for UNION when used with ordering and limits" do
404
+
405
+ @ds1.reverse_order(:number).union(@ds2).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 20 30 38 39'
406
+ @ds1.union(@ds2.reverse_order(:number)).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 20 30 38 39'
407
+
408
+ @ds1.reverse_order(:number).limit(1).union(@ds2).order(:number).map{|x| x[:number].to_s}.must_equal %w'9 10 30 38 39'
409
+ @ds2.reverse_order(:number).limit(1).union(@ds1).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 10 20 38 39'
410
+
411
+ @ds1.union(@ds2.order(:number).limit(1)).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 20 38'
412
+ @ds2.union(@ds1.order(:number).limit(1)).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 30 39'
413
+
414
+ @ds1.union(@ds2).limit(2).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9'
415
+ @ds2.union(@ds1).reverse_order(:number).limit(2).map{|x| x[:number].to_s}.must_equal %w'39 38'
416
+
417
+ @ds1.reverse_order(:number).limit(2).union(@ds2.reverse_order(:number).limit(2)).order(:number).limit(3).map{|x| x[:number].to_s}.must_equal %w'20 30 38'
418
+ @ds2.order(:number).limit(2).union(@ds1.order(:number).limit(2)).reverse_order(:number).limit(3).map{|x| x[:number].to_s}.must_equal %w'10 9 8'
419
+ end
420
+
421
+ it "should give the correct results for compound UNION" do
422
+ @ds1.union(@ds2).union(@ds3).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 20 30 38 39 40'
423
+ @ds1.union(@ds2.union(@ds3)).order(:number).map{|x| x[:number].to_s}.must_equal %w'8 9 10 20 30 38 39 40'
424
+ end
425
+ end
426
+
427
+ describe "Common Table Expressions" do
428
+ before(:all) do
429
+ @db = DB
430
+ @db.create_table!(:i1){Integer :id; Integer :parent_id}
431
+ @ds = @db[:i1]
432
+ @ds.insert(:id=>1)
433
+ @ds.insert(:id=>2)
434
+ @ds.insert(:id=>3, :parent_id=>1)
435
+ @ds.insert(:id=>4, :parent_id=>1)
436
+ @ds.insert(:id=>5, :parent_id=>3)
437
+ @ds.insert(:id=>6, :parent_id=>5)
438
+ end
439
+ after(:all) do
440
+ @db.drop_table?(:i1)
441
+ end
442
+
443
+ it "should give correct results for WITH" do
444
+ @db[:t].with(:t, @ds.filter(:parent_id=>nil).select(:id)).order(:id).map(:id).must_equal [1, 2]
445
+ end
446
+
447
+ it "should support joining a dataset with a CTE" do
448
+ @ds.inner_join(@db[:t].with(:t, @ds.filter(:parent_id=>nil)), :id => :id).select(:i1__id).order(:i1__id).map(:id).must_equal [1,2]
449
+ @db[:t].with(:t, @ds).inner_join(@db[:s].with(:s, @ds.filter(:parent_id=>nil)), :id => :id).select(:t__id).order(:t__id).map(:id).must_equal [1,2]
450
+ end
451
+
452
+ it "should support a subselect in the FROM clause with a CTE" do
453
+ @ds.from(@db[:t].with(:t, @ds)).select_order_map(:id).must_equal [1,2,3,4,5,6]
454
+ @db[:t].with(:t, @ds).from_self.select_order_map(:id).must_equal [1,2,3,4,5,6]
455
+ end
456
+
457
+ it "should support using a CTE inside a CTE" do
458
+ @db[:s].with(:s, @db[:t].with(:t, @ds)).select_order_map(:id).must_equal [1,2,3,4,5,6]
459
+ @db[:s].with_recursive(:s, @db[:t].with(:t, @ds), @db[:t2].with(:t2, @ds)).select_order_map(:id).must_equal [1,1,2,2,3,3,4,4,5,5,6,6]
460
+ end
461
+
462
+ it "should support using a CTE inside UNION/EXCEPT/INTERSECT" do
463
+ @ds.union(@db[:t].with(:t, @ds)).select_order_map(:id).must_equal [1,2,3,4,5,6]
464
+ if @ds.supports_intersect_except?
465
+ @ds.intersect(@db[:t].with(:t, @ds)).select_order_map(:id).must_equal [1,2,3,4,5,6]
466
+ @ds.except(@db[:t].with(:t, @ds)).select_order_map(:id).must_equal []
467
+ end
468
+ end
469
+ end
470
+
471
+ describe "Window Functions" do
472
+ before(:all) do
473
+ @db = DB
474
+ @db.create_table!(:i1){Integer :id; Integer :group_id; Integer :amount}
475
+ @ds = @db[:i1].order(:id)
476
+ @ds.insert(:id=>1, :group_id=>1, :amount=>1)
477
+ @ds.insert(:id=>2, :group_id=>1, :amount=>10)
478
+ @ds.insert(:id=>3, :group_id=>1, :amount=>100)
479
+ @ds.insert(:id=>4, :group_id=>2, :amount=>1000)
480
+ @ds.insert(:id=>5, :group_id=>2, :amount=>10000)
481
+ @ds.insert(:id=>6, :group_id=>2, :amount=>100000)
482
+ end
483
+ after(:all) do
484
+ @db.drop_table?(:i1)
485
+ end
486
+
487
+ it "should give correct results for aggregate window functions" do
488
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id).as(:sum)}.all.
489
+ must_equal [{:sum=>111, :id=>1}, {:sum=>111, :id=>2}, {:sum=>111, :id=>3}, {:sum=>111000, :id=>4}, {:sum=>111000, :id=>5}, {:sum=>111000, :id=>6}]
490
+ @ds.select(:id){sum(:amount).over.as(:sum)}.all.
491
+ must_equal [{:sum=>111111, :id=>1}, {:sum=>111111, :id=>2}, {:sum=>111111, :id=>3}, {:sum=>111111, :id=>4}, {:sum=>111111, :id=>5}, {:sum=>111111, :id=>6}]
492
+ end
493
+
494
+ it "should give correct results for ranking window functions with orders" do
495
+ @ds.select(:id){rank{}.over(:partition=>:group_id, :order=>:id).as(:rank)}.all.
496
+ must_equal [{:rank=>1, :id=>1}, {:rank=>2, :id=>2}, {:rank=>3, :id=>3}, {:rank=>1, :id=>4}, {:rank=>2, :id=>5}, {:rank=>3, :id=>6}]
497
+ @ds.select(:id){rank{}.over(:order=>id).as(:rank)}.all.
498
+ must_equal [{:rank=>1, :id=>1}, {:rank=>2, :id=>2}, {:rank=>3, :id=>3}, {:rank=>4, :id=>4}, {:rank=>5, :id=>5}, {:rank=>6, :id=>6}]
499
+ end
500
+
501
+ it "should give correct results for aggregate window functions with orders" do
502
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>:id).as(:sum)}.all.
503
+ must_equal [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1000, :id=>4}, {:sum=>11000, :id=>5}, {:sum=>111000, :id=>6}]
504
+ @ds.select(:id){sum(:amount).over(:order=>:id).as(:sum)}.all.
505
+ must_equal [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1111, :id=>4}, {:sum=>11111, :id=>5}, {:sum=>111111, :id=>6}]
506
+ end
507
+
508
+ it "should give correct results for aggregate window functions with frames" do
509
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>:id, :frame=>:all).as(:sum)}.all.
510
+ must_equal [{:sum=>111, :id=>1}, {:sum=>111, :id=>2}, {:sum=>111, :id=>3}, {:sum=>111000, :id=>4}, {:sum=>111000, :id=>5}, {:sum=>111000, :id=>6}]
511
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>:all).as(:sum)}.all.
512
+ must_equal [{:sum=>111111, :id=>1}, {:sum=>111111, :id=>2}, {:sum=>111111, :id=>3}, {:sum=>111111, :id=>4}, {:sum=>111111, :id=>5}, {:sum=>111111, :id=>6}]
513
+
514
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>:id, :frame=>:rows).as(:sum)}.all.
515
+ must_equal [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1000, :id=>4}, {:sum=>11000, :id=>5}, {:sum=>111000, :id=>6}]
516
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>:rows).as(:sum)}.all.
517
+ must_equal [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1111, :id=>4}, {:sum=>11111, :id=>5}, {:sum=>111111, :id=>6}]
518
+ end
519
+ end
520
+
521
+ describe Sequel::SQL::Constants do
522
+ before do
523
+ @db = DB
524
+ @ds = @db[:constants]
525
+ @c = proc do |v|
526
+ case v
527
+ when Time
528
+ v
529
+ when DateTime, String
530
+ Time.parse(v.to_s)
531
+ else
532
+ v
533
+ end
534
+ end
535
+ @c2 = proc{|v| v.is_a?(Date) ? v : Date.parse(v) }
536
+ end
537
+ after do
538
+ @db.drop_table?(:constants)
539
+ end
540
+
541
+ it "should have working CURRENT_TIMESTAMP" do
542
+ @db.create_table!(:constants){DateTime :ts}
543
+ @ds.insert(:ts=>Sequel::CURRENT_TIMESTAMP)
544
+ (Time.now - @c[@ds.get(:ts)]).must_be_close_to 0, 60
545
+ end
546
+ end
547
+
548
+ describe "Sequel::Dataset#import and #multi_insert" do
549
+ before do
550
+ @db = DB
551
+ @db.create_table!(:imp){Integer :i}
552
+ @ids = @db[:imp].order(:i)
553
+ end
554
+ after do
555
+ @db.drop_table?(:imp)
556
+ end
557
+
558
+ it "should import with multi_insert and an array of hashes" do
559
+ @ids.multi_insert([{:i=>10}, {:i=>20}])
560
+ @ids.all.must_equal [{:i=>10}, {:i=>20}]
561
+ end
562
+
563
+ it "should import with an array of arrays of values" do
564
+ @ids.import([:i], [[10], [20]])
565
+ @ids.all.must_equal [{:i=>10}, {:i=>20}]
566
+ end
567
+
568
+ it "should import with a dataset" do
569
+ @db.create_table!(:exp2){Integer :i}
570
+ @db[:exp2].import([:i], [[10], [20]])
571
+ @ids.import([:i], @db[:exp2])
572
+ @ids.all.must_equal [{:i=>10}, {:i=>20}]
573
+ @db.drop_table(:exp2)
574
+ end
575
+
576
+ it "should have import work with the :slice_size option" do
577
+ @ids.import([:i], [[10], [20], [30]], :slice_size=>1)
578
+ @ids.all.must_equal [{:i=>10}, {:i=>20}, {:i=>30}]
579
+ end
580
+
581
+ it "should import many rows at once" do
582
+ @ids.import([:i], (1..20).to_a.map{|x| [x]})
583
+ @ids.select_order_map(:i).must_equal((1..20).to_a)
584
+ end
585
+ end
586
+
587
+ describe "Sequel::Dataset convenience methods" do
588
+ before do
589
+ @db = DB
590
+ @db.create_table!(:a){Integer :a; Integer :b}
591
+ @ds = @db[:a].order(:a)
592
+ end
593
+ after do
594
+ @db.drop_table?(:a)
595
+ end
596
+
597
+ it "#empty? should return whether the dataset returns no rows" do
598
+ @ds.empty?.must_equal true
599
+ @ds.insert(20, 10)
600
+ @ds.empty?.must_equal false
601
+ end
602
+
603
+ it "#empty? should work correctly for datasets with limits" do
604
+ ds = @ds.limit(1)
605
+ ds.empty?.must_equal true
606
+ ds.insert(20, 10)
607
+ ds.empty?.must_equal false
608
+ end
609
+
610
+ it "#empty? should work correctly for datasets with limits and offsets" do
611
+ ds = @ds.order(:a).limit(1, 1)
612
+ ds.empty?.must_equal true
613
+ ds.insert(20, 10)
614
+ ds.empty?.must_equal true
615
+ ds.insert(20, 10)
616
+ ds.empty?.must_equal false
617
+ end
618
+
619
+ it "#group_and_count should return a grouping by count" do
620
+ @ds.group_and_count(:a).order{count(:a)}.all.must_equal []
621
+ @ds.insert(20, 10)
622
+ @ds.group_and_count(:a).order{count(:a)}.all.each{|h| h[:count] = h[:count].to_i}.must_equal [{:a=>20, :count=>1}]
623
+ @ds.insert(20, 30)
624
+ @ds.group_and_count(:a).order{count(:a)}.all.each{|h| h[:count] = h[:count].to_i}.must_equal [{:a=>20, :count=>2}]
625
+ @ds.insert(30, 30)
626
+ @ds.group_and_count(:a).order{count(:a)}.all.each{|h| h[:count] = h[:count].to_i}.must_equal [{:a=>30, :count=>1}, {:a=>20, :count=>2}]
627
+ end
628
+
629
+ it "#group_and_count should support column aliases" do
630
+ @ds.group_and_count(:a___c).order{count(:a)}.all.must_equal []
631
+ @ds.insert(20, 10)
632
+ @ds.group_and_count(:a___c).order{count(:a)}.all.each{|h| h[:count] = h[:count].to_i}.must_equal [{:c=>20, :count=>1}]
633
+ @ds.insert(20, 30)
634
+ @ds.group_and_count(:a___c).order{count(:a)}.all.each{|h| h[:count] = h[:count].to_i}.must_equal [{:c=>20, :count=>2}]
635
+ @ds.insert(30, 30)
636
+ @ds.group_and_count(:a___c).order{count(:a)}.all.each{|h| h[:count] = h[:count].to_i}.must_equal [{:c=>30, :count=>1}, {:c=>20, :count=>2}]
637
+ end
638
+
639
+ it "#range should return the range between the maximum and minimum values" do
640
+ @ds = @ds.unordered
641
+ @ds.insert(20, 10)
642
+ @ds.insert(30, 10)
643
+ @ds.range(:a).must_equal(20..30)
644
+ @ds.range(:b).must_equal(10..10)
645
+ end
646
+
647
+ it "#interval should return the different between the maximum and minimum values" do
648
+ @ds = @ds.unordered
649
+ @ds.insert(20, 10)
650
+ @ds.insert(30, 10)
651
+ @ds.interval(:a).to_i.must_equal 10
652
+ @ds.interval(:b).to_i.must_equal 0
653
+ end
654
+ end
655
+
656
+ describe "Sequel::Dataset main SQL methods" do
657
+ before do
658
+ @db = DB
659
+ @db.create_table!(:d){Integer :a; Integer :b}
660
+ @ds = @db[:d].order(:a)
661
+ end
662
+ after do
663
+ @db.drop_table?(:d)
664
+ end
665
+
666
+ it "#exists should return a usable exists clause" do
667
+ @ds.filter(@db[:d___c].filter(:c__a=>:d__b).exists).all.must_equal []
668
+ @ds.insert(20, 30)
669
+ @ds.insert(10, 20)
670
+ @ds.filter(@db[:d___c].filter(:c__a=>:d__b).exists).all.must_equal [{:a=>10, :b=>20}]
671
+ end
672
+
673
+ it "#filter and #exclude should work with placeholder strings" do
674
+ @ds.insert(20, 30)
675
+ @ds.filter("a > ?", 15).all.must_equal [{:a=>20, :b=>30}]
676
+ @ds.exclude("b < ?", 15).all.must_equal [{:a=>20, :b=>30}]
677
+ @ds.filter("b < ?", 15).invert.all.must_equal [{:a=>20, :b=>30}]
678
+ end
679
+
680
+ it "#and and #or should work correctly" do
681
+ @ds.insert(20, 30)
682
+ @ds.filter(:a=>20).and(:b=>30).all.must_equal [{:a=>20, :b=>30}]
683
+ @ds.filter(:a=>20).and(:b=>15).all.must_equal []
684
+ @ds.filter(:a=>20).or(:b=>15).all.must_equal [{:a=>20, :b=>30}]
685
+ @ds.filter(:a=>10).or(:b=>15).all.must_equal []
686
+ end
687
+
688
+ it "#select_group should work correctly" do
689
+ @ds.unordered!
690
+ @ds.select_group(:a).all.must_equal []
691
+ @ds.insert(20, 30)
692
+ @ds.select_group(:a).all.must_equal [{:a=>20}]
693
+ @ds.select_group(:b).all.must_equal [{:b=>30}]
694
+ @ds.insert(20, 40)
695
+ @ds.select_group(:a).all.must_equal [{:a=>20}]
696
+ @ds.order(:b).select_group(:b).all.must_equal [{:b=>30}, {:b=>40}]
697
+ end
698
+
699
+ it "#select_group should work correctly when aliasing" do
700
+ @ds.unordered!
701
+ @ds.insert(20, 30)
702
+ @ds.select_group(:b___c).all.must_equal [{:c=>30}]
703
+ end
704
+
705
+ it "#having should work correctly" do
706
+ @ds.unordered!
707
+ @ds.select{[b, max(a).as(c)]}.group(:b).having{max(a) > 30}.all.must_equal []
708
+ @ds.insert(20, 30)
709
+ @ds.select{[b, max(a).as(c)]}.group(:b).having{max(a) > 30}.all.must_equal []
710
+ @ds.insert(40, 20)
711
+ @ds.select{[b, max(a).as(c)]}.group(:b).having{max(a) > 30}.all.each{|h| h[:c] = h[:c].to_i}.must_equal [{:b=>20, :c=>40}]
712
+ end
713
+
714
+ it "#having should work without a previous group" do
715
+ @ds.unordered!
716
+ @ds.select{max(a).as(c)}.having{max(a) > 30}.all.must_equal []
717
+ @ds.insert(20, 30)
718
+ @ds.select{max(a).as(c)}.having{max(a) > 30}.all.must_equal []
719
+ @ds.insert(40, 20)
720
+ @ds.select{max(a).as(c)}.having{max(a) > 30}.all.each{|h| h[:c] = h[:c].to_i}.must_equal [{:c=>40}]
721
+ end
722
+ end
723
+
724
+ describe "Sequel::Dataset convenience methods" do
725
+ before do
726
+ @db = DB
727
+ @db.create_table!(:a){Integer :a; Integer :b; Integer :c; Integer :d}
728
+ @ds = @db[:a].order(:a)
729
+ @ds.insert(1, 2, 3, 4)
730
+ @ds.insert(5, 6, 7, 8)
731
+ end
732
+ after do
733
+ @db.drop_table?(:a)
734
+ end
735
+
736
+ it "should have working #map" do
737
+ @ds.map(:a).must_equal [1, 5]
738
+ @ds.map(:b).must_equal [2, 6]
739
+ @ds.map([:a, :b]).must_equal [[1, 2], [5, 6]]
740
+ end
741
+
742
+ it "should have working #to_hash" do
743
+ @ds.to_hash(:a).must_equal(1=>{:a=>1, :b=>2, :c=>3, :d=>4}, 5=>{:a=>5, :b=>6, :c=>7, :d=>8})
744
+ @ds.to_hash(:b).must_equal(2=>{:a=>1, :b=>2, :c=>3, :d=>4}, 6=>{:a=>5, :b=>6, :c=>7, :d=>8})
745
+ @ds.to_hash([:a, :b]).must_equal([1, 2]=>{:a=>1, :b=>2, :c=>3, :d=>4}, [5, 6]=>{:a=>5, :b=>6, :c=>7, :d=>8})
746
+
747
+ @ds.to_hash(:a, :b).must_equal(1=>2, 5=>6)
748
+ @ds.to_hash([:a, :c], :b).must_equal([1, 3]=>2, [5, 7]=>6)
749
+ @ds.to_hash(:a, [:b, :c]).must_equal(1=>[2, 3], 5=>[6, 7])
750
+ @ds.to_hash([:a, :c], [:b, :d]).must_equal([1, 3]=>[2, 4], [5, 7]=>[6, 8])
751
+ end
752
+
753
+ it "should have working #to_hash_groups" do
754
+ ds = @ds.order(*@ds.columns)
755
+ ds.insert(1, 2, 3, 9)
756
+ ds.to_hash_groups(:a).must_equal(1=>[{:a=>1, :b=>2, :c=>3, :d=>4}, {:a=>1, :b=>2, :c=>3, :d=>9}], 5=>[{:a=>5, :b=>6, :c=>7, :d=>8}])
757
+ ds.to_hash_groups(:b).must_equal(2=>[{:a=>1, :b=>2, :c=>3, :d=>4}, {:a=>1, :b=>2, :c=>3, :d=>9}], 6=>[{:a=>5, :b=>6, :c=>7, :d=>8}])
758
+ ds.to_hash_groups([:a, :b]).must_equal([1, 2]=>[{:a=>1, :b=>2, :c=>3, :d=>4}, {:a=>1, :b=>2, :c=>3, :d=>9}], [5, 6]=>[{:a=>5, :b=>6, :c=>7, :d=>8}])
759
+
760
+ ds.to_hash_groups(:a, :d).must_equal(1=>[4, 9], 5=>[8])
761
+ ds.to_hash_groups([:a, :c], :d).must_equal([1, 3]=>[4, 9], [5, 7]=>[8])
762
+ ds.to_hash_groups(:a, [:b, :d]).must_equal(1=>[[2, 4], [2, 9]], 5=>[[6, 8]])
763
+ ds.to_hash_groups([:a, :c], [:b, :d]).must_equal([1, 3]=>[[2, 4], [2, 9]], [5, 7]=>[[6, 8]])
764
+ end
765
+
766
+ it "should have working #select_map" do
767
+ @ds.select_map(:a).must_equal [1, 5]
768
+ @ds.select_map(:b).must_equal [2, 6]
769
+ @ds.select_map([:a]).must_equal [[1], [5]]
770
+ @ds.select_map([:a, :b]).must_equal [[1, 2], [5, 6]]
771
+
772
+ @ds.select_map(:a___e).must_equal [1, 5]
773
+ @ds.select_map(:b___e).must_equal [2, 6]
774
+ @ds.select_map([:a___e, :b___f]).must_equal [[1, 2], [5, 6]]
775
+ @ds.select_map([:a__a___e, :a__b___f]).must_equal [[1, 2], [5, 6]]
776
+ @ds.select_map([Sequel.expr(:a__a).as(:e), Sequel.expr(:a__b).as(:f)]).must_equal [[1, 2], [5, 6]]
777
+ @ds.select_map([Sequel.qualify(:a, :a).as(:e), Sequel.qualify(:a, :b).as(:f)]).must_equal [[1, 2], [5, 6]]
778
+ @ds.select_map([Sequel.identifier(:a).qualify(:a).as(:e), Sequel.qualify(:a, :b).as(:f)]).must_equal [[1, 2], [5, 6]]
779
+ end
780
+
781
+ it "should have working #select_order_map" do
782
+ @ds.select_order_map(:a).must_equal [1, 5]
783
+ @ds.select_order_map(Sequel.desc(:a__b)).must_equal [6, 2]
784
+ @ds.select_order_map(Sequel.desc(:a__b___e)).must_equal [6, 2]
785
+ @ds.select_order_map(Sequel.qualify(:a, :b).as(:e)).must_equal [2, 6]
786
+ @ds.select_order_map([:a]).must_equal [[1], [5]]
787
+ @ds.select_order_map([Sequel.desc(:a), :b]).must_equal [[5, 6], [1, 2]]
788
+
789
+ @ds.select_order_map(:a___e).must_equal [1, 5]
790
+ @ds.select_order_map(:b___e).must_equal [2, 6]
791
+ @ds.select_order_map([Sequel.desc(:a___e), :b___f]).must_equal [[5, 6], [1, 2]]
792
+ @ds.select_order_map([Sequel.desc(:a__a___e), :a__b___f]).must_equal [[5, 6], [1, 2]]
793
+ @ds.select_order_map([Sequel.desc(:a__a), Sequel.expr(:a__b).as(:f)]).must_equal [[5, 6], [1, 2]]
794
+ @ds.select_order_map([Sequel.qualify(:a, :a).desc, Sequel.qualify(:a, :b).as(:f)]).must_equal [[5, 6], [1, 2]]
795
+ @ds.select_order_map([Sequel.identifier(:a).qualify(:a).desc, Sequel.qualify(:a, :b).as(:f)]).must_equal [[5, 6], [1, 2]]
796
+ end
797
+
798
+ it "should have working #select_hash" do
799
+ @ds.select_hash(:a, :b).must_equal(1=>2, 5=>6)
800
+ @ds.select_hash(:a__a___e, :b).must_equal(1=>2, 5=>6)
801
+ @ds.select_hash(Sequel.expr(:a__a).as(:e), :b).must_equal(1=>2, 5=>6)
802
+ @ds.select_hash(Sequel.qualify(:a, :a).as(:e), :b).must_equal(1=>2, 5=>6)
803
+ @ds.select_hash(Sequel.identifier(:a).qualify(:a).as(:e), :b).must_equal(1=>2, 5=>6)
804
+ @ds.select_hash([:a, :c], :b).must_equal([1, 3]=>2, [5, 7]=>6)
805
+ @ds.select_hash(:a, [:b, :c]).must_equal(1=>[2, 3], 5=>[6, 7])
806
+ @ds.select_hash([:a, :c], [:b, :d]).must_equal([1, 3]=>[2, 4], [5, 7]=>[6, 8])
807
+ end
808
+
809
+ it "should have working #select_hash_groups" do
810
+ ds = @ds.order(*@ds.columns)
811
+ ds.insert(1, 2, 3, 9)
812
+ ds.select_hash_groups(:a, :d).must_equal(1=>[4, 9], 5=>[8])
813
+ ds.select_hash_groups(:a__a___e, :d).must_equal(1=>[4, 9], 5=>[8])
814
+ ds.select_hash_groups(Sequel.expr(:a__a).as(:e), :d).must_equal(1=>[4, 9], 5=>[8])
815
+ ds.select_hash_groups(Sequel.qualify(:a, :a).as(:e), :d).must_equal(1=>[4, 9], 5=>[8])
816
+ ds.select_hash_groups(Sequel.identifier(:a).qualify(:a).as(:e), :d).must_equal(1=>[4, 9], 5=>[8])
817
+ ds.select_hash_groups([:a, :c], :d).must_equal([1, 3]=>[4, 9], [5, 7]=>[8])
818
+ ds.select_hash_groups(:a, [:b, :d]).must_equal(1=>[[2, 4], [2, 9]], 5=>[[6, 8]])
819
+ ds.select_hash_groups([:a, :c], [:b, :d]).must_equal([1, 3]=>[[2, 4], [2, 9]], [5, 7]=>[[6, 8]])
820
+ end
821
+ end
822
+
823
+ describe "Sequel::Dataset DSL support" do
824
+ before do
825
+ @db = DB
826
+ @db.create_table!(:a){Integer :a; Integer :b}
827
+ @ds = @db[:a].order(:a)
828
+ end
829
+ after do
830
+ @db.drop_table?(:a)
831
+ end
832
+
833
+ it "should work with standard mathematical operators" do
834
+ @ds.insert(20, 10)
835
+ @ds.get{a + b}.to_i.must_equal 30
836
+ @ds.get{a - b}.to_i.must_equal 10
837
+ @ds.get{a * b}.to_i.must_equal 200
838
+ @ds.get{a / b}.to_i.must_equal 2
839
+ end
840
+
841
+ it "should work with bitwise AND and OR operators" do
842
+ @ds.insert(3, 5)
843
+ @ds.get{a.sql_number | b}.to_i.must_equal 7
844
+ @ds.get{a.sql_number & b}.to_i.must_equal 1
845
+ @ds.get{a.sql_number | b | 8}.to_i.must_equal 15
846
+ @ds.get{a.sql_number & b & 8}.to_i.must_equal 0
847
+ end
848
+
849
+ it "should work with the bitwise compliment operator" do
850
+ @ds.insert(-3, 3)
851
+ @ds.get{~a.sql_number}.to_i.must_equal 2
852
+ @ds.get{~b.sql_number}.to_i.must_equal(-4)
853
+ end
854
+
855
+ it "should work with the bitwise xor operator" do
856
+ @ds.insert(3, 5)
857
+ @ds.get{a.sql_number ^ b}.to_i.must_equal 6
858
+ @ds.get{a.sql_number ^ b ^ 1}.to_i.must_equal 7
859
+ end
860
+
861
+ it "should work with the modulus operator" do
862
+ @ds.insert(3, 5)
863
+ @ds.get{a.sql_number % 4}.to_i.must_equal 3
864
+ @ds.get{b.sql_number % 4}.to_i.must_equal 1
865
+ @ds.get{a.sql_number % 4 % 2}.to_i.must_equal 1
866
+ end
867
+
868
+ it "should work with inequality operators" do
869
+ @ds.insert(10, 11)
870
+ @ds.insert(11, 11)
871
+ @ds.insert(20, 19)
872
+ @ds.insert(20, 20)
873
+ @ds.filter{a > b}.select_order_map(:a).must_equal [20]
874
+ @ds.filter{a >= b}.select_order_map(:a).must_equal [11, 20, 20]
875
+ @ds.filter{a < b}.select_order_map(:a).must_equal [10]
876
+ @ds.filter{a <= b}.select_order_map(:a).must_equal [10, 11, 20]
877
+ end
878
+
879
+ it "should work with casting and string concatentation" do
880
+ @ds.insert(20, 20)
881
+ @ds.get{Sequel.cast(a, String).sql_string + Sequel.cast(b, String)}.must_equal '2020'
882
+ end
883
+
884
+ it "should work with ordering" do
885
+ @ds.insert(10, 20)
886
+ @ds.insert(20, 10)
887
+ @ds.order(:a, :b).all.must_equal [{:a=>10, :b=>20}, {:a=>20, :b=>10}]
888
+ @ds.order(Sequel.asc(:a), Sequel.asc(:b)).all.must_equal [{:a=>10, :b=>20}, {:a=>20, :b=>10}]
889
+ @ds.order(Sequel.desc(:a), Sequel.desc(:b)).all.must_equal [{:a=>20, :b=>10}, {:a=>10, :b=>20}]
890
+ end
891
+
892
+ it "should work with qualifying" do
893
+ @ds.insert(10, 20)
894
+ @ds.get(:a__b).must_equal 20
895
+ @ds.get{a__b}.must_equal 20
896
+ @ds.get(Sequel.qualify(:a, :b)).must_equal 20
897
+ end
898
+
899
+ it "should work with aliasing" do
900
+ @ds.insert(10, 20)
901
+ @ds.get(:a__b___c).must_equal 20
902
+ @ds.get{a__b.as(c)}.must_equal 20
903
+ @ds.get(Sequel.qualify(:a, :b).as(:c)).must_equal 20
904
+ @ds.get(Sequel.as(:b, :c)).must_equal 20
905
+ end
906
+
907
+ it "should work with selecting all columns of a table" do
908
+ @ds.insert(20, 10)
909
+ @ds.select_all(:a).all.must_equal [{:a=>20, :b=>10}]
910
+ end
911
+
912
+ it "should work with ranges as hash values" do
913
+ @ds.insert(20, 10)
914
+ @ds.filter(:a=>(10..30)).all.must_equal [{:a=>20, :b=>10}]
915
+ @ds.filter(:a=>(25..30)).all.must_equal []
916
+ @ds.filter(:a=>(10..15)).all.must_equal []
917
+ @ds.exclude(:a=>(10..30)).all.must_equal []
918
+ @ds.exclude(:a=>(25..30)).all.must_equal [{:a=>20, :b=>10}]
919
+ @ds.exclude(:a=>(10..15)).all.must_equal [{:a=>20, :b=>10}]
920
+ end
921
+
922
+ it "should work with nil as hash value" do
923
+ @ds.insert(20, nil)
924
+ @ds.filter(:a=>nil).all.must_equal []
925
+ @ds.filter(:b=>nil).all.must_equal [{:a=>20, :b=>nil}]
926
+ @ds.exclude(:b=>nil).all.must_equal []
927
+ @ds.exclude(:a=>nil).all.must_equal [{:a=>20, :b=>nil}]
928
+ end
929
+
930
+ it "should work with arrays as hash values" do
931
+ @ds.insert(20, 10)
932
+ @ds.filter(:a=>[10]).all.must_equal []
933
+ @ds.filter(:a=>[20, 10]).all.must_equal [{:a=>20, :b=>10}]
934
+ @ds.exclude(:a=>[10]).all.must_equal [{:a=>20, :b=>10}]
935
+ @ds.exclude(:a=>[20, 10]).all.must_equal []
936
+ end
937
+
938
+ it "should work with ranges as hash values" do
939
+ @ds.insert(20, 10)
940
+ @ds.filter(:a=>(10..30)).all.must_equal [{:a=>20, :b=>10}]
941
+ @ds.filter(:a=>(25..30)).all.must_equal []
942
+ @ds.filter(:a=>(10..15)).all.must_equal []
943
+ @ds.exclude(:a=>(10..30)).all.must_equal []
944
+ @ds.exclude(:a=>(25..30)).all.must_equal [{:a=>20, :b=>10}]
945
+ @ds.exclude(:a=>(10..15)).all.must_equal [{:a=>20, :b=>10}]
946
+ end
947
+
948
+ it "should work with CASE statements" do
949
+ @ds.insert(20, 10)
950
+ @ds.filter(Sequel.case({{:a=>20}=>20}, 0) > 0).all.must_equal [{:a=>20, :b=>10}]
951
+ @ds.filter(Sequel.case({{:a=>15}=>20}, 0) > 0).all.must_equal []
952
+ @ds.filter(Sequel.case({20=>20}, 0, :a) > 0).all.must_equal [{:a=>20, :b=>10}]
953
+ @ds.filter(Sequel.case({15=>20}, 0, :a) > 0).all.must_equal []
954
+ end
955
+
956
+ it "should work with multiple value arrays" do
957
+ @ds.insert(20, 10)
958
+ @ds.quote_identifiers = false
959
+ @ds.filter([:a, :b]=>[[20, 10]]).all.must_equal [{:a=>20, :b=>10}]
960
+ @ds.filter([:a, :b]=>[[10, 20]]).all.must_equal []
961
+ @ds.filter([:a, :b]=>[[20, 10], [1, 2]]).all.must_equal [{:a=>20, :b=>10}]
962
+ @ds.filter([:a, :b]=>[[10, 10], [20, 20]]).all.must_equal []
963
+
964
+ @ds.exclude([:a, :b]=>[[20, 10]]).all.must_equal []
965
+ @ds.exclude([:a, :b]=>[[10, 20]]).all.must_equal [{:a=>20, :b=>10}]
966
+ @ds.exclude([:a, :b]=>[[20, 10], [1, 2]]).all.must_equal []
967
+ @ds.exclude([:a, :b]=>[[10, 10], [20, 20]]).all.must_equal [{:a=>20, :b=>10}]
968
+ end
969
+
970
+ it "should work with IN/NOT in with datasets" do
971
+ @ds.insert(20, 10)
972
+ ds = @ds.unordered
973
+ @ds.quote_identifiers = false
974
+
975
+ @ds.filter(:a=>ds.select(:a)).all.must_equal [{:a=>20, :b=>10}]
976
+ @ds.filter(:a=>ds.select(:a).where(:a=>15)).all.must_equal []
977
+ @ds.exclude(:a=>ds.select(:a)).all.must_equal []
978
+ @ds.exclude(:a=>ds.select(:a).where(:a=>15)).all.must_equal [{:a=>20, :b=>10}]
979
+
980
+ @ds.filter([:a, :b]=>ds.select(:a, :b)).all.must_equal [{:a=>20, :b=>10}]
981
+ @ds.filter([:a, :b]=>ds.select(:b, :a)).all.must_equal []
982
+ @ds.exclude([:a, :b]=>ds.select(:a, :b)).all.must_equal []
983
+ @ds.exclude([:a, :b]=>ds.select(:b, :a)).all.must_equal [{:a=>20, :b=>10}]
984
+
985
+ @ds.filter([:a, :b]=>ds.select(:a, :b).where(:a=>15)).all.must_equal []
986
+ @ds.exclude([:a, :b]=>ds.select(:a, :b).where(:a=>15)).all.must_equal [{:a=>20, :b=>10}]
987
+ end
988
+
989
+ it "should work empty arrays" do
990
+ @ds.insert(20, 10)
991
+ @ds.filter(:a=>[]).all.must_equal []
992
+ @ds.exclude(:a=>[]).all.must_equal [{:a=>20, :b=>10}]
993
+ @ds.filter([:a, :b]=>[]).all.must_equal []
994
+ @ds.exclude([:a, :b]=>[]).all.must_equal [{:a=>20, :b=>10}]
995
+ end
996
+
997
+ it "should work empty arrays with nulls" do
998
+ @ds = @ds.extension(:empty_array_consider_nulls)
999
+ @ds.insert(nil, nil)
1000
+ @ds.filter(:a=>[]).all.must_equal []
1001
+ @ds.exclude(:a=>[]).all.must_equal []
1002
+ @ds.filter([:a, :b]=>[]).all.must_equal []
1003
+ @ds.exclude([:a, :b]=>[]).all.must_equal []
1004
+
1005
+ pr = proc{|r| r.is_a?(Integer) ? (r != 0) : r}
1006
+ pr[@ds.get(Sequel.expr(:a=>[]))].must_equal nil
1007
+ pr[@ds.get(~Sequel.expr(:a=>[]))].must_equal nil
1008
+ pr[@ds.get(Sequel.expr([:a, :b]=>[]))].must_equal nil
1009
+ pr[@ds.get(~Sequel.expr([:a, :b]=>[]))].must_equal nil
1010
+ end
1011
+
1012
+ it "should work empty arrays with nulls and the empty_array_ignore_nulls extension" do
1013
+ ds = @ds
1014
+ ds.insert(nil, nil)
1015
+ ds.filter(:a=>[]).all.must_equal []
1016
+ ds.exclude(:a=>[]).all.must_equal [{:a=>nil, :b=>nil}]
1017
+ ds.filter([:a, :b]=>[]).all.must_equal []
1018
+ ds.exclude([:a, :b]=>[]).all.must_equal [{:a=>nil, :b=>nil}]
1019
+
1020
+ pr = proc{|r| r.is_a?(Integer) ? (r != 0) : r}
1021
+ pr[ds.get(Sequel.expr(:a=>[]))].must_equal false
1022
+ pr[ds.get(~Sequel.expr(:a=>[]))].must_equal true
1023
+ pr[ds.get(Sequel.expr([:a, :b]=>[]))].must_equal false
1024
+ pr[ds.get(~Sequel.expr([:a, :b]=>[]))].must_equal true
1025
+ end
1026
+
1027
+ it "should work multiple conditions" do
1028
+ @ds.insert(20, 10)
1029
+ @ds.filter(:a=>20, :b=>10).all.must_equal [{:a=>20, :b=>10}]
1030
+ @ds.filter([[:a, 20], [:b, 10]]).all.must_equal [{:a=>20, :b=>10}]
1031
+ @ds.filter({:a=>20}, {:b=>10}).all.must_equal [{:a=>20, :b=>10}]
1032
+ @ds.filter(Sequel.|({:a=>20}, {:b=>5})).all.must_equal [{:a=>20, :b=>10}]
1033
+ @ds.filter(Sequel.~(:a=>10)).all.must_equal [{:a=>20, :b=>10}]
1034
+ end
1035
+ end
1036
+
1037
+ describe "SQL Extract Function" do
1038
+ before do
1039
+ @db = DB
1040
+ @db.create_table!(:a){DateTime :a}
1041
+ @ds = @db[:a].order(:a)
1042
+ end
1043
+ after do
1044
+ @db.drop_table?(:a)
1045
+ end
1046
+
1047
+ it "should return the part of the datetime asked for" do
1048
+ t = Time.now
1049
+ def @ds.supports_timestamp_timezones?() false end
1050
+ @ds.insert(t)
1051
+ @ds.get{a.extract(:year)}.must_equal t.year
1052
+ @ds.get{a.extract(:month)}.must_equal t.month
1053
+ @ds.get{a.extract(:day)}.must_equal t.day
1054
+ @ds.get{a.extract(:hour)}.must_equal t.hour
1055
+ @ds.get{a.extract(:minute)}.must_equal t.min
1056
+ @ds.get{a.extract(:second)}.to_i.must_equal t.sec
1057
+ end
1058
+ end
1059
+
1060
+ describe "Dataset string methods" do
1061
+ before do
1062
+ @db = DB
1063
+ @db.create_table!(:a) do
1064
+ String :a
1065
+ String :b
1066
+ end
1067
+ @ds = @db[:a].order(:a)
1068
+ end
1069
+ after do
1070
+ @db.drop_table?(:a)
1071
+ end
1072
+
1073
+ it "#grep should return matching rows" do
1074
+ @ds.insert('foo', 'bar')
1075
+ @ds.grep(:a, 'foo').all.must_equal [{:a=>'foo', :b=>'bar'}]
1076
+ @ds.grep(:b, 'foo').all.must_equal []
1077
+ @ds.grep(:b, 'bar').all.must_equal [{:a=>'foo', :b=>'bar'}]
1078
+ @ds.grep(:a, 'bar').all.must_equal []
1079
+ @ds.grep([:a, :b], %w'foo bar').all.must_equal [{:a=>'foo', :b=>'bar'}]
1080
+ @ds.grep([:a, :b], %w'boo far').all.must_equal []
1081
+ end
1082
+
1083
+ it "#grep should work with :all_patterns and :all_columns options" do
1084
+ @ds.insert('foo bar', ' ')
1085
+ @ds.insert('foo d', 'bar')
1086
+ @ds.insert('foo e', ' ')
1087
+ @ds.insert(' ', 'bar')
1088
+ @ds.insert('foo f', 'baz')
1089
+ @ds.insert('foo baz', 'bar baz')
1090
+ @ds.insert('foo boo', 'boo foo')
1091
+
1092
+ @ds.grep([:a, :b], %w'%foo% %bar%', :all_patterns=>true).all.must_equal [{:a=>'foo bar', :b=>' '}, {:a=>'foo baz', :b=>'bar baz'}, {:a=>'foo d', :b=>'bar'}]
1093
+ @ds.grep([:a, :b], %w'%foo% %bar% %blob%', :all_patterns=>true).all.must_equal []
1094
+
1095
+ @ds.grep([:a, :b], %w'%bar% %foo%', :all_columns=>true).all.must_equal [{:a=>"foo baz", :b=>"bar baz"}, {:a=>"foo boo", :b=>"boo foo"}, {:a=>"foo d", :b=>"bar"}]
1096
+ @ds.grep([:a, :b], %w'%baz%', :all_columns=>true).all.must_equal [{:a=>'foo baz', :b=>'bar baz'}]
1097
+
1098
+ @ds.grep([:a, :b], %w'%baz% %foo%', :all_columns=>true, :all_patterns=>true).all.must_equal []
1099
+ @ds.grep([:a, :b], %w'%boo% %foo%', :all_columns=>true, :all_patterns=>true).all.must_equal [{:a=>'foo boo', :b=>'boo foo'}]
1100
+ end
1101
+
1102
+ it "#like should return matching rows" do
1103
+ @ds.insert('foo', 'bar')
1104
+ @ds.filter(Sequel.expr(:a).like('foo')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1105
+ @ds.filter(Sequel.expr(:a).like('bar')).all.must_equal []
1106
+ @ds.filter(Sequel.expr(:a).like('foo', 'bar')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1107
+ @ds.exclude(Sequel.expr(:a).like('foo')).all.must_equal []
1108
+ @ds.exclude(Sequel.expr(:a).like('bar')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1109
+ @ds.exclude(Sequel.expr(:a).like('foo', 'bar')).all.must_equal []
1110
+ end
1111
+
1112
+ it "#like should be case sensitive" do
1113
+ @ds.insert('foo', 'bar')
1114
+ @ds.filter(Sequel.expr(:a).like('Foo')).all.must_equal []
1115
+ @ds.filter(Sequel.expr(:b).like('baR')).all.must_equal []
1116
+ @ds.filter(Sequel.expr(:a).like('FOO', 'BAR')).all.must_equal []
1117
+ @ds.exclude(Sequel.expr(:a).like('Foo')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1118
+ @ds.exclude(Sequel.expr(:b).like('baR')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1119
+ @ds.exclude(Sequel.expr(:a).like('FOO', 'BAR')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1120
+ end
1121
+
1122
+ it "#ilike should return matching rows, in a case insensitive manner" do
1123
+ @ds.insert('foo', 'bar')
1124
+ @ds.filter(Sequel.expr(:a).ilike('Foo')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1125
+ @ds.filter(Sequel.expr(:a).ilike('baR')).all.must_equal []
1126
+ @ds.filter(Sequel.expr(:a).ilike('FOO', 'BAR')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1127
+ @ds.exclude(Sequel.expr(:a).ilike('Foo')).all.must_equal []
1128
+ @ds.exclude(Sequel.expr(:a).ilike('baR')).all.must_equal [{:a=>'foo', :b=>'bar'}]
1129
+ @ds.exclude(Sequel.expr(:a).ilike('FOO', 'BAR')).all.must_equal []
1130
+ end
1131
+
1132
+ it "#like with regexp return matching rows" do
1133
+ @ds.insert('foo', 'bar')
1134
+ @ds.filter(Sequel.expr(:a).like(/fo/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1135
+ @ds.filter(Sequel.expr(:a).like(/fo$/)).all.must_equal []
1136
+ @ds.filter(Sequel.expr(:a).like(/fo/, /ar/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1137
+ @ds.exclude(Sequel.expr(:a).like(/fo/)).all.must_equal []
1138
+ @ds.exclude(Sequel.expr(:a).like(/fo$/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1139
+ @ds.exclude(Sequel.expr(:a).like(/fo/, /ar/)).all.must_equal []
1140
+ end
1141
+
1142
+ it "#like with regexp should be case sensitive if regexp is case sensitive" do
1143
+ @ds.insert('foo', 'bar')
1144
+ @ds.filter(Sequel.expr(:a).like(/Fo/)).all.must_equal []
1145
+ @ds.filter(Sequel.expr(:b).like(/baR/)).all.must_equal []
1146
+ @ds.filter(Sequel.expr(:a).like(/FOO/, /BAR/)).all.must_equal []
1147
+ @ds.exclude(Sequel.expr(:a).like(/Fo/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1148
+ @ds.exclude(Sequel.expr(:b).like(/baR/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1149
+ @ds.exclude(Sequel.expr(:a).like(/FOO/, /BAR/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1150
+
1151
+ @ds.filter(Sequel.expr(:a).like(/Fo/i)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1152
+ @ds.filter(Sequel.expr(:b).like(/baR/i)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1153
+ @ds.filter(Sequel.expr(:a).like(/FOO/i, /BAR/i)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1154
+ @ds.exclude(Sequel.expr(:a).like(/Fo/i)).all.must_equal []
1155
+ @ds.exclude(Sequel.expr(:b).like(/baR/i)).all.must_equal []
1156
+ @ds.exclude(Sequel.expr(:a).like(/FOO/i, /BAR/i)).all.must_equal []
1157
+ end
1158
+
1159
+ it "#ilike with regexp should return matching rows, in a case insensitive manner" do
1160
+ @ds.insert('foo', 'bar')
1161
+ @ds.filter(Sequel.expr(:a).ilike(/Fo/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1162
+ @ds.filter(Sequel.expr(:b).ilike(/baR/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1163
+ @ds.filter(Sequel.expr(:a).ilike(/FOO/, /BAR/)).all.must_equal [{:a=>'foo', :b=>'bar'}]
1164
+ @ds.exclude(Sequel.expr(:a).ilike(/Fo/)).all.must_equal []
1165
+ @ds.exclude(Sequel.expr(:b).ilike(/baR/)).all.must_equal []
1166
+ @ds.exclude(Sequel.expr(:a).ilike(/FOO/, /BAR/)).all.must_equal []
1167
+ end
1168
+
1169
+ it "should work with strings created with Sequel.join" do
1170
+ @ds.insert('foo', 'bar')
1171
+ @ds.get(Sequel.join([:a, "bar"])).must_equal 'foobar'
1172
+ @ds.get(Sequel.join(["foo", :b], ' ')).must_equal 'foo bar'
1173
+ end
1174
+ end
1175
+
1176
+ describe "Dataset identifier methods" do
1177
+ before(:all) do
1178
+ class ::String
1179
+ def uprev
1180
+ upcase.reverse
1181
+ end
1182
+ end
1183
+ @db = DB
1184
+ @db.create_table!(:a){Integer :ab}
1185
+ @db[:a].insert(1)
1186
+ end
1187
+ before do
1188
+ @ds = @db[:a].order(:ab)
1189
+ end
1190
+ after(:all) do
1191
+ @db.drop_table?(:a)
1192
+ end
1193
+
1194
+ it "#identifier_output_method should change how identifiers are output" do
1195
+ @ds.identifier_output_method = :upcase
1196
+ @ds.first.must_equal(:AB=>1)
1197
+ @ds.identifier_output_method = :uprev
1198
+ @ds.first.must_equal(:BA=>1)
1199
+ end
1200
+
1201
+ it "should work with a nil identifier_output_method" do
1202
+ @ds.identifier_output_method = nil
1203
+ [{:ab=>1}, {:AB=>1}].must_include(@ds.first)
1204
+ end
1205
+
1206
+ it "should work when not quoting identifiers" do
1207
+ @ds.quote_identifiers = false
1208
+ @ds.first.must_equal(:ab=>1)
1209
+ end
1210
+ end
1211
+
1212
+ describe "Dataset defaults and overrides" do
1213
+ before do
1214
+ @db = DB
1215
+ @db.create_table!(:a){Integer :a}
1216
+ @ds = @db[:a].order(:a).extension(:set_overrides)
1217
+ end
1218
+ after do
1219
+ @db.drop_table?(:a)
1220
+ end
1221
+
1222
+ it "#set_defaults should set defaults that can be overridden" do
1223
+ @ds = @ds.set_defaults(:a=>10)
1224
+ @ds.insert
1225
+ @ds.insert(:a=>20)
1226
+ @ds.all.must_equal [{:a=>10}, {:a=>20}]
1227
+ end
1228
+
1229
+ it "#set_overrides should set defaults that cannot be overridden" do
1230
+ @ds = @ds.set_overrides(:a=>10)
1231
+ @ds.insert
1232
+ @ds.insert(:a=>20)
1233
+ @ds.all.must_equal [{:a=>10}, {:a=>10}]
1234
+ end
1235
+ end
1236
+
1237
+ describe "Emulated functions" do
1238
+ before do
1239
+ @db = DB
1240
+ @db.create_table!(:a){String :a}
1241
+ @ds = @db[:a]
1242
+ end
1243
+ after do
1244
+ @db.drop_table?(:a)
1245
+ end
1246
+
1247
+ it "Sequel.char_length should return the length of characters in the string" do
1248
+ @ds.get(Sequel.char_length(:a)).must_equal nil
1249
+ @ds.insert(:a=>'foo')
1250
+ @ds.get(Sequel.char_length(:a)).must_equal 3
1251
+ end
1252
+
1253
+ it "Sequel.char_length should return the length of characters in the string including trailing blanks" do
1254
+ @ds.insert(:a=>' foo22 ')
1255
+ @ds.get(Sequel.char_length(:a)).must_equal 7
1256
+ end
1257
+
1258
+ it "Sequel.trim should return the string with spaces trimmed from both sides" do
1259
+ @ds.get(Sequel.trim(:a)).must_equal nil
1260
+ @ds.insert(:a=>'foo')
1261
+ @ds.get(Sequel.trim(:a)).must_equal 'foo'
1262
+ end
1263
+
1264
+ it "Sequel.trim should return the string with spaces trimmed from both sides" do
1265
+ @ds.insert(:a=>' foo22 ')
1266
+ @ds.get(Sequel.trim(:a)).must_equal 'foo22'
1267
+ end
1268
+ end