sequel 5.10.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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