sequel_model 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,107 @@
1
+ module Sequel
2
+ class Model
3
+ ID_POSTFIX = '_id'.freeze
4
+
5
+ # Creates a 1-1 relationship by defining an association method, e.g.:
6
+ #
7
+ # class Session < Sequel::Model(:sessions)
8
+ # end
9
+ #
10
+ # class Node < Sequel::Model(:nodes)
11
+ # one_to_one :producer, :from => Session
12
+ # # which is equivalent to
13
+ # def producer
14
+ # Session[producer_id] if producer_id
15
+ # end
16
+ # end
17
+ #
18
+ # You can also set the foreign key explicitly by including a :key option:
19
+ #
20
+ # one_to_one :producer, :from => Session, :key => :producer_id
21
+ #
22
+ # The one_to_one macro also creates a setter, which accepts nil, a hash or
23
+ # a model instance, e.g.:
24
+ #
25
+ # p = Producer[1234]
26
+ # node = Node[:path => '/']
27
+ # node.producer = p
28
+ # node.producer_id #=> 1234
29
+ #
30
+ def self.one_to_one(name, opts)
31
+ # deprecation
32
+ if opts[:class]
33
+ warn "The :class option has been deprecated. Please use :from instead."
34
+ opts[:from] = opts[:class]
35
+ end
36
+
37
+ from = opts[:from]
38
+ from || (raise Error, "No association source defined (use :from option)")
39
+ key = opts[:key] || (name.to_s + ID_POSTFIX).to_sym
40
+
41
+ setter_name = "#{name}=".to_sym
42
+
43
+ case from
44
+ when Symbol
45
+ class_def(name) {(k = @values[key]) ? db[from][:id => k] : nil}
46
+ when Sequel::Dataset
47
+ class_def(name) {(k = @values[key]) ? from[:id => k] : nil}
48
+ else
49
+ class_def(name) {(k = @values[key]) ? from[k] : nil}
50
+ end
51
+ class_def(setter_name) do |v|
52
+ case v
53
+ when nil
54
+ set(key => nil)
55
+ when Sequel::Model
56
+ set(key => v.pk)
57
+ when Hash
58
+ set(key => v[:id])
59
+ end
60
+ end
61
+
62
+ # define_method name, &eval(ONE_TO_ONE_PROC % [key, from])
63
+ end
64
+
65
+ # Creates a 1-N relationship by defining an association method, e.g.:
66
+ #
67
+ # class Book < Sequel::Model(:books)
68
+ # end
69
+ #
70
+ # class Author < Sequel::Model(:authors)
71
+ # one_to_many :books, :from => Book
72
+ # # which is equivalent to
73
+ # def books
74
+ # Book.filter(:author_id => id)
75
+ # end
76
+ # end
77
+ #
78
+ # You can also set the foreign key explicitly by including a :key option:
79
+ #
80
+ # one_to_many :books, :from => Book, :key => :author_id
81
+ #
82
+ def self.one_to_many(name, opts)
83
+ # deprecation
84
+ if opts[:class]
85
+ warn "The :class option has been deprecated. Please use :from instead."
86
+ opts[:from] = opts[:class]
87
+ end
88
+ # deprecation
89
+ if opts[:on]
90
+ warn "The :on option has been deprecated. Please use :key instead."
91
+ opts[:key] = opts[:on]
92
+ end
93
+
94
+
95
+ from = opts[:from]
96
+ from || (raise Error, "No association source defined (use :from option)")
97
+ key = opts[:key] || (self.to_s + ID_POSTFIX).to_sym
98
+
99
+ case from
100
+ when Symbol
101
+ class_def(name) {db[from].filter(key => pk)}
102
+ else
103
+ class_def(name) {from.filter(key => pk)}
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,52 @@
1
+ module Sequel
2
+ class Model
3
+ # Defines a table schema (see Schema::Generator for more information).
4
+ #
5
+ # This is only needed if you want to use the create_table or drop_table
6
+ # methods.
7
+ def self.set_schema(name = nil, &block)
8
+ name ? set_dataset(db[name]) : name = table_name
9
+ @schema = Schema::Generator.new(db, &block)
10
+ if @schema.primary_key_name
11
+ set_primary_key @schema.primary_key_name
12
+ end
13
+ end
14
+
15
+ # Returns table schema for direct descendant of Model.
16
+ def self.schema
17
+ @schema || ((superclass != Model) && (superclass.schema))
18
+ end
19
+
20
+ # Returns name of table.
21
+ def self.table_name
22
+ dataset.opts[:from].first
23
+ end
24
+
25
+ # Returns true if table exists, false otherwise.
26
+ def self.table_exists?
27
+ db.table_exists?(table_name)
28
+ end
29
+
30
+ # Creates table.
31
+ def self.create_table
32
+ db.create_table_sql_list(table_name, *schema.create_info).each {|s| db << s}
33
+ end
34
+
35
+ # Drops table.
36
+ def self.drop_table
37
+ db.execute db.drop_table_sql(table_name)
38
+ end
39
+
40
+ # Like create_table but invokes drop_table when table_exists? is true.
41
+ def self.create_table!
42
+ drop_table if table_exists?
43
+ create_table
44
+ end
45
+
46
+ # Deprecated, use create_table! instead.
47
+ def self.recreate_table
48
+ warn "Model.recreate_table is deprecated. Please use Model.create_table! instead."
49
+ create_table!
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,117 @@
1
+ module Sequel
2
+ class Model
3
+ # =Basic Sequel Validations
4
+ #
5
+ # Sequel validations are based on the Validatable gem http://validatable.rubyforge.org/
6
+ #
7
+ # To assign default validations to a sequel model:
8
+ #
9
+ # class MyModel < SequelModel(:items)
10
+ # validates do
11
+ # format_of...
12
+ # presence_of...
13
+ # acceptance_of...
14
+ # confirmation_of...
15
+ # length_of...
16
+ # true_for...
17
+ # numericality_of...
18
+ # format_of...
19
+ # validates_base...
20
+ # validates_each...
21
+ # end
22
+ # end
23
+ #
24
+ # You may also perform the usual 'longhand' way to assign default model validates
25
+ # directly within the model class itself:
26
+ #
27
+ # class MyModel < SequelModel(:items)
28
+ # validates_format_of...
29
+ # validates_presence_of...
30
+ # validates_acceptance_of...
31
+ # validates_confirmation_of...
32
+ # validates_length_of...
33
+ # validates_true_for...
34
+ # validates_numericality_of...
35
+ # validates_format_of...
36
+ # validates_base...
37
+ # validates_each...
38
+ # end
39
+ #
40
+ # Each validation allows for arguments:
41
+ # TODO: fill the argument options in here
42
+ #
43
+ # =Advanced Sequel Validations
44
+ #
45
+ # TODO: verify that advanced validates work as stated (aka write specs)
46
+ # NOTE: experimental
47
+ #
48
+ # To store validates for conditional usage simply specify a name with which to store them
49
+ # class User < Sequel::Model
50
+ #
51
+ # # This set of validates becomes stored as :default and gets injected into the model.
52
+ # validates do
53
+ # # standard validates calls
54
+ # end
55
+ #
56
+ # validates(:registration) do
57
+ # # user registration specific validates
58
+ # end
59
+ #
60
+ # validates(:promotion) do
61
+ # # user promotion validates
62
+ # end
63
+ #
64
+ # end
65
+ #
66
+ # To use the above validates:
67
+ #
68
+ # @user.valid? # Runs the default validations only.
69
+ # @user.valid?(:registration) # Runs both default and registration validations
70
+ # @user.valid?(:promotion) # Runs both default and promotion validations
71
+ #
72
+ # You may determine whether the model has validates via:
73
+ #
74
+ # has_validations? # will return true / false based on existence of validations on the model.
75
+ #
76
+ # You may also retrieve the validations block if needed:
77
+ #
78
+ # validates(:registration) # returns the registration validation block.
79
+ #
80
+ # validates() method parameters:
81
+ # a validations block - runs the validations block on the model & stores as :default
82
+ # a name and a validations block - stores the block under the name
83
+ # a name - returns a stored block of that name or nil
84
+ # nothing - returns true / false based on if validations exist for the model.
85
+ #
86
+ module Validations
87
+ class Generator
88
+ def initialize(model_class ,&block)
89
+ @model_class = model_class
90
+ instance_eval(&block)
91
+ end
92
+
93
+ def method_missing(method, *args)
94
+ method = :"validates_#{method}"
95
+ @model_class.send(method, *args)
96
+ end
97
+ end
98
+ end
99
+
100
+ begin
101
+ require "validatable"
102
+ include ::Validatable
103
+ def self.validates(&block)
104
+ Validations::Generator.new(self, &block)
105
+ end
106
+ # return true if there are validations stored, false otherwise
107
+ def self.has_validations?
108
+ validations.length > 0 ? true : false
109
+ end
110
+ rescue LoadError
111
+ STDERR.puts <<-MESSAGE
112
+ Install the validatable gem in order to use Sequel Model validations
113
+ If you would like model validations to work, install the validatable gem
114
+ MESSAGE
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,150 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "Model attribute setters" do
4
+
5
+ before(:each) do
6
+ MODEL_DB.reset
7
+
8
+ @c = Class.new(Sequel::Model(:items)) do
9
+ def columns
10
+ [:id, :x, :y]
11
+ end
12
+ end
13
+ end
14
+
15
+ it "should mark the column value as changed" do
16
+ o = @c.new
17
+ o.changed_columns.should == []
18
+
19
+ o.x = 2
20
+ o.changed_columns.should == [:x]
21
+
22
+ o.y = 3
23
+ o.changed_columns.should == [:x, :y]
24
+
25
+ o.changed_columns.clear
26
+
27
+ o[:x] = 2
28
+ o.changed_columns.should == [:x]
29
+
30
+ o[:y] = 3
31
+ o.changed_columns.should == [:x, :y]
32
+ end
33
+
34
+ end
35
+
36
+ describe "Model#serialize" do
37
+
38
+ before(:each) do
39
+ MODEL_DB.reset
40
+ end
41
+
42
+ it "should translate values to YAML when creating records" do
43
+ @c = Class.new(Sequel::Model(:items)) do
44
+ no_primary_key
45
+ serialize :abc
46
+ end
47
+
48
+ @c.create(:abc => 1)
49
+ @c.create(:abc => "hello")
50
+
51
+ MODEL_DB.sqls.should == [ \
52
+ "INSERT INTO items (abc) VALUES ('--- 1\n')", \
53
+ "INSERT INTO items (abc) VALUES ('--- hello\n')", \
54
+ ]
55
+ end
56
+
57
+ it "should support calling after the class is defined" do
58
+ @c = Class.new(Sequel::Model(:items)) do
59
+ no_primary_key
60
+ end
61
+
62
+ @c.serialize :def
63
+
64
+ @c.create(:def => 1)
65
+ @c.create(:def => "hello")
66
+
67
+ MODEL_DB.sqls.should == [ \
68
+ "INSERT INTO items (def) VALUES ('--- 1\n')", \
69
+ "INSERT INTO items (def) VALUES ('--- hello\n')", \
70
+ ]
71
+ end
72
+
73
+ it "should support using the Marshal format" do
74
+ @c = Class.new(Sequel::Model(:items)) do
75
+ no_primary_key
76
+ serialize :abc, :format => :marshal
77
+ end
78
+
79
+ @c.create(:abc => 1)
80
+ @c.create(:abc => "hello")
81
+
82
+ MODEL_DB.sqls.should == [ \
83
+ "INSERT INTO items (abc) VALUES ('\004\bi\006')", \
84
+ "INSERT INTO items (abc) VALUES ('\004\b\"\nhello')", \
85
+ ]
86
+ end
87
+
88
+ it "should translate values to and from YAML using accessor methods" do
89
+ @c = Class.new(Sequel::Model(:items)) do
90
+ serialize :abc, :def
91
+ end
92
+
93
+ ds = @c.dataset
94
+ ds.extend(Module.new {
95
+ attr_accessor :raw
96
+
97
+ def fetch_rows(sql, &block)
98
+ block.call(@raw)
99
+ end
100
+
101
+ @@sqls = nil
102
+
103
+ def insert(*args)
104
+ @@sqls = insert_sql(*args)
105
+ end
106
+
107
+ def update(*args)
108
+ @@sqls = update_sql(*args)
109
+ end
110
+
111
+ def sqls
112
+ @@sqls
113
+ end
114
+
115
+ def columns
116
+ [:id, :abc, :def]
117
+ end
118
+ }
119
+ )
120
+
121
+ ds.raw = {:id => 1, :abc => "--- 1\n", :def => "--- hello\n"}
122
+ o = @c.first
123
+ o.id.should == 1
124
+ o.abc.should == 1
125
+ o.def.should == "hello"
126
+
127
+ o.set(:abc => 23)
128
+ ds.sqls.should == "UPDATE items SET abc = '#{23.to_yaml}' WHERE (id = 1)"
129
+
130
+ ds.raw = {:id => 1, :abc => "--- 1\n", :def => "--- hello\n"}
131
+ o = @c.create(:abc => [1, 2, 3])
132
+ ds.sqls.should == "INSERT INTO items (abc) VALUES ('#{[1, 2, 3].to_yaml}')"
133
+ end
134
+
135
+ end
136
+
137
+ describe Sequel::Model, "super_dataset" do
138
+
139
+ before(:each) do
140
+ MODEL_DB.reset
141
+ class SubClass < Sequel::Model(:items) ; end
142
+ end
143
+
144
+ it "should call the superclass's dataset" do
145
+ SubClass.should_receive(:superclass).exactly(3).times.and_return(Sequel::Model(:items))
146
+ Sequel::Model(:items).should_receive(:dataset)
147
+ SubClass.super_dataset
148
+ end
149
+
150
+ end
@@ -0,0 +1,150 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe Sequel::Model, "caching" do
4
+
5
+ before(:each) do
6
+ MODEL_DB.reset
7
+
8
+ @cache_class = Class.new(Hash) do
9
+ attr_accessor :ttl
10
+ def set(k, v, ttl); self[k] = v; @ttl = ttl; end
11
+ def get(k); self[k]; end
12
+ end
13
+ cache = @cache_class.new
14
+ @cache = cache
15
+
16
+ @c = Class.new(Sequel::Model(:items)) do
17
+ set_cache cache
18
+
19
+ def self.columns
20
+ [:name, :id]
21
+ end
22
+ end
23
+
24
+ $cache_dataset_row = {:name => 'sharon', :id => 1}
25
+ @dataset = @c.dataset
26
+ $sqls = []
27
+ @dataset.extend(Module.new {
28
+ def fetch_rows(sql)
29
+ $sqls << sql
30
+ yield $cache_dataset_row
31
+ end
32
+
33
+ def update(values)
34
+ $sqls << update_sql(values)
35
+ $cache_dataset_row.merge!(values)
36
+ end
37
+
38
+ def delete
39
+ $sqls << delete_sql
40
+ end
41
+ })
42
+ end
43
+
44
+ it "should set the model's cache store" do
45
+ @c.cache_store.should be(@cache)
46
+ end
47
+
48
+ it "should have a default ttl of 3600" do
49
+ @c.cache_ttl.should == 3600
50
+ end
51
+
52
+ it "should take a ttl option" do
53
+ @c.set_cache @cache, :ttl => 1234
54
+ @c.cache_ttl.should == 1234
55
+ end
56
+
57
+ it "should offer a set_cache_ttl method for setting the ttl" do
58
+ @c.cache_ttl.should == 3600
59
+ @c.set_cache_ttl 1234
60
+ @c.cache_ttl.should == 1234
61
+ end
62
+
63
+ it "should generate a cache key appropriate to the class" do
64
+ m = @c.new
65
+ m.values[:id] = 1
66
+ m.cache_key.should == "#{m.class}:1"
67
+
68
+ # custom primary key
69
+ @c.set_primary_key :ttt
70
+ m = @c.new
71
+ m.values[:ttt] = 333
72
+ m.cache_key.should == "#{m.class}:333"
73
+
74
+ # composite primary key
75
+ @c.set_primary_key [:a, :b, :c]
76
+ m = @c.new
77
+ m.values[:a] = 123
78
+ m.values[:c] = 456
79
+ m.values[:b] = 789
80
+ m.cache_key.should == "#{m.class}:123,789,456"
81
+ end
82
+
83
+ it "should raise error if attempting to generate cache_key and primary key value is null" do
84
+ m = @c.new
85
+ proc {m.cache_key}.should raise_error(Sequel::Error)
86
+
87
+ m.values[:id] = 1
88
+ proc {m.cache_key}.should_not raise_error(Sequel::Error)
89
+ end
90
+
91
+ it "should set the cache when reading from the database" do
92
+ $sqls.should == []
93
+ @cache.should be_empty
94
+
95
+ m = @c[1]
96
+ $sqls.should == ['SELECT * FROM items WHERE (id = 1) LIMIT 1']
97
+ m.values.should == $cache_dataset_row
98
+ @cache[m.cache_key].should == m
99
+
100
+ # read from cache
101
+ m2 = @c[1]
102
+ $sqls.should == ['SELECT * FROM items WHERE (id = 1) LIMIT 1']
103
+ m2.should == m
104
+ m2.values.should == $cache_dataset_row
105
+ end
106
+
107
+ it "should delete the cache when writing to the database" do
108
+ # fill the cache
109
+ m = @c[1]
110
+ @cache[m.cache_key].should == m
111
+
112
+ m.set(:name => 'tutu')
113
+ @cache.has_key?(m.cache_key).should be_false
114
+ $sqls.last.should == "UPDATE items SET name = 'tutu' WHERE (id = 1)"
115
+
116
+ m = @c[1]
117
+ @cache[m.cache_key].should == m
118
+ m.name = 'hey'
119
+ m.save
120
+ @cache.has_key?(m.cache_key).should be_false
121
+ $sqls.last.should == "UPDATE items SET name = 'hey', id = 1 WHERE (id = 1)"
122
+ end
123
+
124
+ it "should delete the cache when deleting the record" do
125
+ # fill the cache
126
+ m = @c[1]
127
+ @cache[m.cache_key].should == m
128
+
129
+ m.delete
130
+ @cache.has_key?(m.cache_key).should be_false
131
+ $sqls.last.should == "DELETE FROM items WHERE (id = 1)"
132
+ end
133
+
134
+ it "should support #[] as a shortcut to #find with hash" do
135
+ m = @c[:id => 3]
136
+ @cache[m.cache_key].should be_nil
137
+ $sqls.last.should == "SELECT * FROM items WHERE (id = 3) LIMIT 1"
138
+
139
+ m = @c[1]
140
+ @cache[m.cache_key].should == m
141
+ $sqls.should == ["SELECT * FROM items WHERE (id = 3) LIMIT 1", \
142
+ "SELECT * FROM items WHERE (id = 1) LIMIT 1"]
143
+
144
+ @c[:id => 4]
145
+ $sqls.should == ["SELECT * FROM items WHERE (id = 3) LIMIT 1", \
146
+ "SELECT * FROM items WHERE (id = 1) LIMIT 1", \
147
+ "SELECT * FROM items WHERE (id = 4) LIMIT 1"]
148
+ end
149
+
150
+ end