mongodb-mongo-activerecord-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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