mongo_record 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,64 @@
1
+ # Copyright 2009 10gen, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Mongo stores trees of JSON-like documents. These +to_mongo_value+ methods
16
+ # covert objects to Hash values, which are converted by the Mongo driver
17
+ # to the proper types.
18
+
19
+ class Object
20
+ # Convert an Object to a Mongo value. Used by MongoRecord::Base when saving
21
+ # data to Mongo.
22
+ def to_mongo_value
23
+ self
24
+ end
25
+
26
+ # From Rails
27
+ def instance_values #:nodoc:
28
+ instance_variables.inject({}) do |values, name|
29
+ values[name.to_s[1..-1]] = instance_variable_get(name)
30
+ values
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ class Array
37
+ # Convert an Array to a Mongo value. Used by MongoRecord::Base when saving
38
+ # data to Mongo.
39
+ def to_mongo_value
40
+ self.collect {|v| v.to_mongo_value}
41
+ end
42
+ end
43
+
44
+ class Hash
45
+ # Convert an Hash to a Mongo value. Used by MongoRecord::Base when saving
46
+ # data to Mongo.
47
+ def to_mongo_value
48
+ h = {}
49
+ self.each {|k,v| h[k] = v.to_mongo_value}
50
+ h
51
+ end
52
+
53
+ # Same symbolize_keys method used in Rails
54
+ def symbolize_keys
55
+ inject({}) do |options, (key, value)|
56
+ options[(key.to_sym rescue key) || key] = value
57
+ options
58
+ end
59
+ end
60
+
61
+ def symbolize_keys!
62
+ self.replace(self.symbolize_keys)
63
+ end
64
+ end
@@ -0,0 +1,111 @@
1
+ # Copyright 2009 10gen, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MongoRecord
16
+
17
+ # A destination for Ruby's built-in Logger class. It writes log messages
18
+ # to a Mongo database collection. Each item in the collection consists of
19
+ # two fields (besides the _id): +time+ and +msg+. +time+ is automatically
20
+ # generated when +write+ is called.
21
+ #
22
+ # If we are running outside of the cloud, all log messages are echoed to
23
+ # $stderr.
24
+ #
25
+ # The collection is capped, which means after the limit is reached old
26
+ # records are deleted when new ones are inserted. See the new method and
27
+ # the Mongo documentation for details.
28
+ #
29
+ # Example:
30
+ #
31
+ # logger = Logger.new(MongoRecord::LogDevice('my_log_name'))
32
+ #
33
+ # The database connection defaults to the global $db. You can set the
34
+ # connection using MongoRecord::LogDevice.connection= and read it with
35
+ # MongoRecord::LogDevice.connection.
36
+ #
37
+ # # Set the connection to something besides $db
38
+ # MongoRecord::LogDevice.connection = connect('my-database')
39
+ class LogDevice
40
+
41
+ DEFAULT_CAP_SIZE = (10 * 1024 * 1024)
42
+
43
+ @@connection = nil
44
+
45
+ class << self # Class methods
46
+
47
+ # Return the database connection. The default value is
48
+ # <code>$db</code>.
49
+ def connection
50
+ conn = @@connection || $db
51
+ raise "connection not defined" unless conn
52
+ conn
53
+ end
54
+
55
+ # Set the database connection. If the connection is set to +nil+, then
56
+ # <code>$db</code> will be used.
57
+ def connection=(val)
58
+ @@connection = val
59
+ end
60
+
61
+ end
62
+
63
+ # +name+ is the name of the Mongo database collection that will hold all
64
+ # log messages. +options+ is a hash that may have the following entries:
65
+ #
66
+ # <code>:size</code> - Optional. The max size of the collection, in
67
+ # bytes. If it is nil or negative then +DEFAULT_CAP_SIZE+ is used.
68
+ #
69
+ # <code>:max</code> - Optional. Specifies the maximum number of log
70
+ # records, after which the oldest items are deleted as new ones are
71
+ # inserted.
72
+ #
73
+ # <code>:stderr</code> - Optional. If not +nil+ then all log messages will
74
+ # be copied to $stderr.
75
+ #
76
+ # Note: a non-nil :max requires a :size value. The collection will never
77
+ # grow above :size. If you leave :size nil then it will be
78
+ # +DEFAULT_CAP_SIZE+.
79
+ #
80
+ # Note: once a capped collection has been created, you can't redefine
81
+ # the size or max falues for that collection. To do so, you must drop
82
+ # and recreate (or let a LogDevice object recreate) the collection.
83
+ def initialize(name, options = {})
84
+ @collection_name = name
85
+ options[:capped] = true
86
+ options[:size] ||= DEFAULT_CAP_SIZE
87
+ options[:size] = DEFAULT_CAP_SIZE if options[:size] <= 0
88
+
89
+ # It's OK to call createCollection if the collection already exists.
90
+ # Size and max won't change, though.
91
+ #
92
+ # Note we can't use the name "create_collection" because a DB JSObject
93
+ # does not have normal keys and returns collection objects as the
94
+ # value of all unknown names.
95
+ self.class.connection.create_collection(@collection_name, options)
96
+
97
+ @console = options[:stderr]
98
+ end
99
+
100
+ # Write a log message to the database. We save the message and a timestamp.
101
+ def write(str)
102
+ $stderr.puts str if @console
103
+ self.class.connection.collection(@collection_name).insert({:time => Time.now, :msg => str})
104
+ end
105
+
106
+ # Close the log. This method is a sham. Nothing happens. You may
107
+ # continue to use this LogDevice.
108
+ def close
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,235 @@
1
+ # Copyright 2009 10gen, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MongoRecord
16
+
17
+ module SQL
18
+
19
+ # A simple tokenizer for SQL.
20
+ class Tokenizer
21
+
22
+ attr_reader :sql
23
+
24
+ def initialize(sql)
25
+ @sql = sql
26
+ @length = sql.length
27
+ @pos = 0
28
+ @extra_tokens = []
29
+ end
30
+
31
+ # Push +tok+ onto the stack.
32
+ def add_extra_token(tok)
33
+ @extra_tokens.push(tok)
34
+ end
35
+
36
+ # Skips whitespace, setting @pos to the position of the next
37
+ # non-whitespace character. If there is none, @pos will == @length.
38
+ def skip_whitespace
39
+ while @pos < @length && [" ", "\n", "\r", "\t"].include?(@sql[@pos,1])
40
+ @pos += 1
41
+ end
42
+ end
43
+
44
+ # Return +true+ if there are more non-whitespace characters.
45
+ def more?
46
+ skip_whitespace
47
+ @pos < @length
48
+ end
49
+
50
+ # Return the next string without its surrounding quotes. Assumes we have
51
+ # already seen a quote character.
52
+ def next_string(c)
53
+ q = c
54
+ @pos += 1
55
+ t = ''
56
+ while @pos < @length
57
+ c = @sql[@pos, 1]
58
+ case c
59
+ when q
60
+ if @pos + 1 < @length && @sql[@pos + 1, 1] == q # double quote
61
+ t += q
62
+ @pos += 1
63
+ else
64
+ @pos += 1
65
+ return t
66
+ end
67
+ when '\\'
68
+ @pos += 1
69
+ return t if @pos >= @length
70
+ t << @sql[@pos, 1]
71
+ else
72
+ t << c
73
+ end
74
+ @pos += 1
75
+ end
76
+ raise "unterminated string in SQL: #{@sql}"
77
+ end
78
+
79
+ # Return +true+ if the next character is a legal starting identifier
80
+ # character.
81
+ def identifier_char?(c)
82
+ c =~ /[\.a-zA-Z0-9_]/ ? true : false
83
+ end
84
+
85
+ # Return +true+ if +c+ is a single or double quote character.
86
+ def quote?(c)
87
+ c == '"' || c == "'"
88
+ end
89
+
90
+ # Return the next token, or +nil+ if there are no more.
91
+ def next_token
92
+ return @extra_tokens.pop unless @extra_tokens.empty?
93
+
94
+ skip_whitespace
95
+ c = @sql[@pos, 1]
96
+ return next_string(c) if quote?(c)
97
+
98
+ first_is_identifier_char = identifier_char?(c)
99
+ t = c
100
+ @pos += 1
101
+ while @pos < @length
102
+ c = @sql[@pos, 1]
103
+ break if c == ' '
104
+
105
+ this_is_identifier_char = identifier_char?(c)
106
+ break if first_is_identifier_char != this_is_identifier_char && @length > 0
107
+ break if !this_is_identifier_char && quote?(c)
108
+
109
+ t << c
110
+ @pos += 1
111
+ end
112
+
113
+ case t
114
+ when ''
115
+ nil
116
+ when /^\d+$/
117
+ t.to_i
118
+ else
119
+ t
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ # Only parses simple WHERE clauses right now. The parser returns a query
126
+ # Hash suitable for use by Mongo.
127
+ class Parser
128
+
129
+ # Parse a WHERE clause (without the "WHERE") ane return a query Hash
130
+ # suitable for use by Mongo.
131
+ def self.parse_where(sql, remove_table_names=false)
132
+ Parser.new(Tokenizer.new(sql)).parse_where(remove_table_names)
133
+ end
134
+
135
+ def initialize(tokenizer)
136
+ @tokenizer = tokenizer
137
+ end
138
+
139
+ # Given a regexp string like '%foo%', return a Regexp object. We set
140
+ # Regexp::IGNORECASE so that all regex matches are case-insensitive.
141
+ def regexp_from_string(str)
142
+ if str[0,1] == '%'
143
+ str = str[1..-1]
144
+ else
145
+ str = '^' + str
146
+ end
147
+
148
+ if str[-1,1] == '%'
149
+ str = str[0..-2]
150
+ else
151
+ str = str + '$'
152
+ end
153
+ Regexp.new(str, Regexp::IGNORECASE)
154
+ end
155
+
156
+ # Parse a WHERE clause (without the "WHERE") and return a query Hash
157
+ # suitable for use by Mongo.
158
+ def parse_where(remove_table_names=false)
159
+ filters = {}
160
+ done = false
161
+ while !done && @tokenizer.more?
162
+ name = @tokenizer.next_token
163
+ raise "sql parser can't handle nested stuff yet: #{@tokenizer.sql}" if name == '('
164
+ name.sub!(/.*\./, '') if remove_table_names # Remove "schema.table." from "schema.table.col"
165
+
166
+ op = @tokenizer.next_token
167
+ op += (' ' + @tokenizer.next_token) if op.downcase == 'not'
168
+ op = op.downcase
169
+
170
+ val = @tokenizer.next_token
171
+
172
+ case op
173
+ when "="
174
+ filters[name] = val
175
+ when "<"
176
+ filters[name] = { :$lt => val }
177
+ when "<="
178
+ filters[name] = { :$lte => val }
179
+ when ">"
180
+ filters[name] = { :$gt => val }
181
+ when ">="
182
+ filters[name] = { :$gte => val }
183
+ when "<>", "!="
184
+ filters[name] = { :$ne => val }
185
+ when "like"
186
+ filters[name] = regexp_from_string(val)
187
+ when "in"
188
+ raise "'in' must be followed by a list of values: #{@tokenizer.sql}" unless val == '('
189
+ filters[name] = { :$in => read_array }
190
+ when "between"
191
+ conjunction = @tokenizer.next_token.downcase
192
+ raise "syntax error: expected 'between X and Y', but saw '" + conjunction + "' instead of 'and'" unless conjunction == 'and'
193
+ val2 = @tokenizer.next_token
194
+ val2, val = val, val2 if val > val2 # Make sure val <= val2
195
+ filters[name] = { :$gte => val, :$lte => val2 }
196
+ else
197
+ raise "can't handle sql operator [#{op}] yet: #{@tokenizer.sql}"
198
+ end
199
+
200
+ break unless @tokenizer.more?
201
+
202
+ tok = @tokenizer.next_token.downcase
203
+ case tok
204
+ when 'and'
205
+ next
206
+ when 'or'
207
+ raise "sql parser can't handle ors yet: #{@tokenizer.sql}"
208
+ when 'order', 'group', 'limit'
209
+ @tokenizer.add_extra_token(tok)
210
+ done = true
211
+ else
212
+ raise "can't handle [#{tok}] yet"
213
+ end
214
+ end
215
+ filters
216
+ end
217
+
218
+ private
219
+
220
+ # Read and return an array of values from a clause like "('a', 'b',
221
+ # 'c')". We have already read the first '('.
222
+ def read_array
223
+ vals = []
224
+ while @tokenizer.more?
225
+ vals.push(@tokenizer.next_token)
226
+ sep = @tokenizer.next_token
227
+ return vals if sep == ')'
228
+ raise "missing ',' in 'in' list of values: #{@tokenizer.sql}" unless sep == ','
229
+ end
230
+ raise "missing ')' at end of 'in' list of values: #{@tokenizer.sql}"
231
+ end
232
+ end
233
+
234
+ end
235
+ end
@@ -0,0 +1,109 @@
1
+ # Copyright 2009 10gen, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'mongo_record/base'
16
+
17
+ module MongoRecord
18
+
19
+ # A MongoRecord::Subobject is an MongoRecord::Base subclass that disallows
20
+ # many operations. Subobjects are those that are contained within and
21
+ # saved with some other object.
22
+ #
23
+ # Using MongoRecord::Subobject is completely optional.
24
+ #
25
+ # As an example, say a Student object contains an Address. You might want
26
+ # to make Address a subclass of Subobject so that you don't accidentally
27
+ # try to save an address to a collection by itself.
28
+ class Subobject < Base
29
+
30
+ class << self # Class methods
31
+
32
+ # Subobjects ignore the collection name.
33
+ def collection_name(coll_name)
34
+ end
35
+
36
+ # Disallow find.
37
+ def find(*args)
38
+ complain("found")
39
+ end
40
+
41
+ # Disallow count.
42
+ def count(*args)
43
+ complain("counted")
44
+ end
45
+
46
+ # Disallow delete.
47
+ def delete(id)
48
+ complain("deleted")
49
+ end
50
+ alias_method :remove, :delete
51
+
52
+ # Disallow destroy.
53
+ def destroy(id)
54
+ complain("destroyed")
55
+ end
56
+
57
+ # Disallow destroy_all.
58
+ def destroy_all(conditions = nil)
59
+ complain("destroyed")
60
+ end
61
+
62
+ # Disallow delete_all.
63
+ def delete_all(conditions=nil)
64
+ complain("deleted")
65
+ end
66
+
67
+ # Disallow create.
68
+ def create(values_hash)
69
+ complain("created")
70
+ end
71
+
72
+ private
73
+
74
+ def complain(cant_do_this)
75
+ raise "Subobjects can't be #{cant_do_this} by themselves. Use a subobject query."
76
+ end
77
+
78
+ end # End of class methods
79
+
80
+ public
81
+
82
+ # Subobjects do not have their own ids.
83
+ def id=(val); raise "Subobjects don't have ids"; end
84
+
85
+ # Subobjects do not have their own ids.
86
+ # You'll get a deprecation warning if you call this outside of Rails.
87
+ def id; raise "Subobjects don't have ids"; end
88
+
89
+ # to_param normally returns the id of an object. Since subobjects don't
90
+ # have ids, this is disallowed.
91
+ def to_param; raise "Subobjects don't have ids"; end
92
+
93
+ # Disallow new_record?
94
+ def new_record?; raise "Subobjects don't have ids"; end
95
+
96
+ # Disallow udpate.
97
+ def update
98
+ self.class.complain("updated")
99
+ end
100
+
101
+ # Disallow delete and remove.
102
+ def delete
103
+ self.class.complain("deleted")
104
+ end
105
+ alias_method :remove, :delete
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright 2009 10gen, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Include files for using Mongo, MongoRecord::Base, and a database logger.
16
+
17
+ require 'rubygems'
18
+ require 'mongo'
19
+ require 'mongo_record/base'
20
+ require 'mongo_record/subobject'
21
+ require 'mongo_record/log_device'
@@ -0,0 +1,38 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'mongo_record'
3
+ s.version = '0.4.2'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = 'ActiveRecord-like models for the 10gen Mongo DB'
6
+ s.description = 'MongoRecord is an ActiveRecord-like framework for the 10gen Mongo database. For more information about Mongo, see http://www.mongodb.org.'
7
+
8
+ s.add_dependency('mongo', ['>= 0.15'])
9
+
10
+ s.require_paths = ['lib']
11
+
12
+ s.files = ['examples/tracks.rb', 'lib/mongo_record.rb',
13
+ 'lib/mongo_record/base.rb',
14
+ 'lib/mongo_record/convert.rb',
15
+ 'lib/mongo_record/log_device.rb',
16
+ 'lib/mongo_record/sql.rb',
17
+ 'lib/mongo_record/subobject.rb',
18
+ 'README.rdoc', 'Rakefile',
19
+ 'mongo-activerecord-ruby.gemspec',
20
+ 'LICENSE']
21
+ s.test_files = ['tests/address.rb',
22
+ 'tests/course.rb',
23
+ 'tests/student.rb',
24
+ 'tests/class_in_module.rb',
25
+ 'tests/test_log_device.rb',
26
+ 'tests/test_mongo.rb',
27
+ 'tests/test_sql.rb',
28
+ 'tests/track2.rb',
29
+ 'tests/track3.rb']
30
+
31
+ s.has_rdoc = true
32
+ s.rdoc_options = ['--main', 'README.rdoc', '--inline-source']
33
+ s.extra_rdoc_files = ['README.rdoc']
34
+
35
+ s.authors = ['Jim Menard', 'Mike Dirolf']
36
+ s.email = 'mongodb-user@googlegroups.com'
37
+ s.homepage = 'http://www.mongodb.org'
38
+ end
data/tests/address.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'mongo_record'
2
+
3
+ class Address < MongoRecord::Subobject
4
+
5
+ fields :street, :city, :state, :postal_code
6
+ belongs_to :student
7
+
8
+ def to_s
9
+ "#{street}\n#{city}, #{state} #{postal_code}"
10
+ end
11
+
12
+ end
@@ -0,0 +1,11 @@
1
+ require 'mongo_record'
2
+
3
+ module MyMod
4
+ class A < MongoRecord::Base
5
+ field :something
6
+ end
7
+ end
8
+
9
+ class B < MongoRecord::Base
10
+ has_many :a, :class_name => "MyMod::A"
11
+ end
data/tests/course.rb ADDED
@@ -0,0 +1,10 @@
1
+ class Course < MongoRecord::Base
2
+
3
+ # Declare Mongo collection name and ivars to be saved
4
+ collection_name :courses
5
+ field :name
6
+
7
+ def to_s
8
+ "Course #{name}"
9
+ end
10
+ end
data/tests/student.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'mongo_record'
2
+ require File.join(File.dirname(__FILE__), 'address')
3
+
4
+ class Score < MongoRecord::Base
5
+
6
+ field :grade
7
+ has_one :for_course, :class_name => 'Course' # Mongo will store course db reference, not duplicate object
8
+
9
+ def passed?
10
+ @grade >= 2.0
11
+ end
12
+
13
+ def to_s
14
+ "#@for_course: #@grade"
15
+ end
16
+
17
+ end
18
+
19
+ class Student < MongoRecord::Base
20
+
21
+ collection_name :students
22
+ fields :name, :email, :num_array, :created_at, :created_on, :updated_on
23
+ has_one :address
24
+ has_many :scores, :class_name => "Score"
25
+
26
+ def initialize(row=nil)
27
+ super
28
+ @address ||= Address.new
29
+ end
30
+
31
+ def add_score(course_id, grade)
32
+ @scores << Score.new(:for_course => Course.find(course_id), :grade => grade)
33
+ end
34
+ end