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