mongo_record 0.4.2

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,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