sequel 5.38.0 → 5.43.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +54 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/doc/cheat_sheet.rdoc +5 -5
  6. data/doc/code_order.rdoc +0 -12
  7. data/doc/fork_safety.rdoc +84 -0
  8. data/doc/postgresql.rdoc +1 -1
  9. data/doc/querying.rdoc +3 -3
  10. data/doc/release_notes/5.39.0.txt +19 -0
  11. data/doc/release_notes/5.40.0.txt +40 -0
  12. data/doc/release_notes/5.41.0.txt +25 -0
  13. data/doc/release_notes/5.42.0.txt +136 -0
  14. data/doc/release_notes/5.43.0.txt +98 -0
  15. data/doc/sql.rdoc +1 -1
  16. data/doc/testing.rdoc +2 -0
  17. data/lib/sequel/adapters/ado.rb +16 -16
  18. data/lib/sequel/adapters/jdbc.rb +2 -2
  19. data/lib/sequel/adapters/shared/mssql.rb +21 -1
  20. data/lib/sequel/adapters/shared/postgres.rb +5 -3
  21. data/lib/sequel/adapters/shared/sqlite.rb +35 -1
  22. data/lib/sequel/database/misc.rb +1 -2
  23. data/lib/sequel/database/schema_generator.rb +16 -1
  24. data/lib/sequel/database/schema_methods.rb +19 -5
  25. data/lib/sequel/database/transactions.rb +1 -1
  26. data/lib/sequel/dataset/features.rb +10 -0
  27. data/lib/sequel/dataset/prepared_statements.rb +2 -0
  28. data/lib/sequel/dataset/sql.rb +32 -10
  29. data/lib/sequel/extensions/async_thread_pool.rb +438 -0
  30. data/lib/sequel/extensions/blank.rb +8 -0
  31. data/lib/sequel/extensions/date_arithmetic.rb +7 -9
  32. data/lib/sequel/extensions/eval_inspect.rb +2 -0
  33. data/lib/sequel/extensions/inflector.rb +8 -0
  34. data/lib/sequel/extensions/migration.rb +2 -0
  35. data/lib/sequel/extensions/named_timezones.rb +5 -1
  36. data/lib/sequel/extensions/pg_array.rb +1 -0
  37. data/lib/sequel/extensions/pg_interval.rb +34 -8
  38. data/lib/sequel/extensions/pg_row.rb +1 -0
  39. data/lib/sequel/extensions/query.rb +2 -0
  40. data/lib/sequel/model/associations.rb +28 -4
  41. data/lib/sequel/model/base.rb +23 -6
  42. data/lib/sequel/model/plugins.rb +5 -0
  43. data/lib/sequel/plugins/association_proxies.rb +2 -0
  44. data/lib/sequel/plugins/async_thread_pool.rb +39 -0
  45. data/lib/sequel/plugins/auto_validations.rb +15 -1
  46. data/lib/sequel/plugins/column_encryption.rb +711 -0
  47. data/lib/sequel/plugins/composition.rb +7 -2
  48. data/lib/sequel/plugins/constraint_validations.rb +2 -1
  49. data/lib/sequel/plugins/dataset_associations.rb +4 -1
  50. data/lib/sequel/plugins/json_serializer.rb +37 -22
  51. data/lib/sequel/plugins/nested_attributes.rb +8 -3
  52. data/lib/sequel/plugins/pg_array_associations.rb +4 -0
  53. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +2 -0
  54. data/lib/sequel/plugins/serialization.rb +8 -3
  55. data/lib/sequel/plugins/serialization_modification_detection.rb +1 -1
  56. data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
  57. data/lib/sequel/plugins/tree.rb +9 -4
  58. data/lib/sequel/plugins/validation_helpers.rb +6 -2
  59. data/lib/sequel/timezones.rb +8 -3
  60. data/lib/sequel/version.rb +1 -1
  61. metadata +36 -21
@@ -0,0 +1,98 @@
1
+ = New Features
2
+
3
+ * A column_encryption plugin has been added to support encrypting the
4
+ content of individual columns in a table.
5
+
6
+ Column values are encrypted with AES-256-GCM using a per-value
7
+ cipher key derived from a key provided in the configuration using
8
+ HMAC-SHA256.
9
+
10
+ If you would like to support encryption of columns in more than one
11
+ model, you should probably load the plugin into the parent class of
12
+ your models and specify the keys:
13
+
14
+ Sequel::Model.plugin :column_encryption do |enc|
15
+ enc.key 0, ENV["SEQUEL_COLUMN_ENCRYPTION_KEY"]
16
+ end
17
+
18
+ This specifies a single master encryption key. Unless you are
19
+ actively rotating keys, it is best to use a single master key.
20
+
21
+ In the above call, 0 is the id of the key, and
22
+ ENV["SEQUEL_COLUMN_ENCRYPTION_KEY"] is the content of the key, which
23
+ must be a string with exactly 32 bytes. As indicated, this key
24
+ should not be hardcoded or otherwise committed to the source control
25
+ repository.
26
+
27
+ For models that need encrypted columns, you load the plugin again,
28
+ but specify the columns to encrypt:
29
+
30
+ ConfidentialModel.plugin :column_encryption do |enc|
31
+ enc.column :encrypted_column_name
32
+ enc.column :searchable_column_name, searchable: true
33
+ enc.column :ci_searchable_column_name, searchable: :case_insensitive
34
+ end
35
+
36
+ With this, all three specified columns (encrypted_column_name,
37
+ searchable_column_name, and ci_searchable_column_name) will be
38
+ marked as encrypted columns. When you run the following code:
39
+
40
+ ConfidentialModel.create(
41
+ encrypted_column_name: 'These',
42
+ searchable_column_name: 'will be',
43
+ ci_searchable_column_name: 'Encrypted'
44
+ )
45
+
46
+ It will save encrypted versions to the database.
47
+ encrypted_column_name will not be searchable, searchable_column_name
48
+ will be searchable with an exact match, and
49
+ ci_searchable_column_name will be searchable with a case insensitive
50
+ match.
51
+
52
+ To search searchable encrypted columns, use with_encrypted_value.
53
+ This example code will return the model instance created in the code
54
+ example in the previous section:
55
+
56
+ ConfidentialModel.
57
+ with_encrypted_value(:searchable_column_name, "will be")
58
+ with_encrypted_value(:ci_searchable_column_name, "encrypted").
59
+ first
60
+
61
+ To rotate encryption keys, add a new key above the existing key,
62
+ with a new key ID:
63
+
64
+ Sequel::Model.plugin :column_encryption do |enc|
65
+ enc.key 1, ENV["SEQUEL_COLUMN_ENCRYPTION_KEY"]
66
+ enc.key 0, ENV["SEQUEL_OLD_COLUMN_ENCRYPTION_KEY"]
67
+ end
68
+
69
+ Newly encrypted data will then use the new key. Records encrypted
70
+ with the older key will still be decrypted correctly.
71
+
72
+ To force reencryption for existing records that are using the older
73
+ key, you can use the needing_reencryption dataset method and the
74
+ reencrypt instance method. For a small number of records, you can
75
+ probably do:
76
+
77
+ ConfidentialModel.needing_reencryption.all(&:reencrypt)
78
+
79
+ With more than a small number of records, you'll want to do this in
80
+ batches. It's possible you could use an approach such as:
81
+
82
+ ds = ConfidentialModel.needing_reencryption.limit(100)
83
+ true until ds.all(&:reencrypt).empty?
84
+
85
+ After all values have been reencrypted for all models, and no models
86
+ use the older encryption key, you can remove it from the
87
+ configuration:
88
+
89
+ Sequel::Model.plugin :column_encryption do |enc|
90
+ enc.key 1, ENV["SEQUEL_COLUMN_ENCRYPTION_KEY"]
91
+ end
92
+
93
+ The column_encryption plugin supports encrypting serialized data,
94
+ as well as enforcing uniquenss of searchable encrypted columns
95
+ (in the absence of key rotation). By design, it does not support
96
+ compression, mixing encrypted and unencrypted data in the same
97
+ column, or support arbitrary encryption ciphers. See the plugin
98
+ documentation for more details.
data/doc/sql.rdoc CHANGED
@@ -544,7 +544,7 @@ On some databases, you can specify null ordering:
544
544
 
545
545
  === All Columns (.*)
546
546
 
547
- To select all columns in a table, Sequel supports the * method on identifiers and qualified without an argument:
547
+ To select all columns in a table, Sequel supports the * method on identifiers and qualified identifiers without an argument:
548
548
 
549
549
  Sequel[:table].* # "table".*
550
550
  Sequel[:schema][:table].* # "schema"."table".*
data/doc/testing.rdoc CHANGED
@@ -157,6 +157,8 @@ The SEQUEL_INTEGRATION_URL environment variable specifies the Database connectio
157
157
 
158
158
  === Other
159
159
 
160
+ SEQUEL_ASYNC_THREAD_POOL :: Use the async_thread_pool extension when running the specs
161
+ SEQUEL_ASYNC_THREAD_POOL_PREEMPT :: Use the async_thread_pool extension when running the specs, with the :preempt_async_thread option
160
162
  SEQUEL_COLUMNS_INTROSPECTION :: Use the columns_introspection extension when running the specs
161
163
  SEQUEL_CONNECTION_VALIDATOR :: Use the connection validator extension when running the specs
162
164
  SEQUEL_DUPLICATE_COLUMNS_HANDLER :: Use the duplicate columns handler extension with value given when running the specs
@@ -195,10 +195,25 @@ module Sequel
195
195
  end
196
196
 
197
197
  @conversion_procs = CONVERSION_PROCS.dup
198
+ @conversion_procs[AdDBTimeStamp] = method(:adb_timestamp_to_application_timestamp)
198
199
 
199
200
  super
200
201
  end
201
202
 
203
+ def adb_timestamp_to_application_timestamp(v)
204
+ # This hard codes a timestamp_precision of 6 when converting.
205
+ # That is the default timestamp_precision, but the ado/mssql adapter uses a timestamp_precision
206
+ # of 3. However, timestamps returned by ado/mssql have nsec values that end up rounding to a
207
+ # the same value as if a timestamp_precision of 3 was hard coded (either xxx999yzz, where y is
208
+ # 5-9 or xxx000yzz where y is 0-4).
209
+ #
210
+ # ADO subadapters should override this they would like a different timestamp precision and the
211
+ # this code does not work for them (for example, if they provide full nsec precision).
212
+ #
213
+ # Note that fractional second handling for WIN32OLE objects is not correct on ruby <2.2
214
+ to_application_timestamp([v.year, v.month, v.day, v.hour, v.min, v.sec, (v.nsec/1000.0).round * 1000])
215
+ end
216
+
202
217
  def dataset_class_default
203
218
  Dataset
204
219
  end
@@ -233,23 +248,8 @@ module Sequel
233
248
  cols = []
234
249
  conversion_procs = db.conversion_procs
235
250
 
236
- ts_cp = nil
237
251
  recordset.Fields.each do |field|
238
- type = field.Type
239
- cp = if type == AdDBTimeStamp
240
- ts_cp ||= begin
241
- nsec_div = 1000000000.0/(10**(timestamp_precision))
242
- nsec_mul = 10**(timestamp_precision+3)
243
- meth = db.method(:to_application_timestamp)
244
- lambda do |v|
245
- # Fractional second handling is not correct on ruby <2.2
246
- meth.call([v.year, v.month, v.day, v.hour, v.min, v.sec, (v.nsec/nsec_div).round * nsec_mul])
247
- end
248
- end
249
- else
250
- conversion_procs[type]
251
- end
252
- cols << [output_identifier(field.Name), cp]
252
+ cols << [output_identifier(field.Name), conversion_procs[field.Type]]
253
253
  end
254
254
 
255
255
  self.columns = cols.map(&:first)
@@ -71,11 +71,11 @@ module Sequel
71
71
  class TypeConvertor
72
72
  CONVERTORS = convertors = {}
73
73
  %w'Boolean Float Double Int Long Short'.each do |meth|
74
- x = convertors[meth.to_sym] = Object.new
74
+ x = x = convertors[meth.to_sym] = Object.new
75
75
  class_eval("def x.call(r, i) v = r.get#{meth}(i); v unless r.wasNull end", __FILE__, __LINE__)
76
76
  end
77
77
  %w'Object Array String Time Date Timestamp BigDecimal Blob Bytes Clob'.each do |meth|
78
- x = convertors[meth.to_sym] = Object.new
78
+ x = x = convertors[meth.to_sym] = Object.new
79
79
  class_eval("def x.call(r, i) r.get#{meth}(i) end", __FILE__, __LINE__)
80
80
  end
81
81
  x = convertors[:RubyTime] = Object.new
@@ -244,6 +244,16 @@ module Sequel
244
244
 
245
245
  private
246
246
 
247
+ # Add CLUSTERED or NONCLUSTERED as needed
248
+ def add_clustered_sql_fragment(sql, opts)
249
+ clustered = opts[:clustered]
250
+ unless clustered.nil?
251
+ sql += " #{'NON' unless clustered}CLUSTERED"
252
+ end
253
+
254
+ sql
255
+ end
256
+
247
257
  # Add dropping of the default constraint to the list of SQL queries.
248
258
  # This is necessary before dropping the column or changing its type.
249
259
  def add_drop_default_constraint_sql(sqls, table, column)
@@ -284,7 +294,7 @@ module Sequel
284
294
  when :set_column_null
285
295
  sch = schema(table).find{|k,v| k.to_s == op[:name].to_s}.last
286
296
  type = sch[:db_type]
287
- if [:string, :decimal].include?(sch[:type]) && !["text", "ntext"].include?(type) && (size = (sch[:max_chars] || sch[:column_size]))
297
+ if [:string, :decimal, :blob].include?(sch[:type]) && !["text", "ntext"].include?(type) && (size = (sch[:max_chars] || sch[:column_size]))
288
298
  size = "MAX" if size == -1
289
299
  type += "(#{size}#{", #{sch[:scale]}" if sch[:scale] && sch[:scale].to_i > 0})"
290
300
  end
@@ -396,6 +406,11 @@ module Sequel
396
406
  super.with_quote_identifiers(true)
397
407
  end
398
408
 
409
+ # Handle clustered and nonclustered primary keys
410
+ def primary_key_constraint_sql_fragment(opts)
411
+ add_clustered_sql_fragment(super, opts)
412
+ end
413
+
399
414
  # Use sp_rename to rename the table
400
415
  def rename_table_sql(name, new_name)
401
416
  "sp_rename #{literal(quote_schema_table(name))}, #{quote_identifier(schema_and_table(new_name).pop)}"
@@ -492,6 +507,11 @@ module Sequel
492
507
  :'varbinary(max)'
493
508
  end
494
509
 
510
+ # Handle clustered and nonclustered unique constraints
511
+ def unique_constraint_sql_fragment(opts)
512
+ add_clustered_sql_fragment(super, opts)
513
+ end
514
+
495
515
  # MSSQL supports views with check option, but not local.
496
516
  def view_with_check_option_support
497
517
  true
@@ -846,7 +846,7 @@ module Sequel
846
846
  # :schema :: The schema to search
847
847
  # :server :: The server to use
848
848
  def tables(opts=OPTS, &block)
849
- pg_class_relname('r', opts, &block)
849
+ pg_class_relname(['r', 'p'], opts, &block)
850
850
  end
851
851
 
852
852
  # Check whether the given type name string/symbol (e.g. :hstore) is supported by
@@ -1500,9 +1500,11 @@ module Sequel
1500
1500
  # disallowed or there is a size specified, use the varchar type.
1501
1501
  # Otherwise use the text type.
1502
1502
  def type_literal_generic_string(column)
1503
- if column[:fixed]
1503
+ if column[:text]
1504
+ :text
1505
+ elsif column[:fixed]
1504
1506
  "char(#{column[:size]||255})"
1505
- elsif column[:text] == false or column[:size]
1507
+ elsif column[:text] == false || column[:size]
1506
1508
  "varchar(#{column[:size]||255})"
1507
1509
  else
1508
1510
  :text
@@ -561,7 +561,7 @@ module Sequel
561
561
  Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
562
562
  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']])
563
563
  Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'with values compounds'], ['else', %w'with select distinct columns from join where group having window compounds order limit lock']])
564
- Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
564
+ Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 33300', %w'with update table set from where'], ['elsif db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
565
565
 
566
566
  def cast_sql_append(sql, expr, type)
567
567
  if type == Time or type == DateTime
@@ -753,6 +753,11 @@ module Sequel
753
753
  false
754
754
  end
755
755
 
756
+ # SQLite does not support deleting from a joined dataset
757
+ def supports_deleting_joins?
758
+ false
759
+ end
760
+
756
761
  # SQLite does not support INTERSECT ALL or EXCEPT ALL
757
762
  def supports_intersect_except_all?
758
763
  false
@@ -763,6 +768,11 @@ module Sequel
763
768
  false
764
769
  end
765
770
 
771
+ # SQLite 3.33.0 supports modifying joined datasets
772
+ def supports_modifying_joins?
773
+ db.sqlite_version >= 33300
774
+ end
775
+
766
776
  # SQLite does not support multiple columns for the IN/NOT IN operators
767
777
  def supports_multiple_column_in?
768
778
  false
@@ -825,6 +835,13 @@ module Sequel
825
835
  end
826
836
  end
827
837
 
838
+ # Raise an InvalidOperation exception if insert is not allowed for this dataset.
839
+ def check_insert_allowed!
840
+ raise(InvalidOperation, "Grouped datasets cannot be modified") if opts[:group]
841
+ raise(InvalidOperation, "Joined datasets cannot be modified") if joined_dataset?
842
+ end
843
+ alias check_delete_allowed! check_insert_allowed!
844
+
828
845
  # SQLite supports a maximum of 500 rows in a VALUES clause.
829
846
  def default_import_slice
830
847
  500
@@ -944,6 +961,23 @@ module Sequel
944
961
  def _truncate_sql(table)
945
962
  "DELETE FROM #{table}"
946
963
  end
964
+
965
+ # Use FROM to specify additional tables in an update query
966
+ def update_from_sql(sql)
967
+ if(from = @opts[:from][1..-1]).empty?
968
+ raise(Error, 'Need multiple FROM tables if updating/deleting a dataset with JOINs') if @opts[:join]
969
+ else
970
+ sql << ' FROM '
971
+ source_list_append(sql, from)
972
+ select_join_sql(sql)
973
+ end
974
+ end
975
+
976
+ # Only include the primary table in the main update clause
977
+ def update_table_sql(sql)
978
+ sql << ' '
979
+ source_list_append(sql, @opts[:from][0..0])
980
+ end
947
981
  end
948
982
  end
949
983
  end
@@ -213,8 +213,7 @@ module Sequel
213
213
  Sequel.extension(*exts)
214
214
  exts.each do |ext|
215
215
  if pr = Sequel.synchronize{EXTENSIONS[ext]}
216
- unless Sequel.synchronize{@loaded_extensions.include?(ext)}
217
- Sequel.synchronize{@loaded_extensions << ext}
216
+ if Sequel.synchronize{@loaded_extensions.include?(ext) ? false : (@loaded_extensions << ext)}
218
217
  pr.call(self)
219
218
  end
220
219
  else
@@ -143,8 +143,14 @@ module Sequel
143
143
  # :identity :: Create an identity column.
144
144
  #
145
145
  # MySQL specific options:
146
+ #
146
147
  # :generated_type :: Set the type of column when using :generated_always_as,
147
148
  # should be :virtual or :stored to force a type.
149
+ #
150
+ # Microsoft SQL Server specific options:
151
+ #
152
+ # :clustered :: When using :primary_key or :unique, marks the primary key or unique
153
+ # constraint as CLUSTERED (if true), or NONCLUSTERED (if false).
148
154
  def column(name, type, opts = OPTS)
149
155
  columns << {:name => name, :type => type}.merge!(opts)
150
156
  if index_opts = opts[:index]
@@ -153,7 +159,7 @@ module Sequel
153
159
  nil
154
160
  end
155
161
 
156
- # Adds a named constraint (or unnamed if name is nil),
162
+ # Adds a named CHECK constraint (or unnamed if name is nil),
157
163
  # with the given block or args. To provide options for the constraint, pass
158
164
  # a hash as the first argument.
159
165
  #
@@ -161,6 +167,15 @@ module Sequel
161
167
  # # CONSTRAINT blah CHECK num >= 1 AND num <= 5
162
168
  # constraint({name: :blah, deferrable: true}, num: 1..5)
163
169
  # # CONSTRAINT blah CHECK num >= 1 AND num <= 5 DEFERRABLE INITIALLY DEFERRED
170
+ #
171
+ # If the first argument is a hash, the following options are supported:
172
+ #
173
+ # Options:
174
+ # :name :: The name of the CHECK constraint
175
+ # :deferrable :: Whether the CHECK constraint should be marked DEFERRABLE.
176
+ #
177
+ # PostgreSQL specific options:
178
+ # :not_valid :: Whether the CHECK constraint should be marked NOT VALID.
164
179
  def constraint(name, *args, &block)
165
180
  opts = name.is_a?(Hash) ? name : {:name=>name}
166
181
  constraints << opts.merge(:type=>:check, :check=>block || args)
@@ -262,6 +262,10 @@ module Sequel
262
262
  # # SELECT * FROM items WHERE foo
263
263
  # # WITH CHECK OPTION
264
264
  #
265
+ # DB.create_view(:bar_items, DB[:items].select(:foo), columns: [:bar])
266
+ # # CREATE VIEW bar_items (bar) AS
267
+ # # SELECT foo FROM items
268
+ #
265
269
  # Options:
266
270
  # :columns :: The column names to use for the view. If not given,
267
271
  # automatically determined based on the input dataset.
@@ -580,14 +584,14 @@ module Sequel
580
584
  sql << ' NULL'
581
585
  end
582
586
  end
583
-
587
+
584
588
  # Add primary key SQL fragment to column creation SQL.
585
589
  def column_definition_primary_key_sql(sql, column)
586
590
  if column[:primary_key]
587
591
  if name = column[:primary_key_constraint_name]
588
592
  sql << " CONSTRAINT #{quote_identifier(name)}"
589
593
  end
590
- sql << ' PRIMARY KEY'
594
+ sql << " " << primary_key_constraint_sql_fragment(column)
591
595
  constraint_deferrable_sql_append(sql, column[:primary_key_deferrable])
592
596
  end
593
597
  end
@@ -608,7 +612,7 @@ module Sequel
608
612
  if name = column[:unique_constraint_name]
609
613
  sql << " CONSTRAINT #{quote_identifier(name)}"
610
614
  end
611
- sql << ' UNIQUE'
615
+ sql << ' ' << unique_constraint_sql_fragment(column)
612
616
  constraint_deferrable_sql_append(sql, column[:unique_deferrable])
613
617
  end
614
618
  end
@@ -656,11 +660,11 @@ module Sequel
656
660
  check = "(#{check})" unless check[0..0] == '(' && check[-1..-1] == ')'
657
661
  sql << "CHECK #{check}"
658
662
  when :primary_key
659
- sql << "PRIMARY KEY #{literal(constraint[:columns])}"
663
+ sql << "#{primary_key_constraint_sql_fragment(constraint)} #{literal(constraint[:columns])}"
660
664
  when :foreign_key
661
665
  sql << column_references_table_constraint_sql(constraint.merge(:deferrable=>nil))
662
666
  when :unique
663
- sql << "UNIQUE #{literal(constraint[:columns])}"
667
+ sql << "#{unique_constraint_sql_fragment(constraint)} #{literal(constraint[:columns])}"
664
668
  else
665
669
  raise Error, "Invalid constraint type #{constraint[:type]}, should be :check, :primary_key, :foreign_key, or :unique"
666
670
  end
@@ -892,6 +896,11 @@ module Sequel
892
896
  on_delete_clause(action)
893
897
  end
894
898
 
899
+ # Add fragment for primary key specification, separated for easier overridding.
900
+ def primary_key_constraint_sql_fragment(_)
901
+ 'PRIMARY KEY'
902
+ end
903
+
895
904
  # Proxy the quote_schema_table method to the dataset
896
905
  def quote_schema_table(table)
897
906
  schema_utility_dataset.quote_schema_table(table)
@@ -1047,6 +1056,11 @@ module Sequel
1047
1056
  "#{type}#{literal(Array(elements)) if elements}#{' UNSIGNED' if column[:unsigned]}"
1048
1057
  end
1049
1058
 
1059
+ # Add fragment for unique specification, separated for easier overridding.
1060
+ def unique_constraint_sql_fragment(_)
1061
+ 'UNIQUE'
1062
+ end
1063
+
1050
1064
  # Whether clob should be used for String text: true columns.
1051
1065
  def uses_clob_for_text?
1052
1066
  false