sequel_model 0.1

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.
@@ -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