sequel 5.10.0 → 5.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 629d77185f762e5399741ae9b1caf03c8cd2bbe5f72fd9487900dc0c4bfc2e7a
4
- data.tar.gz: ee46b9c3180a67eb356b1f2ae7b274cc293687b333bf216b76a3e55def081c57
3
+ metadata.gz: 935bcf2280ce67a62dfd6e705308c85324f9d82303f5144ccbede8883d0956e5
4
+ data.tar.gz: 91dc87ca6afcaa82372233829919bab98227e2f7a7555bc0229811cfc829b482
5
5
  SHA512:
6
- metadata.gz: 2b5a5c9ed14dcd9bc6c98286bb4c22637052bb2f152f352d5002b096e2d361e2ac6ec15dcd44881e3e96e37b6acd8896161ad94cac4b674d55a6f54a9d43a948
7
- data.tar.gz: 847891aeebf175eca649d58461c38d3127311594bb3906bb73429726230b57dd1555ffbd760e3d5f7b30bb9907d5c6d461af1ac0d3b8e166b8dcbf61787fea97
6
+ metadata.gz: f0540143323390f812a543e8a28fa2ec3184faad7ad5508e9d64cf49bf6642f09dd636f0356d8bd2a4ad632a21e5a4358eda096d86b08ddaae0f62cdf8bfc9fa
7
+ data.tar.gz: f85a4c4880b34f737b00affaab544473e02d70052e70ddc438597455974b411a65093f2aa511af84558a15121f3032192df191677bc7ea4b1c0b779cda1714ae
data/CHANGELOG CHANGED
@@ -1,3 +1,23 @@
1
+ === 5.11.0 (2018-08-01)
2
+
3
+ * Fix using the jdbc/sqlserver adapter on JRuby 9.2+ (jeremyevans)
4
+
5
+ * Fix dumping schema for numeric/decimal columns with default values, broken starting in 5.9.0 (jeremyevans)
6
+
7
+ * Recognize additional check constraint violations on certain versions of SQLite (jeremyevans)
8
+
9
+ * Use cached model instances for Model.first calls without an argument or with a single integer argument in the static_cache plugin (AlexWayfer) (#1529)
10
+
11
+ * Support ON CONFLICT clause for INSERT on SQLite 3.24+ (jeremyevans)
12
+
13
+ * Support Dataset#window for WINDOW clause on MySQL 8 and SQLAnywhere (jeremyevans)
14
+
15
+ * Enable window function support on SQLAnywhere (jeremyevans)
16
+
17
+ * Support using a hash as a window function :frame option value, with support for ROWS/RANGE/GROUPS, numeric offsets, and EXCLUDE (jeremyevans)
18
+
19
+ * Allow using set_column_default with a nil value to remove the default value for a column on MySQL when the column is NOT NULL (jeremyevans)
20
+
1
21
  === 5.10.0 (2018-07-01)
2
22
 
3
23
  * Use input type casts when using the postgres adapter with pg 0.18+ to reduce string allocations for some primitive types used as prepared statement arguments (jeremyevans)
@@ -0,0 +1,83 @@
1
+ = New Features
2
+
3
+ * Sequel now supports more window frame specification types when
4
+ using window functions. You can now provide the window frame
5
+ specification as a hash, and Sequel will format the correct
6
+ SQL. Specifically, this adds support for RANGE and GROUPS,
7
+ numeric offsets, and EXCLUDE on a database that supports it
8
+ (e.g. PostgreSQL 11+). Examples:
9
+
10
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3,
11
+ :frame=>{:type=>:range, :start=>1, :end=>1})}
12
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3
13
+ # RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM albums
14
+
15
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3,
16
+ :frame=>{:type=>:groups, :start=>[2, :preceding], :end=>[1, :preceding]})}
17
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3
18
+ # GROUPS BETWEEN 2 PRECEDING AND 1 PRECEDING) FROM albums
19
+
20
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3,
21
+ :frame=>{:type=>:range, :start=>:preceding, :exclude=>:current})}
22
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3
23
+ # RANGE UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM albums
24
+
25
+ * The SQLite 3.24+ ON CONFLICT clause to INSERT is now supported.
26
+ This support is very similar to the PostgreSQL support for the
27
+ same feature, also known as UPSERT (UPDATE if the row already
28
+ exists, INSERT if it does not). This support is different than
29
+ the previous support for INSERT ON CONFLICT REPLACE (also known as
30
+ INSERT OR REPLACE), but it uses the same method name in order to
31
+ be compatible with the PostgreSQL support. The new syntax requires
32
+ passing a hash to Dataset#insert_conflict. Examples:
33
+
34
+ DB[:table].insert_conflict({}).insert(a: 1, b: 2)
35
+ # INSERT INTO TABLE (a, b) VALUES (1, 2)
36
+ # ON CONFLICT DO NOTHING
37
+
38
+ DB[:table].insert_conflict(target: :a).insert(a: 1, b: 2)
39
+ # INSERT INTO TABLE (a, b) VALUES (1, 2)
40
+ # ON CONFLICT (a) DO NOTHING
41
+
42
+ DB[:table].insert_conflict(target: :a,
43
+ conflict_where: {c: true}).insert(a: 1, b: 2)
44
+ # INSERT INTO TABLE (a, b) VALUES (1, 2)
45
+ # ON CONFLICT (a) WHERE (c IS TRUE) DO NOTHING
46
+
47
+ DB[:table].insert_conflict(target: :a,
48
+ update: {b: Sequel[:excluded][:b]}).insert(a: 1, b: 2)
49
+ # INSERT INTO TABLE (a, b) VALUES (1, 2)
50
+ # ON CONFLICT (a) DO UPDATE SET b = excluded.b
51
+
52
+ DB[:table].insert_conflict(target: :a,
53
+ update: {b: Sequel[:excluded][:b]},
54
+ update_where: {Sequel[:table][:status_id] => 1}).insert(a: 1, b: 2)
55
+ # INSERT INTO TABLE (a, b) VALUES (1, 2) ON CONFLICT (a)
56
+ # DO UPDATE SET b = excluded.b WHERE (table.status_id = 1)
57
+
58
+ * Dataset#window for the WINDOW clause has been moved from the
59
+ PostgreSQL-specific support to core, and has been enabled on
60
+ MySQL 8+ and SQLAnywhere. This allows you to specify a shared
61
+ window specification in a query, which can be used by multiple
62
+ window functions.
63
+
64
+ = Other Improvements
65
+
66
+ * When using the static_cache plugin, Model.first when called without
67
+ a block and without arguments or with a single Integer argument now
68
+ uses the cached values instead of issuing a query.
69
+
70
+ * Using set_column_default with a nil value now correctly removes an
71
+ existing default value on MySQL when the column is NOT NULL.
72
+
73
+ * Window function support has been enabled on SQLAnywhere, since it
74
+ works correctly.
75
+
76
+ * Dumping schema for numeric/decimal columns with default values
77
+ now works correctly. This was broken starting in Sequel 5.9.0
78
+ due to changes to use BigDecimal() instead of BigDecimal.new().
79
+
80
+ * The jdbc/sqlserver adapter now works correctly on JRuby 9.2+.
81
+
82
+ * An additional check constraint violation failure message is now
83
+ recognized on SQLite.
@@ -173,6 +173,16 @@ user input for function names.
173
173
 
174
174
  DB[:table].select(Sequel.function(params[:id])) # SQL injection!
175
175
 
176
+ ==== SQL Window Frames
177
+
178
+ For backwards compatibility, Sequel supports regular strings in the
179
+ window function :frame option, which will be treated as a literal string:
180
+
181
+ DB[:table].select{fun(arg).over(:frame=>'SQL Here')}
182
+
183
+ You should make sure the frame argument is not derived from user input,
184
+ or switch to using a hash as the :frame option value.
185
+
176
186
  ==== auto_literal_strings extension
177
187
 
178
188
  If the auto_literal_strings extension is used for backwards compatibility,
@@ -229,6 +229,18 @@ If the database supports window functions, Sequel can handle them by calling the
229
229
  DB[:albums].select{function(c1, c2).over(:partition=>[c3, c4], :order=>[c5, c6.desc])}
230
230
  # SELECT function(c1, c2) OVER (PARTITION BY c3, c4 ORDER BY c5, c6 DESC) FROM albums
231
231
 
232
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3, :frame=>:rows)}
233
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM albums
234
+
235
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3, :frame=>{:type=>:range, :start=>1, :end=>1})}
236
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3 RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM albums
237
+
238
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3, :frame=>{:type=>:groups, :start=>[2, :preceding], :end=>[1, :preceding]})}
239
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3 GROUPS BETWEEN 2 PRECEDING AND 1 PRECEDING) FROM albums
240
+
241
+ DB[:albums].select{function(c1).over(:partition=>c2, :order=>:c3, :frame=>{:type=>:range, :start=>:preceding, :exclude=>:current})}
242
+ # SELECT function(c1) OVER (PARTITION BY c2 ORDER BY c3 RANGE UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM albums
243
+
232
244
  === Schema Qualified Functions
233
245
 
234
246
  If the database supports schema qualified functions, Sequel can handle them by calling the +function+ method on a qualified identifier:
@@ -75,7 +75,7 @@ module Sequel
75
75
  private
76
76
 
77
77
  def _metadata_dataset
78
- super.extend(MetadataDatasetMethods)
78
+ super.with_extend(MetadataDatasetMethods)
79
79
  end
80
80
 
81
81
  def disconnect_error?(exception, opts)
@@ -232,7 +232,18 @@ module Sequel
232
232
  alias alter_table_rename_column_sql alter_table_change_column_sql
233
233
  alias alter_table_set_column_type_sql alter_table_change_column_sql
234
234
  alias alter_table_set_column_null_sql alter_table_change_column_sql
235
- alias alter_table_set_column_default_sql alter_table_change_column_sql
235
+
236
+ def alter_table_set_column_default_sql(table, op)
237
+ return super unless op[:default].nil?
238
+
239
+ opts = schema(table).find{|x| x[0] == op[:name]}
240
+
241
+ if opts && opts[1][:allow_null] == false
242
+ "ALTER COLUMN #{quote_identifier(op[:name])} DROP DEFAULT"
243
+ else
244
+ super
245
+ end
246
+ end
236
247
 
237
248
  def alter_table_add_constraint_sql(table, op)
238
249
  if op[:type] == :foreign_key
@@ -591,7 +602,7 @@ module Sequel
591
602
 
592
603
  Dataset.def_sql_method(self, :delete, %w'with delete from where order limit')
593
604
  Dataset.def_sql_method(self, :insert, %w'insert ignore into columns values on_duplicate_key_update')
594
- Dataset.def_sql_method(self, :select, %w'with select distinct calc_found_rows columns from join where group having compounds order limit lock')
605
+ Dataset.def_sql_method(self, :select, %w'with select distinct calc_found_rows columns from join where group having window compounds order limit lock')
595
606
  Dataset.def_sql_method(self, :update, %w'with update ignore table set where order limit')
596
607
 
597
608
  include Sequel::Dataset::Replace
@@ -835,6 +846,11 @@ module Sequel
835
846
  db.supports_timestamp_usecs?
836
847
  end
837
848
 
849
+ # MySQL 8+ supports WINDOW clause.
850
+ def supports_window_clause?
851
+ !db.mariadb? && db.server_version >= 80000
852
+ end
853
+
838
854
  # MariaDB 10.2+ and MySQL 8+ support window functions
839
855
  def supports_window_functions?
840
856
  db.server_version >= (db.mariadb? ? 100200 : 80000)
@@ -1673,11 +1673,29 @@ module Sequel
1673
1673
  true
1674
1674
  end
1675
1675
 
1676
+ # PostgreSQL 8.4+ supports WINDOW clause.
1677
+ def supports_window_clause?
1678
+ server_version >= 80400
1679
+ end
1680
+
1676
1681
  # PostgreSQL 8.4+ supports window functions
1677
1682
  def supports_window_functions?
1678
1683
  server_version >= 80400
1679
1684
  end
1680
1685
 
1686
+ # Base support added in 8.4, offset supported added in 9.0,
1687
+ # GROUPS and EXCLUDE support added in 11.0.
1688
+ def supports_window_function_frame_option?(option)
1689
+ case option
1690
+ when :rows, :range
1691
+ true
1692
+ when :offset
1693
+ server_version >= 90000
1694
+ when :groups, :exclude
1695
+ server_version >= 110000
1696
+ end
1697
+ end
1698
+
1681
1699
  # Truncates the dataset. Returns nil.
1682
1700
  #
1683
1701
  # Options:
@@ -1702,13 +1720,6 @@ module Sequel
1702
1720
  end
1703
1721
  end
1704
1722
 
1705
- # Return a clone of the dataset with an addition named window that can be
1706
- # referenced in window functions. See Sequel::SQL::Window for a list of
1707
- # options that can be passed in.
1708
- def window(name, opts)
1709
- clone(:window=>(@opts[:window]||[]) + [[name, SQL::Window.new(opts)]])
1710
- end
1711
-
1712
1723
  protected
1713
1724
 
1714
1725
  # If returned primary keys are requested, use RETURNING unless already set on the
@@ -1900,23 +1911,6 @@ module Sequel
1900
1911
  expression_list_append(sql, opts[:values])
1901
1912
  end
1902
1913
 
1903
- # SQL fragment for named window specifications
1904
- def select_window_sql(sql)
1905
- if ws = @opts[:window]
1906
- sql << " WINDOW "
1907
- c = false
1908
- co = ', '
1909
- as = ' AS '
1910
- ws.map do |name, window|
1911
- sql << co if c
1912
- literal_append(sql, name)
1913
- sql << as
1914
- literal_append(sql, window)
1915
- c ||= true
1916
- end
1917
- end
1918
- end
1919
-
1920
1914
  # Use WITH RECURSIVE instead of WITH if any of the CTEs is recursive
1921
1915
  def select_with_sql_base
1922
1916
  opts[:with].any?{|w| w[:recursive]} ? "WITH RECURSIVE " : super
@@ -233,7 +233,7 @@ module Sequel
233
233
 
234
234
  module DatasetMethods
235
235
  Dataset.def_sql_method(self, :insert, %w'insert into columns values')
236
- Dataset.def_sql_method(self, :select, %w'with select distinct limit columns into from join where group having compounds order lock')
236
+ Dataset.def_sql_method(self, :select, %w'with select distinct limit columns into from join where group having window compounds order lock')
237
237
 
238
238
  # Whether to convert smallint to boolean arguments for this dataset.
239
239
  # Defaults to the IBMDB module setting.
@@ -275,6 +275,14 @@ module Sequel
275
275
  false
276
276
  end
277
277
 
278
+ def supports_window_clause?
279
+ true
280
+ end
281
+
282
+ def supports_window_functions?
283
+ true
284
+ end
285
+
278
286
  # Uses CROSS APPLY to join the given table into the current dataset.
279
287
  def cross_apply(table)
280
288
  join_table(:cross_apply, table)
@@ -299,7 +299,7 @@ module Sequel
299
299
  DATABASE_ERROR_REGEXPS = {
300
300
  /(is|are) not unique\z|PRIMARY KEY must be unique\z|UNIQUE constraint failed: .+\z/ => UniqueConstraintViolation,
301
301
  /foreign key constraint failed\z/i => ForeignKeyConstraintViolation,
302
- /\ACHECK constraint failed/ => CheckConstraintViolation,
302
+ /\A(SQLITE ERROR 275 \(CONSTRAINT_CHECK\) : )?CHECK constraint failed/ => CheckConstraintViolation,
303
303
  /\A(SQLITE ERROR 19 \(CONSTRAINT\) : )?constraint failed\z/ => ConstraintViolation,
304
304
  /may not be NULL\z|NOT NULL constraint failed: .+\z/ => NotNullConstraintViolation,
305
305
  /\ASQLITE ERROR \d+ \(\) : CHECK constraint failed: / => CheckConstraintViolation
@@ -500,7 +500,7 @@ module Sequel
500
500
  EXTRACT_MAP.each_value(&:freeze)
501
501
 
502
502
  Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
503
- Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 30803', %w'with insert conflict into columns values'], ["else", %w'insert conflict into columns values']])
503
+ Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 30803', %w'with insert conflict into columns values on_conflict'], ["else", %w'insert conflict into columns values']])
504
504
  Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'with values compounds'], ['else', %w'with select distinct columns from join where group having compounds order limit lock']])
505
505
  Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
506
506
 
@@ -620,6 +620,14 @@ module Sequel
620
620
  # supports the following conflict resolution algoriths: ROLLBACK, ABORT,
621
621
  # FAIL, IGNORE and REPLACE.
622
622
  #
623
+ # On SQLite 3.24.0+, you can pass a hash to use an ON CONFLICT clause.
624
+ # With out :update option, uses ON CONFLICT DO NOTHING. Options:
625
+ #
626
+ # :conflict_where :: The index filter, when using a partial index to determine uniqueness.
627
+ # :target :: The column name or expression to handle uniqueness violations on.
628
+ # :update :: A hash of columns and values to set. Uses ON CONFLICT DO UPDATE.
629
+ # :update_where :: A WHERE condition to use for the update.
630
+ #
623
631
  # Examples:
624
632
  #
625
633
  # DB[:table].insert_conflict.insert(a: 1, b: 2)
@@ -627,11 +635,39 @@ module Sequel
627
635
  #
628
636
  # DB[:table].insert_conflict(:replace).insert(a: 1, b: 2)
629
637
  # # INSERT OR REPLACE INTO TABLE (a, b) VALUES (1, 2)
630
- def insert_conflict(resolution = :ignore)
631
- unless INSERT_CONFLICT_RESOLUTIONS.include?(resolution.to_s.upcase)
632
- raise Error, "Invalid value passed to Dataset#insert_conflict: #{resolution.inspect}. The allowed values are: :rollback, :abort, :fail, :ignore, or :replace"
638
+ #
639
+ # DB[:table].insert_conflict({}).insert(a: 1, b: 2)
640
+ # # INSERT INTO TABLE (a, b) VALUES (1, 2)
641
+ # # ON CONFLICT DO NOTHING
642
+ #
643
+ # DB[:table].insert_conflict(target: :a).insert(a: 1, b: 2)
644
+ # # INSERT INTO TABLE (a, b) VALUES (1, 2)
645
+ # # ON CONFLICT (a) DO NOTHING
646
+ #
647
+ # DB[:table].insert_conflict(target: :a, conflict_where: {c: true}).insert(a: 1, b: 2)
648
+ # # INSERT INTO TABLE (a, b) VALUES (1, 2)
649
+ # # ON CONFLICT (a) WHERE (c IS TRUE) DO NOTHING
650
+ #
651
+ # DB[:table].insert_conflict(target: :a, update: {b: Sequel[:excluded][:b]}).insert(a: 1, b: 2)
652
+ # # INSERT INTO TABLE (a, b) VALUES (1, 2)
653
+ # # ON CONFLICT (a) DO UPDATE SET b = excluded.b
654
+ #
655
+ # DB[:table].insert_conflict(target: :a,
656
+ # update: {b: Sequel[:excluded][:b]}, update_where: {Sequel[:table][:status_id] => 1}).insert(a: 1, b: 2)
657
+ # # INSERT INTO TABLE (a, b) VALUES (1, 2)
658
+ # # ON CONFLICT (a) DO UPDATE SET b = excluded.b WHERE (table.status_id = 1)
659
+ def insert_conflict(opts = :ignore)
660
+ case opts
661
+ when Symbol, String
662
+ unless INSERT_CONFLICT_RESOLUTIONS.include?(opts.to_s.upcase)
663
+ raise Error, "Invalid symbol or string passed to Dataset#insert_conflict: #{opts.inspect}. The allowed values are: :rollback, :abort, :fail, :ignore, or :replace"
664
+ end
665
+ clone(:insert_conflict => opts)
666
+ when Hash
667
+ clone(:insert_on_conflict => opts)
668
+ else
669
+ raise Error, "Invalid value passed to Dataset#insert_conflict: #{opts.inspect}, should use a symbol or a hash"
633
670
  end
634
- clone(:insert_conflict => resolution)
635
671
  end
636
672
 
637
673
  # Ignore uniqueness/exclusion violations when inserting, using INSERT OR IGNORE.
@@ -729,6 +765,36 @@ module Sequel
729
765
  end
730
766
  end
731
767
 
768
+ # Add ON CONFLICT clause if it should be used
769
+ def insert_on_conflict_sql(sql)
770
+ if opts = @opts[:insert_on_conflict]
771
+ sql << " ON CONFLICT"
772
+
773
+ if target = opts[:constraint]
774
+ sql << " ON CONSTRAINT "
775
+ identifier_append(sql, target)
776
+ elsif target = opts[:target]
777
+ sql << ' '
778
+ identifier_append(sql, Array(target))
779
+ if conflict_where = opts[:conflict_where]
780
+ sql << " WHERE "
781
+ literal_append(sql, conflict_where)
782
+ end
783
+ end
784
+
785
+ if values = opts[:update]
786
+ sql << " DO UPDATE SET "
787
+ update_sql_values_hash(sql, values)
788
+ if update_where = opts[:update_where]
789
+ sql << " WHERE "
790
+ literal_append(sql, update_where)
791
+ end
792
+ else
793
+ sql << " DO NOTHING"
794
+ end
795
+ end
796
+ end
797
+
732
798
  # SQLite uses a preceding X for hex escaping strings
733
799
  def literal_blob_append(sql, v)
734
800
  sql << "X'" << v.unpack("H*").first << "'"
@@ -178,11 +178,29 @@ module Sequel
178
178
  true
179
179
  end
180
180
 
181
+ # Whether the dataset supports the WINDOW clause to define windows used by multiple
182
+ # window functions, false by default.
183
+ def supports_window_clause?
184
+ false
185
+ end
186
+
181
187
  # Whether the dataset supports window functions, false by default.
182
188
  def supports_window_functions?
183
189
  false
184
190
  end
185
191
 
192
+ # Whether the dataset supports the given window function option. True by default.
193
+ # This should only be called if supports_window_functions? is true. Possible options
194
+ # are :rows, :range, :groups, :offset, :exclude.
195
+ def supports_window_function_frame_option?(option)
196
+ case option
197
+ when :rows, :range, :offset
198
+ true
199
+ else
200
+ false
201
+ end
202
+ end
203
+
186
204
  # Whether the dataset supports WHERE TRUE (or WHERE 1 for databases that
187
205
  # that use 1 for true), true by default.
188
206
  def supports_where_true?
@@ -82,6 +82,8 @@ module Sequel
82
82
  # columns are deleted. This method should generally not be called
83
83
  # directly by user code.
84
84
  def clone(opts = (return self; nil))
85
+ # return self used above because clone is called by almost all
86
+ # other query methods, and it is the fastest approach
85
87
  c = super(:freeze=>false)
86
88
  c.opts.merge!(opts)
87
89
  unless opts.each_key{|o| break if COLUMN_CHANGE_OPTS.include?(o)}
@@ -1044,6 +1046,16 @@ module Sequel
1044
1046
  add_filter(:where, cond, &block)
1045
1047
  end
1046
1048
 
1049
+ # Return a clone of the dataset with an addition named window that can be
1050
+ # referenced in window functions. See Sequel::SQL::Window for a list of
1051
+ # options that can be passed in. Example:
1052
+ #
1053
+ # DB[:items].window(:w, :partition=>:c1, :order=>:c2)
1054
+ # # SELECT * FROM items WINDOW w AS (PARTITION BY c1 ORDER BY c2)
1055
+ def window(name, opts)
1056
+ clone(:window=>((@opts[:window]||EMPTY_ARRAY) + [[name, SQL::Window.new(opts)].freeze]).freeze)
1057
+ end
1058
+
1047
1059
  # Add a common table expression (CTE) with the given name and a dataset that defines the CTE.
1048
1060
  # A common table expression acts as an inline view for the query.
1049
1061
  # Options:
@@ -718,41 +718,86 @@ module Sequel
718
718
  # Append literalization of windows (for window functions) to SQL string.
719
719
  def window_sql_append(sql, opts)
720
720
  raise(Error, 'This dataset does not support window functions') unless supports_window_functions?
721
- sql << '('
722
- window, part, order, frame = opts.values_at(:window, :partition, :order, :frame)
723
721
  space = false
724
722
  space_s = ' '
725
- if window
723
+
724
+ sql << '('
725
+
726
+ if window = opts[:window]
726
727
  literal_append(sql, window)
727
728
  space = true
728
729
  end
729
- if part
730
+
731
+ if part = opts[:partition]
730
732
  sql << space_s if space
731
733
  sql << "PARTITION BY "
732
734
  expression_list_append(sql, Array(part))
733
735
  space = true
734
736
  end
735
- if order
737
+
738
+ if order = opts[:order]
736
739
  sql << space_s if space
737
740
  sql << "ORDER BY "
738
741
  expression_list_append(sql, Array(order))
739
742
  space = true
740
743
  end
741
- case frame
742
- when nil
743
- # nothing
744
- when :all
745
- sql << space_s if space
746
- sql << "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"
747
- when :rows
748
- sql << space_s if space
749
- sql << "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"
750
- when String
751
- sql << space_s if space
744
+
745
+ if frame = opts[:frame]
746
+ sql << space_s if space
747
+
748
+ if frame.is_a?(String)
752
749
  sql << frame
753
750
  else
754
- raise Error, "invalid window frame clause, should be :all, :rows, a string, or nil"
751
+ case frame
752
+ when :all
753
+ frame_type = :rows
754
+ frame_start = :preceding
755
+ frame_end = :following
756
+ when :rows, :range, :groups
757
+ frame_type = frame
758
+ frame_start = :preceding
759
+ frame_end = :current
760
+ when Hash
761
+ frame_type = frame[:type]
762
+ unless frame_type == :rows || frame_type == :range || frame_type == :groups
763
+ raise Error, "invalid window :frame :type option: #{frame_type.inspect}"
764
+ end
765
+ unless frame_start = frame[:start]
766
+ raise Error, "invalid window :frame :start option: #{frame_start.inspect}"
767
+ end
768
+ frame_end = frame[:end]
769
+ frame_exclude = frame[:exclude]
770
+ else
771
+ raise Error, "invalid window :frame option: #{frame.inspect}"
772
+ end
773
+
774
+ sql << frame_type.to_s.upcase << " "
775
+ sql << 'BETWEEN ' if frame_end
776
+ window_frame_boundary_sql_append(sql, frame_start, :preceding)
777
+ if frame_end
778
+ sql << " AND "
779
+ window_frame_boundary_sql_append(sql, frame_end, :following)
780
+ end
781
+
782
+ if frame_exclude
783
+ sql << " EXCLUDE "
784
+
785
+ case frame_exclude
786
+ when :current
787
+ sql << "CURRENT ROW"
788
+ when :group
789
+ sql << "GROUP"
790
+ when :ties
791
+ sql << "TIES"
792
+ when :no_others
793
+ sql << "NO OTHERS"
794
+ else
795
+ raise Error, "invalid window :frame :exclude option: #{frame_exclude.inspect}"
796
+ end
797
+ end
798
+ end
755
799
  end
800
+
756
801
  sql << ')'
757
802
  end
758
803
 
@@ -1412,6 +1457,22 @@ module Sequel
1412
1457
  alias delete_where_sql select_where_sql
1413
1458
  alias update_where_sql select_where_sql
1414
1459
 
1460
+ def select_window_sql(sql)
1461
+ if ws = @opts[:window]
1462
+ sql << " WINDOW "
1463
+ c = false
1464
+ co = ', '
1465
+ as = ' AS '
1466
+ ws.map do |name, window|
1467
+ sql << co if c
1468
+ literal_append(sql, name)
1469
+ sql << as
1470
+ literal_append(sql, window)
1471
+ c ||= true
1472
+ end
1473
+ end
1474
+ end
1475
+
1415
1476
  def select_with_sql(sql)
1416
1477
  return unless supports_cte?
1417
1478
  ws = opts[:with]
@@ -1535,5 +1596,35 @@ module Sequel
1535
1596
  def update_update_sql(sql)
1536
1597
  sql << 'UPDATE'
1537
1598
  end
1599
+
1600
+ def window_frame_boundary_sql_append(sql, boundary, direction)
1601
+ case boundary
1602
+ when :current
1603
+ sql << "CURRENT ROW"
1604
+ when :preceding
1605
+ sql << "UNBOUNDED PRECEDING"
1606
+ when :following
1607
+ sql << "UNBOUNDED FOLLOWING"
1608
+ else
1609
+ if boundary.is_a?(Array)
1610
+ offset, direction = boundary
1611
+ unless boundary.length == 2 && (direction == :preceding || direction == :following)
1612
+ raise Error, "invalid window :frame boundary (:start or :end) option: #{boundary.inspect}"
1613
+ end
1614
+ else
1615
+ offset = boundary
1616
+ end
1617
+
1618
+ case offset
1619
+ when Numeric, String, SQL::Cast
1620
+ # nothing
1621
+ else
1622
+ raise Error, "invalid window :frame boundary (:start or :end) option: #{boundary.inspect}"
1623
+ end
1624
+
1625
+ literal_append(sql, offset)
1626
+ sql << (direction == :preceding ? " PRECEDING" : " FOLLOWING")
1627
+ end
1628
+ end
1538
1629
  end
1539
1630
  end
@@ -27,7 +27,7 @@ module Sequel
27
27
  def eval_inspect(obj)
28
28
  case obj
29
29
  when BigDecimal
30
- "BigDecimal(#{obj.to_s.inspect})"
30
+ "Kernel::BigDecimal(#{obj.to_s.inspect})"
31
31
  when Sequel::SQL::Blob, Sequel::LiteralString
32
32
  "#{obj.class}.new(#{obj.to_s.inspect})"
33
33
  when Sequel::SQL::ValueList
@@ -124,8 +124,10 @@ module Sequel
124
124
  move_to(position_value + n)
125
125
  end
126
126
 
127
- # Move this instance to the given place in the list. Raises an
128
- # exception if target is less than 1 or greater than the last position in the list.
127
+ # Move this instance to the given place in the list. If lp is not
128
+ # given or greater than the last list position, uses the last list
129
+ # position. If lp is less than the top list position, uses the
130
+ # top list position.
129
131
  def move_to(target, lp = nil)
130
132
  current = position_value
131
133
  if target != current
@@ -24,6 +24,7 @@ module Sequel
24
24
  # * Primary key lookups (e.g. Model[1])
25
25
  # * Model.all
26
26
  # * Model.each
27
+ # * Model.first (without block, only supporting no arguments or single integer argument)
27
28
  # * Model.count (without an argument or block)
28
29
  # * Model.map
29
30
  # * Model.as_hash
@@ -80,6 +81,20 @@ module Sequel
80
81
  end
81
82
  end
82
83
 
84
+ # If a block is given, multiple arguments are given, or a single
85
+ # non-Integer argument is given, performs the default behavior of
86
+ # issuing a database query. Otherwise, uses the cached values
87
+ # to return either the first cached instance (no arguments) or an
88
+ # array containing the number of instances specified (single integer
89
+ # argument).
90
+ def first(*args)
91
+ if block_given? || args.length > 1 || (args.length == 1 && !args[0].is_a?(Integer))
92
+ super
93
+ else
94
+ @all.first(*args)
95
+ end
96
+ end
97
+
83
98
  # Get the number of records in the cache, without issuing a database query.
84
99
  def count(*a, &block)
85
100
  if a.empty? && !block
@@ -1903,16 +1903,40 @@ module Sequel
1903
1903
  # # (PARTITION BY col7 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
1904
1904
  # Sequel::SQL::Window.new(partition: :col7, frame: :rows)
1905
1905
  # # (PARTITION BY col7 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
1906
- # Sequel::SQL::Window.new(partition: :col7, frame: "RANGE CURRENT ROW")
1906
+ # Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: current})
1907
1907
  # # (PARTITION BY col7 RANGE CURRENT ROW)
1908
+ # Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: 1, end: 1})
1909
+ # # (PARTITION BY col7 RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)
1910
+ # Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: 2, end: [1, :preceding]})
1911
+ # # (PARTITION BY col7 RANGE BETWEEN 2 PRECEDING AND 1 PRECEDING)
1912
+ # Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: 1, end: [2, :following]})
1913
+ # # (PARTITION BY col7 RANGE BETWEEN 1 FOLLOWING AND 2 FOLLOWING)
1914
+ # Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: :preceding, exclude: :current})
1915
+ # # (PARTITION BY col7 RANGE UNBOUNDED PRECEDING EXCLUDE CURRENT ROW)
1908
1916
  #
1909
1917
  # Sequel::SQL::Window.new(window: :named_window) # you can create a named window with Dataset#window
1910
1918
  # # (named_window)
1911
1919
  class Window < Expression
1912
1920
  # The options for this window. Options currently supported:
1913
- # :frame :: if specified, should be :all, :rows, or a String that is used literally. :all always operates over all rows in the
1914
- # partition, while :rows excludes the current row's later peers. The default is to include
1915
- # all previous rows in the partition up to the current row's last peer.
1921
+ # :frame :: if specified, should be :all, :rows, :range, :groups, a String, or a Hash.
1922
+ # :all :: Always operates over all rows in the partition
1923
+ # :rows :: Includes rows in the partition up to and including the current row
1924
+ # :range, :groups :: Includes rows in the partition up to and including the current group
1925
+ # String :: Used as literal SQL code, try to avoid
1926
+ # Hash :: Hash of options for the frame:
1927
+ # :type :: The type of frame, must be :rows, :range, or :groups (required)
1928
+ # :start :: The start of the frame (required). Possible values:
1929
+ # :preceding :: UNBOUNDED PRECEDING
1930
+ # :following :: UNBOUNDED FOLLOWING
1931
+ # :current :: CURRENT ROW
1932
+ # String, Numeric, or Cast :: Used as the offset of rows/values preceding
1933
+ # Array :: Must have two elements, with first element being String, Numeric, or
1934
+ # Cast and second element being :preceding or :following
1935
+ # :end :: The end of the frame. Can be left out. If present, takes the same values as
1936
+ # :start, except that when a String, Numeric, or Hash, it is used as the offset
1937
+ # for rows following
1938
+ # :exclude :: Which rows to exclude. Possible values are :current, :ties, :group
1939
+ # :no_others.
1916
1940
  # :order :: order on the column(s) given
1917
1941
  # :partition :: partition/group on the column(s) given
1918
1942
  # :window :: base results on a previously specified named window
@@ -3,9 +3,11 @@
3
3
  module Sequel
4
4
  # The major version of Sequel. Only bumped for major changes.
5
5
  MAJOR = 5
6
+
6
7
  # The minor version of Sequel. Bumped for every non-patch level
7
8
  # release, generally around once a month.
8
- MINOR = 10
9
+ MINOR = 11
10
+
9
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
10
12
  # releases that fix regressions from previous versions.
11
13
  TINY = 0
@@ -462,6 +462,12 @@ describe "A PostgreSQL database " do
462
462
  DB.drop_table?(:b, :a)
463
463
  end
464
464
 
465
+ it "should handle non-ASCII column aliases" do
466
+ s = String.new("\u00E4").force_encoding(DB.get('a').encoding)
467
+ k, v = DB.select(Sequel.as(s, s)).first.shift
468
+ k.to_s.must_equal v
469
+ end
470
+
465
471
  it "should parse foreign keys referencing current table using :reverse option" do
466
472
  DB.create_table!(:a) do
467
473
  primary_key :id
@@ -1665,36 +1671,6 @@ if DB.server_version >= 80300
1665
1671
  end
1666
1672
  end
1667
1673
 
1668
- if DB.dataset.supports_window_functions?
1669
- describe "Postgres::Dataset named windows" do
1670
- before do
1671
- @db = DB
1672
- @db.create_table!(:i1){Integer :id; Integer :group_id; Integer :amount}
1673
- @ds = @db[:i1].order(:id)
1674
- @ds.insert(:id=>1, :group_id=>1, :amount=>1)
1675
- @ds.insert(:id=>2, :group_id=>1, :amount=>10)
1676
- @ds.insert(:id=>3, :group_id=>1, :amount=>100)
1677
- @ds.insert(:id=>4, :group_id=>2, :amount=>1000)
1678
- @ds.insert(:id=>5, :group_id=>2, :amount=>10000)
1679
- @ds.insert(:id=>6, :group_id=>2, :amount=>100000)
1680
- end
1681
- after do
1682
- @db.drop_table?(:i1)
1683
- end
1684
-
1685
- it "should give correct results for window functions" do
1686
- @ds.window(:win, :partition=>:group_id, :order=>:id).select(:id){sum(:amount).over(:window=>win)}.all.
1687
- 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}]
1688
- @ds.window(:win, :partition=>:group_id).select(:id){sum(:amount).over(:window=>win, :order=>id)}.all.
1689
- 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}]
1690
- @ds.window(:win, {}).select(:id){sum(:amount).over(:window=>:win, :order=>id)}.all.
1691
- 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}]
1692
- @ds.window(:win, :partition=>:group_id).select(:id){sum(:amount).over(:window=>:win, :order=>id, :frame=>:all)}.all.
1693
- 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}]
1694
- end
1695
- end
1696
- end
1697
-
1698
1674
  describe "Postgres::Database functions, languages, schemas, and triggers" do
1699
1675
  before do
1700
1676
  @d = DB
@@ -3575,16 +3551,18 @@ describe 'PostgreSQL row-valued/composite types' do
3575
3551
  Sequel.extension :pg_array_ops, :pg_row_ops
3576
3552
  @ds = @db[:person]
3577
3553
 
3578
- @db.create_table!(:address) do
3554
+ @db.drop_table?(:company, :person, :address)
3555
+
3556
+ @db.create_table(:address) do
3579
3557
  String :street
3580
3558
  String :city
3581
3559
  String :zip
3582
3560
  end
3583
- @db.create_table!(:person) do
3561
+ @db.create_table(:person) do
3584
3562
  Integer :id
3585
3563
  address :address
3586
3564
  end
3587
- @db.create_table!(:company) do
3565
+ @db.create_table(:company) do
3588
3566
  Integer :id
3589
3567
  column :employees, 'person[]'
3590
3568
  end
@@ -598,3 +598,55 @@ describe "A SQLite database" do
598
598
  @db.indexes(:test2).values.first[:columns].must_equal [:name]
599
599
  end if DB.sqlite_version >= 30808
600
600
  end
601
+
602
+ describe "SQLite", 'INSERT ON CONFLICT' do
603
+ before(:all) do
604
+ @db = DB
605
+ @db.create_table!(:ic_test){Integer :a; Integer :b; Integer :c; TrueClass :c_is_unique, :default=>false; unique :a, :name=>:ic_test_a_uidx; unique [:b, :c], :name=>:ic_test_b_c_uidx; index [:c], :where=>:c_is_unique, :unique=>true}
606
+ @ds = @db[:ic_test]
607
+ end
608
+ before do
609
+ @ds.delete
610
+ end
611
+ after(:all) do
612
+ @db.drop_table?(:ic_test)
613
+ end
614
+
615
+ it "Dataset#insert_ignore and insert_conflict should ignore uniqueness violations" do
616
+ @ds.insert(1, 2, 3, false)
617
+ @ds.insert(10, 11, 3, true)
618
+ proc{@ds.insert(1, 3, 4, false)}.must_raise Sequel::UniqueConstraintViolation
619
+ proc{@ds.insert(11, 12, 3, true)}.must_raise Sequel::UniqueConstraintViolation
620
+ @ds.insert_ignore.insert(1, 3, 4, false)
621
+ @ds.insert_conflict.insert(1, 3, 4, false)
622
+ @ds.insert_conflict.insert(11, 12, 3, true)
623
+ @ds.insert_conflict(:target=>:a).insert(1, 3, 4, false)
624
+ @ds.insert_conflict(:target=>:c, :conflict_where=>:c_is_unique).insert(11, 12, 3, true)
625
+ @ds.all.must_equal [{:a=>1, :b=>2, :c=>3, :c_is_unique=>false}, {:a=>10, :b=>11, :c=>3, :c_is_unique=>true}]
626
+ end
627
+
628
+ it "Dataset#insert_ignore and insert_conflict should work with multi_insert/import" do
629
+ @ds.insert(1, 2, 3, false)
630
+ @ds.insert_ignore.multi_insert([{:a=>1, :b=>3, :c=>4}])
631
+ @ds.insert_ignore.import([:a, :b, :c], [[1, 3, 4]])
632
+ @ds.all.must_equal [{:a=>1, :b=>2, :c=>3, :c_is_unique=>false}]
633
+ @ds.insert_conflict(:target=>:a, :update=>{:b=>3}).import([:a, :b, :c], [[1, 3, 4]])
634
+ @ds.all.must_equal [{:a=>1, :b=>3, :c=>3, :c_is_unique=>false}]
635
+ @ds.insert_conflict(:target=>:a, :update=>{:b=>4}).multi_insert([{:a=>1, :b=>5, :c=>6}])
636
+ @ds.all.must_equal [{:a=>1, :b=>4, :c=>3, :c_is_unique=>false}]
637
+ end
638
+
639
+ it "Dataset#insert_conflict should handle upserts" do
640
+ @ds.insert(1, 2, 3, false)
641
+ @ds.insert_conflict(:target=>:a, :update=>{:b=>3}).insert(1, 3, 4, false)
642
+ @ds.all.must_equal [{:a=>1, :b=>3, :c=>3, :c_is_unique=>false}]
643
+ @ds.insert_conflict(:target=>[:b, :c], :update=>{:c=>5}).insert(5, 3, 3, false)
644
+ @ds.all.must_equal [{:a=>1, :b=>3, :c=>5, :c_is_unique=>false}]
645
+ @ds.insert_conflict(:target=>:a, :update=>{:b=>4}).insert(1, 3, nil, false)
646
+ @ds.all.must_equal [{:a=>1, :b=>4, :c=>5, :c_is_unique=>false}]
647
+ @ds.insert_conflict(:target=>:a, :update=>{:b=>5}, :update_where=>{Sequel[:ic_test][:b]=>4}).insert(1, 3, 4, false)
648
+ @ds.all.must_equal [{:a=>1, :b=>5, :c=>5, :c_is_unique=>false}]
649
+ @ds.insert_conflict(:target=>:a, :update=>{:b=>6}, :update_where=>{Sequel[:ic_test][:b]=>4}).insert(1, 3, 4, false)
650
+ @ds.all.must_equal [{:a=>1, :b=>5, :c=>5, :c_is_unique=>false}]
651
+ end
652
+ end if DB.sqlite_version >= 32400
@@ -4189,7 +4189,7 @@ describe "Sequel::Dataset#qualify" do
4189
4189
  end
4190
4190
  end
4191
4191
 
4192
- describe "Sequel::Dataset #with and #with_recursive" do
4192
+ describe "Dataset#with and #with_recursive" do
4193
4193
  before do
4194
4194
  @db = Sequel.mock
4195
4195
  @ds = @db[:t].with_extend{def supports_cte?(*) true end}
@@ -4251,6 +4251,27 @@ describe "Sequel::Dataset #with and #with_recursive" do
4251
4251
  end
4252
4252
  end
4253
4253
 
4254
+ describe "Dataset#window" do
4255
+ before do
4256
+ @db = Sequel.mock
4257
+ @ds = @db[:t].with_extend do
4258
+ Sequel::Dataset.def_sql_method(self, :select, %w'select columns from window')
4259
+ def supports_window_clause?; true end
4260
+ def supports_window_functions?; true end
4261
+ end
4262
+ end
4263
+
4264
+ it "should not support window clause by default" do
4265
+ @db.dataset.supports_window_clause?.must_equal false
4266
+ end
4267
+
4268
+ it "should take a name and hash of window options" do
4269
+ ds = @ds.window(:w, :partition=>:a, :order=>:b)
4270
+ ds.sql.must_equal 'SELECT * FROM t WINDOW w AS (PARTITION BY a ORDER BY b)'
4271
+ ds.window(:w2, :partition=>:c, :order=>:d).sql.must_equal 'SELECT * FROM t WINDOW w AS (PARTITION BY a ORDER BY b), w2 AS (PARTITION BY c ORDER BY d)'
4272
+ end
4273
+ end
4274
+
4254
4275
  describe Sequel::SQL::Constants do
4255
4276
  before do
4256
4277
  @db = Sequel::Database.new
@@ -583,18 +583,75 @@ describe Sequel::SQL::VirtualRow do
583
583
  @d.l{count.function.*.over}.must_equal 'count(*) OVER ()'
584
584
  end
585
585
 
586
- it "should handle method.function.over(:frame=>:all) as a window function call" do
586
+ it "should handle method.function.over(:frame=>:all) as a window function call with frame for all rows" do
587
587
  @d.l{rank.function.over(:frame=>:all)}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)'
588
588
  end
589
589
 
590
- it "should handle method.function.over(:frame=>:rows) as a window function call" do
590
+ it "should handle method.function.over(:frame=>:rows) as a window function call with frame for all rows before current row" do
591
591
  @d.l{rank.function.over(:frame=>:rows)}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
592
592
  end
593
593
 
594
- it "should handle method.function.over(:frame=>'some string') as a window function call" do
594
+ it "should handle method.function.over(:frame=>:groups) as a window function call with frame for all groups before current row" do
595
+ @d.l{rank.function.over(:frame=>:groups)}.must_equal 'rank() OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
596
+ end
597
+
598
+ it "should handle method.function.over(:frame=>:range) as a window function call with frame for all groups before current row" do
599
+ @d.l{rank.function.over(:frame=>:range)}.must_equal 'rank() OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
600
+ end
601
+
602
+ it "should handle window function with :frame hash argument with :type option" do
603
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding})}.must_equal 'rank() OVER (ROWS UNBOUNDED PRECEDING)'
604
+ @d.l{rank.function.over(:frame=>{:type=>:range, :start=>:preceding})}.must_equal 'rank() OVER (RANGE UNBOUNDED PRECEDING)'
605
+ @d.l{rank.function.over(:frame=>{:type=>:groups, :start=>:preceding})}.must_equal 'rank() OVER (GROUPS UNBOUNDED PRECEDING)'
606
+ end
607
+
608
+ it "should handle window function with :frame hash argument with :start option" do
609
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding})}.must_equal 'rank() OVER (ROWS UNBOUNDED PRECEDING)'
610
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:following})}.must_equal 'rank() OVER (ROWS UNBOUNDED FOLLOWING)'
611
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:current})}.must_equal 'rank() OVER (ROWS CURRENT ROW)'
612
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>1})}.must_equal 'rank() OVER (ROWS 1 PRECEDING)'
613
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>[1, :following]})}.must_equal 'rank() OVER (ROWS 1 FOLLOWING)'
614
+ end
615
+
616
+ it "should handle window function with :frame hash argument with :end option" do
617
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>:preceding})}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED PRECEDING)'
618
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>:following})}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)'
619
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>:current})}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
620
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>1})}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING)'
621
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>[1, :preceding]})}.must_equal 'rank() OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)'
622
+ end
623
+
624
+ it "should handle window function with :frame hash argument with :exclude option" do
625
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding,:exclude=>:current})}.must_equal 'rank() OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW)'
626
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :exclude=>:group})}.must_equal 'rank() OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP)'
627
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :exclude=>:ties})}.must_equal 'rank() OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES)'
628
+ @d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :exclude=>:no_others})}.must_equal 'rank() OVER (ROWS UNBOUNDED PRECEDING EXCLUDE NO OTHERS)'
629
+ end
630
+
631
+ it "should handle window function with :frame hash argument with invalid options" do
632
+ proc{@d.l{rank.function.over(:frame=>{:type=>:blah, :start=>:preceding})}}.must_raise Sequel::Error
633
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows})}}.must_raise Sequel::Error
634
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:blah})}}.must_raise Sequel::Error
635
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>[1, :blah]})}}.must_raise Sequel::Error
636
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>[1, :preceding, 3]})}}.must_raise Sequel::Error
637
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>:blah})}}.must_raise Sequel::Error
638
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>[1, :blah]})}}.must_raise Sequel::Error
639
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :end=>[1, :following, 3]})}}.must_raise Sequel::Error
640
+ proc{@d.l{rank.function.over(:frame=>{:type=>:rows, :start=>:preceding, :exclude=>:blah})}}.must_raise Sequel::Error
641
+ end
642
+
643
+ it "should handle method.function.over(:frame=>'some string') as a window function call with explicit frame" do
595
644
  @d.l{rank.function.over(:frame=>'RANGE BETWEEN 3 PRECEDING AND CURRENT ROW')}.must_equal 'rank() OVER (RANGE BETWEEN 3 PRECEDING AND CURRENT ROW)'
596
645
  end
597
646
 
647
+ it "should support window functions options" do
648
+ @d.supports_window_function_frame_option?(:rows).must_equal true
649
+ @d.supports_window_function_frame_option?(:range).must_equal true
650
+ @d.supports_window_function_frame_option?(:groups).must_equal false
651
+ @d.supports_window_function_frame_option?(:offset).must_equal true
652
+ @d.supports_window_function_frame_option?(:exclude).must_equal false
653
+ end
654
+
598
655
  it "should raise an error if an invalid :frame option is used" do
599
656
  proc{@d.l{rank.function.over(:frame=>:blah)}}.must_raise(Sequel::Error)
600
657
  end
@@ -69,6 +69,13 @@ describe "eval_inspect extension" do
69
69
  v = eval(o.inspect)
70
70
  v.must_equal o
71
71
  @ds.literal(v).must_equal @ds.literal(o)
72
+
73
+ ds = @ds
74
+ @ds.db.create_table(:test) do
75
+ v = eval(o.inspect)
76
+ v.must_equal o
77
+ ds.literal(v).must_equal ds.literal(o)
78
+ end
72
79
  end
73
80
  end
74
81
  end
@@ -673,8 +673,8 @@ END_MIG
673
673
  s
674
674
  end
675
675
  e = RUBY_VERSION >= '2.4' ? 'e' : 'E'
676
- @d.dump_table_schema(:t4).gsub(/[+-]\d\d\d\d"\)/, '")').gsub(/\.0+/, '.0').must_equal "create_table(:t4) do\n TrueClass :c1, :default=>false\n String :c2, :default=>\"blah\"\n Integer :c3, :default=>-1\n Float :c4, :default=>1.0\n BigDecimal :c5, :default=>BigDecimal(\"0.1005#{e}3\")\n File :c6, :default=>Sequel::SQL::Blob.new(\"blah\")\n Date :c7, :default=>Date.new(2008, 10, 29)\n DateTime :c8, :default=>DateTime.parse(\"2008-10-29T10:20:30.0\")\n Time :c9, :default=>Sequel::SQLTime.parse(\"10:20:30.0\"), :only_time=>true\n String :c10\n Date :c11, :default=>Sequel::CURRENT_DATE\n DateTime :c12, :default=>Sequel::CURRENT_TIMESTAMP\nend"
677
- @d.dump_table_schema(:t4, :same_db=>true).gsub(/[+-]\d\d\d\d"\)/, '")').gsub(/\.0+/, '.0').must_equal "create_table(:t4) do\n column :c1, \"boolean\", :default=>false\n column :c2, \"varchar\", :default=>\"blah\"\n column :c3, \"integer\", :default=>-1\n column :c4, \"float\", :default=>1.0\n column :c5, \"decimal\", :default=>BigDecimal(\"0.1005#{e}3\")\n column :c6, \"blob\", :default=>Sequel::SQL::Blob.new(\"blah\")\n column :c7, \"date\", :default=>Date.new(2008, 10, 29)\n column :c8, \"datetime\", :default=>DateTime.parse(\"2008-10-29T10:20:30.0\")\n column :c9, \"time\", :default=>Sequel::SQLTime.parse(\"10:20:30.0\")\n column :c10, \"foo\", :default=>Sequel::LiteralString.new(\"'6 weeks'\")\n column :c11, \"date\", :default=>Sequel::CURRENT_DATE\n column :c12, \"timestamp\", :default=>Sequel::CURRENT_TIMESTAMP\nend"
676
+ @d.dump_table_schema(:t4).gsub(/[+-]\d\d\d\d"\)/, '")').gsub(/\.0+/, '.0').must_equal "create_table(:t4) do\n TrueClass :c1, :default=>false\n String :c2, :default=>\"blah\"\n Integer :c3, :default=>-1\n Float :c4, :default=>1.0\n BigDecimal :c5, :default=>Kernel::BigDecimal(\"0.1005#{e}3\")\n File :c6, :default=>Sequel::SQL::Blob.new(\"blah\")\n Date :c7, :default=>Date.new(2008, 10, 29)\n DateTime :c8, :default=>DateTime.parse(\"2008-10-29T10:20:30.0\")\n Time :c9, :default=>Sequel::SQLTime.parse(\"10:20:30.0\"), :only_time=>true\n String :c10\n Date :c11, :default=>Sequel::CURRENT_DATE\n DateTime :c12, :default=>Sequel::CURRENT_TIMESTAMP\nend"
677
+ @d.dump_table_schema(:t4, :same_db=>true).gsub(/[+-]\d\d\d\d"\)/, '")').gsub(/\.0+/, '.0').must_equal "create_table(:t4) do\n column :c1, \"boolean\", :default=>false\n column :c2, \"varchar\", :default=>\"blah\"\n column :c3, \"integer\", :default=>-1\n column :c4, \"float\", :default=>1.0\n column :c5, \"decimal\", :default=>Kernel::BigDecimal(\"0.1005#{e}3\")\n column :c6, \"blob\", :default=>Sequel::SQL::Blob.new(\"blah\")\n column :c7, \"date\", :default=>Date.new(2008, 10, 29)\n column :c8, \"datetime\", :default=>DateTime.parse(\"2008-10-29T10:20:30.0\")\n column :c9, \"time\", :default=>Sequel::SQLTime.parse(\"10:20:30.0\")\n column :c10, \"foo\", :default=>Sequel::LiteralString.new(\"'6 weeks'\")\n column :c11, \"date\", :default=>Sequel::CURRENT_DATE\n column :c12, \"timestamp\", :default=>Sequel::CURRENT_TIMESTAMP\nend"
678
678
  end
679
679
 
680
680
  it "should not use a literal string as a fallback if using MySQL with the :same_db option" do
@@ -45,6 +45,41 @@ describe "Sequel::Plugins::StaticCache" do
45
45
  @db.sqls.must_equal []
46
46
  end
47
47
 
48
+ it "should have first just returns instances without sending a query" do
49
+ @c.first.must_equal @c1
50
+ @c.first(2).must_equal [@c1, @c2]
51
+ @c.first(0).must_equal []
52
+ @db.sqls.must_equal []
53
+ end
54
+
55
+ it "should have first just returns instances with sending a query" do
56
+ @db.fetch = lambda do |s|
57
+ case s
58
+ when /id = '?(\d+)'?/
59
+ id = $1.to_i
60
+ id <= 2 ? { id: id } : nil
61
+ when /id >= '?(\d+)'?/
62
+ id = $1.to_i
63
+ id <= 2 ? (id..2).map { |i| { id: i } } : []
64
+ end
65
+ end
66
+
67
+ @c.first(id: 2).must_equal @c2
68
+ @c.first(id: '2').must_equal @c2
69
+ @c.first(id: 3).must_be_nil
70
+ @c.first { id >= 2 }.must_equal @c2
71
+ @c.first(2) { id >= 1 }.must_equal [@c1, @c2]
72
+ @c.first(Sequel.lit('id = ?', 2)).must_equal @c2
73
+ @db.sqls.must_equal [
74
+ "SELECT * FROM t WHERE (id = 2) LIMIT 1",
75
+ "SELECT * FROM t WHERE (id = '2') LIMIT 1",
76
+ "SELECT * FROM t WHERE (id = 3) LIMIT 1",
77
+ "SELECT * FROM t WHERE (id >= 2) LIMIT 1",
78
+ "SELECT * FROM t WHERE (id >= 1) LIMIT 2",
79
+ "SELECT * FROM t WHERE (id = 2) LIMIT 1"
80
+ ]
81
+ end
82
+
48
83
  it "should have each just iterate over the hash's values without sending a query" do
49
84
  a = []
50
85
  @c.each{|o| a << o}
@@ -108,6 +108,13 @@ describe Sequel::Model, "tree plugin" do
108
108
  "SELECT * FROM nodes WHERE (nodes.parent_id = 1)"]
109
109
  end
110
110
 
111
+ it "should have self_and_siblings return the roots if the current object is a root" do
112
+ h = {:id=>2, :parent_id=>nil, :name=>'AA'}
113
+ @c.dataset = @c.dataset.with_fetch(h)
114
+ @c.load(h).self_and_siblings.must_equal [@c.load(h)]
115
+ @db.sqls.must_equal ["SELECT * FROM nodes WHERE (parent_id IS NULL)"]
116
+ end
117
+
111
118
  it "should have siblings return the children of the current node's parent, except for the current node" do
112
119
  @c.dataset = @c.dataset.with_fetch([[{:id=>1, :parent_id=>3, :name=>'r'}], [{:id=>7, :parent_id=>1, :name=>'r2'}, @o.values.dup]])
113
120
  @o.siblings.must_equal [@c.load(:id=>7, :parent_id=>1, :name=>'r2')]
@@ -902,6 +902,8 @@ if DB.dataset.supports_window_functions?
902
902
  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}]
903
903
  @ds.select(:id){sum(:amount).over(:order=>:id).as(:sum)}.all.
904
904
  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}]
905
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>id.desc, :frame=>:rows).as(:sum)}.all.
906
+ must_equal [{:sum=>111, :id=>1}, {:sum=>110, :id=>2}, {:sum=>100, :id=>3}, {:sum=>111000, :id=>4}, {:sum=>110000, :id=>5}, {:sum=>100000, :id=>6}]
905
907
  end
906
908
 
907
909
  it "should give correct results for aggregate window functions with frames" do
@@ -915,6 +917,71 @@ if DB.dataset.supports_window_functions?
915
917
  @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>:rows).as(:sum)}.all.
916
918
  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}]
917
919
  end
920
+
921
+ it "should give correct results for aggregate window functions with ranges" do
922
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>:id, :frame=>:range).as(:sum)}.all.
923
+ 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}]
924
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>:group_id, :frame=>:range).as(:sum)}.all.
925
+ 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}]
926
+ end if DB.dataset.supports_window_function_frame_option?(:range)
927
+
928
+ it "should give correct results for aggregate window functions with groups" do
929
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :order=>:id, :frame=>:groups).as(:sum)}.all.
930
+ 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}]
931
+ @ds.select(:id){sum(:amount).over(:partition=>:group_id, :frame=>:groups).as(:sum)}.all.
932
+ 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}]
933
+ end if DB.dataset.supports_window_function_frame_option?(:groups)
934
+
935
+ if DB.dataset.supports_window_function_frame_option?(:offset)
936
+ it "should give correct results for aggregate window functions with offsets for ROWS" do
937
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>{:type=>:rows, :start=>1}).as(:sum)}.all.
938
+ must_equal [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>110, :id=>3}, {:sum=>1100, :id=>4}, {:sum=>11000, :id=>5}, {:sum=>110000, :id=>6}]
939
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>{:type=>:rows, :start=>1, :end=>1}).as(:sum)}.all.
940
+ must_equal [{:sum=>11, :id=>1}, {:sum=>111, :id=>2}, {:sum=>1110, :id=>3}, {:sum=>11100, :id=>4}, {:sum=>111000, :id=>5}, {:sum=>110000, :id=>6}]
941
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>{:type=>:rows, :start=>2, :end=>[1, :preceding]}).as(:sum)}.all.
942
+ must_equal [{:sum=>nil, :id=>1}, {:sum=>1, :id=>2}, {:sum=>11, :id=>3}, {:sum=>110, :id=>4}, {:sum=>1100, :id=>5}, {:sum=>11000, :id=>6}]
943
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>{:type=>:rows, :start=>[1, :following], :end=>2}).as(:sum)}.order(:id).all.
944
+ must_equal [{:sum=>110, :id=>1}, {:sum=>1100, :id=>2}, {:sum=>11000, :id=>3}, {:sum=>110000, :id=>4}, {:sum=>100000, :id=>5}, {:sum=>nil, :id=>6}]
945
+ end
946
+
947
+ cspecify "should give correct results for aggregate window functions with offsets for RANGES", :mssql, [proc{DB.server_version < 110000}, :postgres] do
948
+ @ds.select(:id){sum(:amount).over(:order=>:group_id, :frame=>{:type=>:range, :start=>1}).as(:sum)}.all.
949
+ must_equal [{:sum=>111, :id=>1}, {:sum=>111, :id=>2}, {:sum=>111, :id=>3}, {:sum=>111111, :id=>4}, {:sum=>111111, :id=>5}, {:sum=>111111, :id=>6}]
950
+ @ds.select(:id){sum(:amount).over(:order=>:group_id, :frame=>{:type=>:range, :start=>0, :end=>1}).as(:sum)}.all.
951
+ must_equal [{:sum=>111111, :id=>1}, {:sum=>111111, :id=>2}, {:sum=>111111, :id=>3}, {:sum=>111000, :id=>4}, {:sum=>111000, :id=>5}, {:sum=>111000, :id=>6}]
952
+ @ds.select(:id){sum(:amount).over(:order=>:amount, :frame=>{:type=>:range, :start=>100, :end=>1000}).as(:sum)}.all.
953
+ must_equal [{:sum=>1111, :id=>1}, {:sum=>1111, :id=>2}, {:sum=>1111, :id=>3}, {:sum=>1000, :id=>4}, {:sum=>10000, :id=>5}, {:sum=>100000, :id=>6}]
954
+ end if DB.dataset.supports_window_function_frame_option?(:range)
955
+
956
+ it "should give correct results for aggregate window functions with offsets for GROUPS" do
957
+ @ds.select(:id){sum(:amount).over(:order=>:group_id, :frame=>{:type=>:groups, :start=>1}).as(:sum)}.all.
958
+ must_equal [{:sum=>111, :id=>1}, {:sum=>111, :id=>2}, {:sum=>111, :id=>3}, {:sum=>111111, :id=>4}, {:sum=>111111, :id=>5}, {:sum=>111111, :id=>6}]
959
+ @ds.select(:id){sum(:amount).over(:order=>:group_id, :frame=>{:type=>:groups, :start=>0, :end=>1}).as(:sum)}.all.
960
+ must_equal [{:sum=>111111, :id=>1}, {:sum=>111111, :id=>2}, {:sum=>111111, :id=>3}, {:sum=>111000, :id=>4}, {:sum=>111000, :id=>5}, {:sum=>111000, :id=>6}]
961
+ end if DB.dataset.supports_window_function_frame_option?(:groups)
962
+ end
963
+
964
+ it "should give correct results for aggregate window functions with exclusions" do
965
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>{:type=>:rows, :start=>:preceding, :exclude=>:current}).as(:sum)}.all.
966
+ must_equal [{:sum=>nil, :id=>1}, {:sum=>1, :id=>2}, {:sum=>11, :id=>3}, {:sum=>111, :id=>4}, {:sum=>1111, :id=>5}, {:sum=>11111, :id=>6}]
967
+ @ds.select(:id){sum(:amount).over(:order=>:group_id, :frame=>{:type=>:rows, :start=>:preceding, :exclude=>:group}).as(:sum)}.all.
968
+ must_equal [{:sum=>nil, :id=>1}, {:sum=>nil, :id=>2}, {:sum=>nil, :id=>3}, {:sum=>111, :id=>4}, {:sum=>111, :id=>5}, {:sum=>111, :id=>6}]
969
+ @ds.select(:id){sum(:amount).over(:order=>:group_id, :frame=>{:type=>:rows, :start=>:preceding, :exclude=>:ties}).as(:sum)}.all.
970
+ must_equal [{:sum=>1, :id=>1}, {:sum=>10, :id=>2}, {:sum=>100, :id=>3}, {:sum=>1111, :id=>4}, {:sum=>10111, :id=>5}, {:sum=>100111, :id=>6}]
971
+ @ds.select(:id){sum(:amount).over(:order=>:id, :frame=>{:type=>:rows, :start=>:preceding, :exclude=>:no_others}).as(:sum)}.all.
972
+ 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}]
973
+ end if DB.dataset.supports_window_function_frame_option?(:exclude)
974
+
975
+ it "should give correct results for window functions" do
976
+ @ds.window(:win, :partition=>:group_id, :order=>:id).select(:id){sum(:amount).over(:window=>win, :frame=>:rows).as(:sum)}.all.
977
+ 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}]
978
+ @ds.window(:win, :partition=>:group_id).select(:id){sum(:amount).over(:window=>win, :order=>id, :frame=>:rows).as(:sum)}.all.
979
+ 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}]
980
+ @ds.window(:win, {}).select(:id){sum(:amount).over(:window=>:win, :order=>id, :frame=>:rows).as(:sum)}.all.
981
+ 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}]
982
+ @ds.window(:win, :partition=>:group_id).select(:id){sum(:amount).over(:window=>:win, :order=>id, :frame=>:all).as(:sum)}.all.
983
+ 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}]
984
+ end if DB.dataset.supports_window_clause?
918
985
  end
919
986
  end
920
987
 
@@ -1989,6 +1989,13 @@ describe "Caching plugins" do
1989
1989
  end
1990
1990
 
1991
1991
  include CachingPluginSpecs
1992
+
1993
+ it "should have first retrieve correct values" do
1994
+ @Artist.first.must_equal @Artist.load(:id=>1)
1995
+ @Artist.first(1).must_equal [@Artist.load(:id=>1)]
1996
+ @Artist.first(:id=>1).must_equal @Artist.load(:id=>1)
1997
+ @Artist.first{id =~ 1}.must_equal @Artist.load(:id=>1)
1998
+ end
1992
1999
  end
1993
2000
  end
1994
2001
 
@@ -634,6 +634,26 @@ describe "Database schema modifiers" do
634
634
  @ds.all.must_equal [{:id=>10}, {:id=>20}]
635
635
  end
636
636
 
637
+ it "should set column defaults to nil correctly" do
638
+ @db.create_table!(:items){Integer :id}
639
+ @ds.insert(:id=>10)
640
+ @db.alter_table(:items){set_column_default :id, nil}
641
+ @db.schema(:items, :reload=>true).map{|x| x.first}.must_equal [:id]
642
+ @ds.columns!.must_equal [:id]
643
+ @ds.insert
644
+ @ds.all.must_equal [{:id=>10}, {:id=>nil}]
645
+ end
646
+
647
+ it "should set column defaults to nil for NOT NULL columns correctly" do
648
+ @db.create_table!(:items){Integer :id, :null=>false}
649
+ @ds.insert(:id=>10)
650
+ @db.alter_table(:items){set_column_default :id, nil}
651
+ @db.schema(:items, :reload=>true).map{|x| x.first}.must_equal [:id]
652
+ @ds.columns!.must_equal [:id]
653
+ @ds.insert(20)
654
+ @ds.all.must_equal [{:id=>10}, {:id=>20}]
655
+ end
656
+
637
657
  cspecify "should set column types correctly", [:jdbc, :db2], :oracle do
638
658
  @db.create_table!(:items){Integer :id}
639
659
  @ds.insert(:id=>10)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.10.0
4
+ version: 5.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-01 00:00:00.000000000 Z
11
+ date: 2018-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -194,6 +194,7 @@ extra_rdoc_files:
194
194
  - doc/release_notes/5.7.0.txt
195
195
  - doc/release_notes/5.9.0.txt
196
196
  - doc/release_notes/5.10.0.txt
197
+ - doc/release_notes/5.11.0.txt
197
198
  files:
198
199
  - CHANGELOG
199
200
  - MIT-LICENSE
@@ -274,6 +275,7 @@ files:
274
275
  - doc/release_notes/5.0.0.txt
275
276
  - doc/release_notes/5.1.0.txt
276
277
  - doc/release_notes/5.10.0.txt
278
+ - doc/release_notes/5.11.0.txt
277
279
  - doc/release_notes/5.2.0.txt
278
280
  - doc/release_notes/5.3.0.txt
279
281
  - doc/release_notes/5.4.0.txt