-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
base.rb
2264 lines (2042 loc) · 84.4 KB
/
base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# frozen-string-literal: true
module Sequel
class Model
extend Enumerable
extend Inflections
# Class methods for Sequel::Model that implement basic model functionality.
#
# * All of the following methods have class methods created that send the method
# to the model's dataset: all, as_hash, avg, count, cross_join, distinct, each,
# each_server, empty?, except, exclude, exclude_having, fetch_rows,
# filter, first, first!, for_update, from, from_self, full_join, full_outer_join,
# get, graph, grep, group, group_and_count, group_append, group_by, having, import,
# inner_join, insert, intersect, invert, join, join_table, last, left_join,
# left_outer_join, limit, lock_style, map, max, min, multi_insert, naked, natural_full_join,
# natural_join, natural_left_join, natural_right_join, offset, order, order_append, order_by,
# order_more, order_prepend, paged_each, qualify, reverse, reverse_order, right_join,
# right_outer_join, select, select_all, select_append, select_group, select_hash,
# select_hash_groups, select_map, select_more, select_order_map, server,
# single_record, single_record!, single_value, single_value!, sum, to_hash, to_hash_groups,
# truncate, unfiltered, ungraphed, ungrouped, union, unlimited, unordered, where, where_all,
# where_each, where_single_value, with, with_recursive, with_sql
module ClassMethods
# Whether to cache the anonymous models created by Sequel::Model(), true by default. This is
# required for reloading them correctly (avoiding the superclass mismatch).
attr_accessor :cache_anonymous_models
# Array of modules that extend this model's dataset. Stored
# so that if the model's dataset is changed, it will be extended
# with all of these modules.
attr_reader :dataset_method_modules
# The Module subclass to use for dataset_module blocks.
attr_reader :dataset_module_class
# The default options to use for Model#set_fields. These are merged with
# the options given to set_fields.
attr_accessor :default_set_fields_options
# SQL string fragment used for faster DELETE statement creation when deleting/destroying
# model instances, or nil if the optimization should not be used. For internal use only.
attr_reader :fast_instance_delete_sql
# SQL string fragment used for faster lookups by primary key, or nil if the optimization
# should not be used. For internal use only.
attr_reader :fast_pk_lookup_sql
# The dataset that instance datasets (#this) are based on. Generally a naked version of
# the model's dataset limited to one row. For internal use only.
attr_reader :instance_dataset
# Array of plugin modules loaded by this class
#
# Sequel::Model.plugins
# # => [Sequel::Model, Sequel::Model::Associations]
attr_reader :plugins
# The primary key for the class. Sequel can determine this automatically for
# many databases, but not all, so you may need to set it manually. If not
# determined automatically, the default is :id.
attr_reader :primary_key
# Whether to raise an error instead of returning nil on a failure
# to save/create/save_changes/update/destroy due to a validation failure or
# a before_* hook returning false (default: true).
attr_accessor :raise_on_save_failure
# Whether to raise an error when unable to typecast data for a column
# (default: false). This should be set to true if you want to have model
# setter methods raise errors if the argument cannot be typecast properly.
attr_accessor :raise_on_typecast_failure
# Whether to raise an error if an UPDATE or DELETE query related to
# a model instance does not modify exactly 1 row. If set to false,
# Sequel will not check the number of rows modified (default: true).
attr_accessor :require_modification
# If true (the default), requires that all models have valid tables,
# raising exceptions if creating a model without a valid table backing it.
# Setting this to false will allow the creation of model classes where the
# underlying table doesn't exist.
attr_accessor :require_valid_table
# Should be the literal primary key column name if this Model's table has a simple primary key, or
# nil if the model has a compound primary key or no primary key.
attr_reader :simple_pk
# Should be the literal table name if this Model's dataset is a simple table (no select, order, join, etc.),
# or nil otherwise. This and simple_pk are used for an optimization in Model.[].
attr_reader :simple_table
# Whether mass assigning via .create/.new/#set/#update should raise an error
# if an invalid key is used. A key is invalid if no setter method exists
# for that key or the access to the setter method is restricted (e.g. due to it
# being a primary key field). If set to false, silently skip
# any key where the setter method doesn't exist or access to it is restricted.
attr_accessor :strict_param_setting
# Whether to typecast the empty string ('') to nil for columns that
# are not string or blob. In most cases the empty string would be the
# way to specify a NULL SQL value in string form (nil.to_s == ''),
# and an empty string would not usually be typecast correctly for other
# types, so the default is true.
attr_accessor :typecast_empty_string_to_nil
# Whether to typecast attribute values on assignment (default: true).
# If set to false, no typecasting is done, so it will be left up to the
# database to typecast the value correctly.
attr_accessor :typecast_on_assignment
# Whether to use a transaction by default when saving/deleting records (default: true).
# If you are sending database queries in before_* or after_* hooks, you shouldn't change
# the default setting without a good reason.
attr_accessor :use_transactions
# Define a Model method on the given module that calls the Model
# method on the receiver. This is how the Sequel::Model() method is
# defined, and allows you to define Model() methods on other modules,
# making it easier to have custom model settings for all models under
# a namespace. Example:
#
# module Foo
# Model = Class.new(Sequel::Model)
# Model.def_Model(self)
# DB = Model.db = Sequel.connect(ENV['FOO_DATABASE_URL'])
# Model.plugin :prepared_statements
#
# class Bar < Model
# # Uses Foo::DB[:bars]
# end
#
# class Baz < Model(:my_baz)
# # Uses Foo::DB[:my_baz]
# end
# end
def def_Model(mod)
model = self
mod.define_singleton_method(:Model) do |source|
model.Model(source)
end
end
# Lets you create a Model subclass with its dataset already set.
# +source+ should be an instance of one of the following classes:
#
# Database :: Sets the database for this model to +source+.
# Generally only useful when subclassing directly
# from the returned class, where the name of the
# subclass sets the table name (which is combined
# with the +Database+ in +source+ to create the
# dataset to use)
# Dataset :: Sets the dataset for this model to +source+.
# other :: Sets the table name for this model to +source+. The
# class will use the default database for model
# classes in order to create the dataset.
#
# The purpose of this method is to set the dataset/database automatically
# for a model class, if the table name doesn't match the default table
# name that Sequel would use.
#
# When creating subclasses of Sequel::Model itself, this method is usually
# called on Sequel itself, using <tt>Sequel::Model(:something)</tt>.
#
# # Using a symbol
# class Comment < Sequel::Model(:something)
# table_name # => :something
# end
#
# # Using a dataset
# class Comment < Sequel::Model(DB1[:something])
# dataset # => DB1[:something]
# end
#
# # Using a database
# class Comment < Sequel::Model(DB1)
# dataset # => DB1[:comments]
# end
def Model(source)
if cache_anonymous_models
cache = Sequel.synchronize{@Model_cache ||= {}}
if klass = Sequel.synchronize{cache[source]}
return klass
end
end
klass = Class.new(self)
if source.is_a?(::Sequel::Database)
klass.db = source
else
klass.set_dataset(source)
end
if cache_anonymous_models
Sequel.synchronize{cache[source] = klass}
end
klass
end
# Returns the first record from the database matching the conditions.
# If a hash is given, it is used as the conditions. If another
# object is given, it finds the first record whose primary key(s) match
# the given argument(s). If no object is returned by the dataset, returns nil.
#
# Artist[1] # SELECT * FROM artists WHERE id = 1
# # => #<Artist {:id=>1, ...}>
#
# Artist[name: 'Bob'] # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1
# # => #<Artist {:name=>'Bob', ...}>
def [](*args)
args = args.first if args.size <= 1
args.is_a?(Hash) ? first(args) : (primary_key_lookup(args) unless args.nil?)
end
# Initializes a model instance as an existing record. This constructor is
# used by Sequel to initialize model instances when fetching records.
# Requires that values be a hash where all keys are symbols. It
# probably should not be used by external code.
def call(values)
o = allocate
o.instance_variable_set(:@values, values)
o
end
# Clear the setter_methods cache
def clear_setter_methods_cache
@setter_methods = nil unless frozen?
end
# Returns the columns in the result set in their original order.
# Generally, this will use the columns determined via the database
# schema, but in certain cases (e.g. models that are based on a joined
# dataset) it will use <tt>Dataset#columns</tt> to find the columns.
#
# Artist.columns
# # => [:id, :name]
def columns
return @columns if @columns
return nil if frozen?
set_columns(dataset.naked.columns)
end
# Creates instance using new with the given values and block, and saves it.
#
# Artist.create(name: 'Bob')
# # INSERT INTO artists (name) VALUES ('Bob')
#
# Artist.create do |a|
# a.name = 'Jim'
# end # INSERT INTO artists (name) VALUES ('Jim')
def create(values = OPTS, &block)
new(values, &block).save
end
# Returns the dataset associated with the Model class. Raises
# an +Error+ if there is no associated dataset for this class.
# In most cases, you don't need to call this directly, as Model
# proxies many dataset methods to the underlying dataset.
#
# Artist.dataset.all # SELECT * FROM artists
def dataset
@dataset || raise(Error, "No dataset associated with #{self}")
end
# Alias of set_dataset
def dataset=(ds)
set_dataset(ds)
end
# Extend the dataset with a module, similar to adding
# a plugin with the methods defined in DatasetMethods.
# This is the recommended way to add methods to model datasets.
#
# If given an argument, it should be a module, and is used to extend
# the underlying dataset. Otherwise an anonymous module is created, and
# if a block is given, it is module_evaled, allowing you do define
# dataset methods directly using the standard ruby def syntax.
# Returns the module given or the anonymous module created.
#
# # Usage with existing module
# Album.dataset_module Sequel::ColumnsIntrospection
#
# # Usage with anonymous module
# Album.dataset_module do
# def foo
# :bar
# end
# end
# Album.dataset.foo
# # => :bar
# Album.foo
# # => :bar
#
# Any anonymous modules created are actually instances of Sequel::Model::DatasetModule
# (a Module subclass), which allows you to call the subset method on them, which
# defines a dataset method that adds a filter. There are also a number of other
# methods with the same names as the dataset methods, which can use to define
# named dataset methods:
#
# Album.dataset_module do
# where(:released, Sequel[:release_date] <= Sequel::CURRENT_DATE)
# order :by_release_date, :release_date
# select :for_select_options, :id, :name, :release_date
# end
# Album.released.sql
# # => "SELECT * FROM artists WHERE (release_date <= CURRENT_DATE)"
# Album.by_release_date.sql
# # => "SELECT * FROM artists ORDER BY release_date"
# Album.for_select_options.sql
# # => "SELECT id, name, release_date FROM artists"
# Album.released.by_release_date.for_select_options.sql
# # => "SELECT id, name, release_date FROM artists WHERE (release_date <= CURRENT_DATE) ORDER BY release_date"
#
# The following methods are supported: distinct, eager, exclude, exclude_having, grep, group, group_and_count,
# group_append, having, limit, offset, order, order_append, order_prepend, select, select_all,
# select_append, select_group, where, and server.
#
# The advantage of using these DatasetModule methods to define your dataset
# methods is that they can take advantage of dataset caching to improve
# performance.
#
# Any public methods in the dataset module will have class methods created that
# call the method on the dataset, assuming that the class method is not already
# defined.
def dataset_module(mod = nil, &block)
if mod
raise Error, "can't provide both argument and block to Model.dataset_module" if block
dataset_extend(mod)
mod
else
@dataset_module ||= dataset_module_class.new(self)
@dataset_module.module_eval(&block) if block
dataset_extend(@dataset_module)
@dataset_module
end
end
# Returns the database associated with the Model class.
# If this model doesn't have a database associated with it,
# assumes the superclass's database, or the first object in
# Sequel::DATABASES. If no Sequel::Database object has
# been created, raises an error.
#
# Artist.db.transaction do # BEGIN
# Artist.create(name: 'Bob')
# # INSERT INTO artists (name) VALUES ('Bob')
# end # COMMIT
def db
return @db if @db
@db = self == Model ? Sequel.synchronize{DATABASES.first} : superclass.db
raise(Error, "No database associated with #{self}: have you called Sequel.connect or #{self}.db= ?") unless @db
@db
end
# Sets the database associated with the Model class.
# Should only be used if the Model class currently does not
# have a dataset defined.
#
# This can be used directly on Sequel::Model to set the default database to be used
# by subclasses, or to override the database used for specific models:
#
# Sequel::Model.db = DB1
# Artist = Class.new(Sequel::Model)
# Artist.db = DB2
#
# Note that you should not use this to change the model's database
# at runtime. If you have that need, you should look into Sequel's
# sharding support, or consider using separate model classes per Database.
def db=(db)
raise Error, "Cannot use Sequel::Model.db= on model with existing dataset. Use Sequel::Model.dataset= instead." if @dataset
@db = db
end
# Returns the cached schema information if available or gets it
# from the database. This is a hash where keys are column symbols
# and values are hashes of information related to the column. See
# <tt>Database#schema</tt>.
#
# Artist.db_schema
# # {:id=>{:type=>:integer, :primary_key=>true, ...},
# # :name=>{:type=>:string, :primary_key=>false, ...}}
def db_schema
return @db_schema if @db_schema
return nil if frozen?
@db_schema = get_db_schema
end
# Create a column alias, where the column methods have one name, but the underlying storage uses a
# different name.
def def_column_alias(meth, column)
clear_setter_methods_cache
overridable_methods_module.module_eval do
define_method(meth){self[column]}
define_method("#{meth}="){|v| self[column] = v}
end
end
# Finds a single record according to the supplied filter.
# You are encouraged to use Model.[] or Model.first instead of this method.
#
# Artist.find(name: 'Bob')
# # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1
#
# Artist.find{name > 'M'}
# # SELECT * FROM artists WHERE (name > 'M') LIMIT 1
def find(*args, &block)
first(*args, &block)
end
# Like +find+ but invokes create with given conditions when record does not
# exist. Unlike +find+ in that the block used in this method is not passed
# to +find+, but instead is passed to +create+ only if +find+ does not
# return an object.
#
# Artist.find_or_create(name: 'Bob')
# # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1
# # INSERT INTO artists (name) VALUES ('Bob')
#
# Artist.find_or_create(name: 'Jim'){|a| a.hometown = 'Sactown'}
# # SELECT * FROM artists WHERE (name = 'Jim') LIMIT 1
# # INSERT INTO artists (name, hometown) VALUES ('Jim', 'Sactown')
def find_or_create(cond, &block)
find(cond) || create(cond, &block)
end
# Freeze a model class, disallowing any further changes to it.
def freeze
return self if frozen?
dataset_module.freeze
overridable_methods_module.freeze
if @dataset
db_schema.freeze.each_value(&:freeze)
columns.freeze
setter_methods.freeze
else
@setter_methods = [].freeze
end
@dataset_method_modules.freeze
@default_set_fields_options.freeze
@plugins.freeze
super
end
# Whether the model has a dataset. True for most model classes,
# but can be false if the model class is an abstract model class
# designed for subclassing, such as Sequel::Model itself.
def has_dataset?
!@dataset.nil?
end
# Clear the setter_methods cache when a module is included, as it
# may contain setter methods.
def include(*mods)
clear_setter_methods_cache
super
end
# Returns the implicit table name for the model class, which is the demodulized,
# underscored, pluralized name of the class.
#
# Artist.implicit_table_name # => :artists
# Foo::ArtistAlias.implicit_table_name # => :artist_aliases
def implicit_table_name
pluralize(underscore(demodulize(name))).to_sym
end
# Calls #call with the values hash.
def load(values)
call(values)
end
# Mark the model as not having a primary key. Not having a primary key
# can cause issues, among which is that you won't be able to update records.
#
# Artist.primary_key # => :id
# Artist.no_primary_key
# Artist.primary_key # => nil
def no_primary_key
clear_setter_methods_cache
self.simple_pk = @primary_key = nil
end
# Loads a plugin for use with the model class, passing optional arguments
# to the plugin. If the plugin is a module, load it directly. Otherwise,
# require the plugin from sequel/plugins/#{plugin} and then attempt to load
# the module using a the camelized plugin name under Sequel::Plugins.
def plugin(plugin, *args, &block)
m = plugin.is_a?(Module) ? plugin : plugin_module(plugin)
if !m.respond_to?(:apply) && !m.respond_to?(:configure) && (!args.empty? || block)
Deprecation.deprecate("Plugin #{plugin} accepts no arguments or block, and passing arguments/block to it", "Remove arguments and block when loading the plugin")
end
unless @plugins.include?(m)
@plugins << m
m.apply(self, *args, &block) if m.respond_to?(:apply)
extend(m::ClassMethods) if m.const_defined?(:ClassMethods, false)
include(m::InstanceMethods) if m.const_defined?(:InstanceMethods, false)
if m.const_defined?(:DatasetMethods, false)
dataset_extend(m::DatasetMethods, :create_class_methods=>false)
end
end
m.configure(self, *args, &block) if m.respond_to?(:configure)
end
# :nocov:
ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
# :nocov:
# Returns primary key attribute hash. If using a composite primary key
# value such be an array with values for each primary key in the correct
# order. For a standard primary key, value should be an object with a
# compatible type for the key. If the model does not have a primary key,
# raises an +Error+.
#
# Artist.primary_key_hash(1) # => {:id=>1}
# Artist.primary_key_hash([1, 2]) # => {:id1=>1, :id2=>2}
def primary_key_hash(value)
case key = @primary_key
when Symbol
{key => value}
when Array
hash = {}
key.zip(Array(value)){|k,v| hash[k] = v}
hash
else
raise(Error, "#{self} does not have a primary key")
end
end
# Return a hash where the keys are qualified column references. Uses the given
# qualifier if provided, or the table_name otherwise. This is useful if you
# plan to join other tables to this table and you want the column references
# to be qualified.
#
# Artist.where(Artist.qualified_primary_key_hash(1))
# # SELECT * FROM artists WHERE (artists.id = 1)
def qualified_primary_key_hash(value, qualifier=table_name)
case key = @primary_key
when Symbol
{SQL::QualifiedIdentifier.new(qualifier, key) => value}
when Array
hash = {}
key.zip(Array(value)){|k,v| hash[SQL::QualifiedIdentifier.new(qualifier, k)] = v}
hash
else
raise(Error, "#{self} does not have a primary key")
end
end
# Restrict the setting of the primary key(s) when using mass assignment (e.g. +set+). Because
# this is the default, this only make sense to use in a subclass where the
# parent class has used +unrestrict_primary_key+.
def restrict_primary_key
clear_setter_methods_cache
@restrict_primary_key = true
end
# Whether or not setting the primary key(s) when using mass assignment (e.g. +set+) is
# restricted, true by default.
def restrict_primary_key?
@restrict_primary_key
end
# Sets the dataset associated with the Model class. +ds+ can be a +Symbol+,
# +LiteralString+, <tt>SQL::Identifier</tt>, <tt>SQL::QualifiedIdentifier</tt>,
# <tt>SQL::AliasedExpression</tt>
# (all specifying a table name in the current database), or a +Dataset+.
# If a dataset is used, the model's database is changed to the database of the given
# dataset. If a dataset is not used, a dataset is created from the current
# database with the table name given. Other arguments raise an +Error+.
# Returns self.
#
# It also attempts to determine the database schema for the model,
# based on the given dataset.
#
# Note that you should not use this to change the model's dataset
# at runtime. If you have that need, you should look into Sequel's
# sharding support, or creating a separate Model class per dataset
#
# You should avoid calling this method directly if possible. Instead you should
# set the table name or dataset when creating the model class:
#
# # table name
# class Artist < Sequel::Model(:tbl_artists)
# end
#
# # dataset
# class Artist < Sequel::Model(DB[:tbl_artists])
# end
def set_dataset(ds, opts=OPTS)
inherited = opts[:inherited]
@dataset = convert_input_dataset(ds)
@require_modification = @dataset.provides_accurate_rows_matched? if require_modification.nil?
if inherited
self.simple_table = superclass.simple_table
@columns = superclass.instance_variable_get(:@columns)
@db_schema = superclass.instance_variable_get(:@db_schema)
else
@dataset = @dataset.with_extend(*@dataset_method_modules.reverse)
@db_schema = get_db_schema
end
reset_instance_dataset
self
end
# Sets the primary key for this model. You can use either a regular
# or a composite primary key. To not use a primary key, set to nil
# or use +no_primary_key+. On most adapters, Sequel can automatically
# determine the primary key to use, so this method is not needed often.
#
# class Person < Sequel::Model
# # regular key
# set_primary_key :person_id
# end
#
# class Tagging < Sequel::Model
# # composite key
# set_primary_key [:taggable_id, :tag_id]
# end
def set_primary_key(key)
clear_setter_methods_cache
if key.is_a?(Array)
if key.length < 2
key = key.first
else
key = key.dup.freeze
end
end
self.simple_pk = if key && !key.is_a?(Array)
(@dataset || db).literal(key).freeze
end
@primary_key = key
end
# Cache of setter methods to allow by default, in order to speed up mass assignment.
def setter_methods
@setter_methods || (@setter_methods = get_setter_methods)
end
# Returns name of primary table for the dataset. If the table for the dataset
# is aliased, returns the aliased name.
#
# Artist.table_name # => :artists
# Sequel::Model(:foo).table_name # => :foo
# Sequel::Model(Sequel[:foo].as(:bar)).table_name # => :bar
def table_name
dataset.first_source_alias
end
# Allow the setting of the primary key(s) when using the mass assignment methods.
# Using this method can open up security issues, be very careful before using it.
#
# Artist.set(id: 1) # Error
# Artist.unrestrict_primary_key
# Artist.set(id: 1) # No Error
def unrestrict_primary_key
clear_setter_methods_cache
@restrict_primary_key = false
end
# Return the model instance with the primary key, or nil if there is no matching record.
def with_pk(pk)
primary_key_lookup(pk)
end
# Return the model instance with the primary key, or raise NoMatchingRow if there is no matching record.
def with_pk!(pk)
with_pk(pk) || raise(NoMatchingRow.new(dataset))
end
# Add model methods that call dataset methods
Plugins.def_dataset_methods(self, (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS + [:each_server]) - [:<<, :or, :[], :columns, :columns!, :delete, :update, :set_graph_aliases, :add_graph_aliases])
private
# Yield to the passed block and if do_raise is false, swallow all errors other than DatabaseConnectionErrors.
def check_non_connection_error(do_raise=require_valid_table)
begin
db.transaction(:savepoint=>:only){yield}
rescue Sequel::DatabaseConnectionError
raise
rescue Sequel::Error
raise if do_raise
end
end
# Convert the given object to a Dataset that should be used as
# this model's dataset.
def convert_input_dataset(ds)
case ds
when Symbol, SQL::Identifier, SQL::QualifiedIdentifier, SQL::AliasedExpression, LiteralString
self.simple_table = db.literal(ds).freeze
ds = db.from(ds)
when Dataset
ds = ds.from_self(:alias=>ds.first_source) if ds.joined_dataset?
self.simple_table = if ds.send(:simple_select_all?)
ds.literal(ds.first_source_table).freeze
end
@db = ds.db
else
raise(Error, "Model.set_dataset takes one of the following classes as an argument: Symbol, LiteralString, SQL::Identifier, SQL::QualifiedIdentifier, SQL::AliasedExpression, Dataset")
end
set_dataset_row_proc(ds.clone(:model=>self))
end
# Add the module to the class's dataset_method_modules. Extend the dataset with the
# module if the model has a dataset. Add dataset methods to the class for all
# public dataset methods.
def dataset_extend(mod, opts=OPTS)
@dataset = @dataset.with_extend(mod) if @dataset
reset_instance_dataset
dataset_method_modules << mod
unless opts[:create_class_methods] == false
mod.public_instance_methods.each{|meth| def_model_dataset_method(meth)}
end
end
# Create a column accessor for a column with a method name that is hard to use in ruby code.
def def_bad_column_accessor(column)
im = instance_methods
overridable_methods_module.module_eval do
meth = :"#{column}="
unless im.include?(column)
define_method(column){self[column]}
alias_method(column, column)
end
unless im.include?(meth)
define_method(meth){|v| self[column] = v}
alias_method(meth, meth)
end
end
end
# Create the column accessors. For columns that can be used as method names directly in ruby code,
# use a string to define the method for speed. For other columns names, use a block.
def def_column_accessor(*columns)
clear_setter_methods_cache
columns, bad_columns = columns.partition{|x| /\A[A-Za-z_][A-Za-z0-9_]*\z/.match(x.to_s)}
bad_columns.each{|x| def_bad_column_accessor(x)}
im = instance_methods
columns.each do |column|
meth = :"#{column}="
unless im.include?(column)
overridable_methods_module.module_eval("def #{column}; self[:#{column}] end", __FILE__, __LINE__)
overridable_methods_module.send(:alias_method, column, column)
end
unless im.include?(meth)
overridable_methods_module.module_eval("def #{meth}(v); self[:#{column}] = v end", __FILE__, __LINE__)
overridable_methods_module.send(:alias_method, meth, meth)
end
end
end
# Define a model method that calls the dataset method with the same name,
# only used for methods with names that can't be represented directly in
# ruby code.
def def_model_dataset_method(meth)
return if respond_to?(meth, true)
if meth.to_s =~ /\A[A-Za-z_][A-Za-z0-9_]*\z/
instance_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__)
else
define_singleton_method(meth){|*args, &block| dataset.public_send(meth, *args, &block)}
end
singleton_class.send(:alias_method, meth, meth)
# :nocov:
singleton_class.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
# :nocov:
end
# Get the schema from the database, fall back on checking the columns
# via the database if that will return inaccurate results or if
# it raises an error.
def get_db_schema(reload = reload_db_schema?)
set_columns(nil)
return nil unless @dataset
schema_hash = {}
ds_opts = dataset.opts
get_columns = proc{check_non_connection_error{columns} || []}
schema_array = check_non_connection_error(false){db.schema(dataset, :reload=>reload)} if db.supports_schema_parsing?
if schema_array
schema_array.each{|k,v| schema_hash[k] = v}
# Set the primary key(s) based on the schema information,
# if the schema information includes primary key information
if schema_array.all?{|k,v| v.has_key?(:primary_key)}
pks = schema_array.map{|k,v| k if v[:primary_key]}.compact
pks.length > 0 ? set_primary_key(pks) : no_primary_key
end
if (select = ds_opts[:select]) && !(select.length == 1 && select.first.is_a?(SQL::ColumnAll))
# We don't remove the columns from the schema_hash,
# as it's possible they will be used for typecasting
# even if they are not selected.
cols = get_columns.call
cols.each{|c| schema_hash[c] ||= {}}
def_column_accessor(*schema_hash.keys)
else
# Dataset is for a single table with all columns,
# so set the columns based on the order they were
# returned by the schema.
cols = schema_array.map{|k,v| k}
set_columns(cols)
# Also set the columns for the dataset, so the dataset
# doesn't have to do a query to get them.
dataset.send(:columns=, cols)
end
else
# If the dataset uses multiple tables or custom sql or getting
# the schema raised an error, just get the columns and
# create an empty schema hash for it.
get_columns.call.each{|c| schema_hash[c] = {}}
end
schema_hash
end
# Uncached version of setter_methods, to be overridden by plugins
# that want to modify the methods used.
def get_setter_methods
meths = instance_methods.map(&:to_s).select{|l| l.end_with?('=')} - RESTRICTED_SETTER_METHODS
meths -= Array(primary_key).map{|x| "#{x}="} if primary_key && restrict_primary_key?
meths
end
# If possible, set the dataset for the model subclass as soon as it
# is created. Also, make sure the inherited class instance variables
# are copied into the subclass.
#
# Sequel queries the database to get schema information as soon as
# a model class is created:
#
# class Artist < Sequel::Model # Causes schema query
# end
def inherited(subclass)
super
ivs = subclass.instance_variables
inherited_instance_variables.each do |iv, dup|
if (sup_class_value = instance_variable_get(iv)) && dup
sup_class_value = case dup
when :dup
sup_class_value.dup
when :hash_dup
h = {}
sup_class_value.each{|k,v| h[k] = v.dup}
h
when Proc
dup.call(sup_class_value)
else
raise Error, "bad inherited instance variable type: #{dup.inspect}"
end
end
subclass.instance_variable_set(iv, sup_class_value)
end
unless ivs.include?(:@dataset)
if @dataset && self != Model
subclass.set_dataset(@dataset.clone, :inherited=>true)
elsif (n = subclass.name) && !n.to_s.empty?
db
subclass.set_dataset(subclass.implicit_table_name)
end
end
end
# A hash of instance variables to automatically set up in subclasses.
# Keys are instance variable symbols, values should be:
# nil :: Assign directly from superclass to subclass (frozen objects)
# :dup :: Dup object when assigning from superclass to subclass (mutable objects)
# :hash_dup :: Assign hash with same keys, but dup all the values
# Proc :: Call with subclass to do the assignment
def inherited_instance_variables
{
:@cache_anonymous_models=>nil,
:@dataset_method_modules=>:dup,
:@dataset_module_class=>nil,
:@db=>nil,
:@default_set_fields_options=>:dup,
:@fast_instance_delete_sql=>nil,
:@fast_pk_lookup_sql=>nil,
:@plugins=>:dup,
:@primary_key=>nil,
:@raise_on_save_failure=>nil,
:@raise_on_typecast_failure=>nil,
:@require_modification=>nil,
:@require_valid_table=>nil,
:@restrict_primary_key=>nil,
:@setter_methods=>nil,
:@simple_pk=>nil,
:@simple_table=>nil,
:@strict_param_setting=>nil,
:@typecast_empty_string_to_nil=>nil,
:@typecast_on_assignment=>nil,
:@use_transactions=>nil
}
end
# For the given opts hash and default name or :class option, add a
# :class_name option unless already present which contains the name
# of the class to use as a string. The purpose is to allow late
# binding to the class later using constantize.
def late_binding_class_option(opts, default)
case opts[:class]
when String, Symbol
# Delete :class to allow late binding
class_name = opts.delete(:class).to_s
if (namespace = opts[:class_namespace]) && !class_name.start_with?('::')
class_name = "::#{namespace}::#{class_name}"
end
opts[:class_name] ||= class_name
when Class
opts[:class_name] ||= opts[:class].name
end
opts[:class_name] ||= '::' + ((name || '').split("::")[0..-2] + [camelize(default)]).join('::')
end
# Clear the setter_methods cache when a setter method is added.
def method_added(meth)
clear_setter_methods_cache if meth.to_s.end_with?('=')
super
end
# Module that the class includes that holds methods the class adds for column accessors and
# associations so that the methods can be overridden with +super+.
def overridable_methods_module
include(@overridable_methods_module = Module.new) unless @overridable_methods_module
@overridable_methods_module
end
# Returns the module for the specified plugin. If the module is not
# defined, the corresponding plugin required.
def plugin_module(plugin)
module_name = plugin.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
unless Sequel::Plugins.const_defined?(module_name, false)
require "sequel/plugins/#{plugin}"
end
Sequel::Plugins.const_get(module_name)
end
# Find the row in the dataset that matches the primary key. Uses
# a static SQL optimization if the table and primary key are simple.
#
# This method should not be called with a nil primary key, in case
# it is overridden by plugins which assume that the passed argument
# is valid.
def primary_key_lookup(pk)
if sql = @fast_pk_lookup_sql
sql = sql.dup
ds = dataset
ds.literal_append(sql, pk)
ds.fetch_rows(sql){|r| return ds.row_proc.call(r)}
nil
else
dataset.first(primary_key_hash(pk))
end
end
# Whether to reload the database schema by default, ignoring any cached value.
def reload_db_schema?
false
end
# Reset the cached fast primary lookup SQL if a simple table and primary key
# are used, or set it to nil if not used.
def reset_fast_pk_lookup_sql
@fast_pk_lookup_sql = if @simple_table && @simple_pk
"SELECT * FROM #{@simple_table} WHERE #{@simple_pk} = ".freeze
end
@fast_instance_delete_sql = if @simple_table && @simple_pk
"DELETE FROM #{@simple_table} WHERE #{@simple_pk} = ".freeze
end
end
# Reset the instance dataset to a modified copy of the current dataset,
# should be used whenever the model's dataset is modified.
def reset_instance_dataset
@instance_dataset = @dataset.limit(1).naked.skip_limit_check if @dataset
end
# Set the columns for this model and create accessor methods for each column.
def set_columns(new_columns)
@columns = new_columns
def_column_accessor(*new_columns) if new_columns
@columns
end
# Set the dataset's row_proc to the current model.
def set_dataset_row_proc(ds)
ds.with_row_proc(self)