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