activerecord-cassandra-adapter 0.1.0

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,298 @@
1
+ class SQLParser
2
+ options no_result_var
3
+ rule
4
+ sql : create_statement
5
+ | read_statemant
6
+ | update_statemant
7
+ | delete_statemant
8
+
9
+ create_statement : INSERT INTO id '(' id_list ')' VALUES '(' value_list ')'
10
+ {
11
+ {:command => :insert, :table => val[2], :column_list => val[4], :value_list => val[8]}
12
+ }
13
+
14
+ read_statemant : SELECT select_list FROM id where_clause order_by_clause limit_clause offset_clause
15
+ {
16
+ {:command => :select, :table => val[3], :select_list => val[1], :condition => val[4], :order => val[5], :limit => val[6], :offset => val[7]}
17
+ }
18
+ | SELECT DISTINCT id FROM id where_clause
19
+ {
20
+ {:command => :select, :table => val[4], :select_list => val[2], :distinct => val[2], :condition => val[5]}
21
+ }
22
+ | SELECT count_clause FROM id where_clause order_by_clause limit_clause offset_clause
23
+ {
24
+ {:command => :select, :table => val[3], :count => val[1], :condition => val[4], :order => val[5], :limit => val[6], :offset => val[7]}
25
+ }
26
+
27
+ count_clause : COUNT '(' count_arg ')'
28
+ {
29
+ "count_all"
30
+ }
31
+ | COUNT '(' count_arg ')' AS id
32
+ {
33
+ val[5]
34
+ }
35
+
36
+ count_arg : '*'
37
+ | id
38
+
39
+ select_list : '*'
40
+ {
41
+ []
42
+ }
43
+ | id_list
44
+
45
+ where_clause :
46
+ {
47
+ []
48
+ }
49
+ | WHERE id_search_condition
50
+ {
51
+ val[1]
52
+ }
53
+ | WHERE search_condition
54
+ {
55
+ val[1]
56
+ }
57
+
58
+ id_search_condition : id_predicate
59
+ | '(' id_predicate ')'
60
+ {
61
+ val[1]
62
+ }
63
+
64
+ id_predicate : ID '=' value
65
+ {
66
+ val[2]
67
+ }
68
+ | ID IN '(' value_list ')'
69
+ {
70
+ val[3]
71
+ }
72
+
73
+ search_condition : boolean_primary
74
+ {
75
+ [val[0]].flatten
76
+ }
77
+ | search_condition AND boolean_primary
78
+ {
79
+ (val[0] << val[2]).flatten
80
+ }
81
+
82
+ boolean_primary : predicate
83
+ | '(' search_condition ')'
84
+ {
85
+ val[1]
86
+ }
87
+
88
+ predicate : id op value
89
+ {
90
+ {:name => val[0], :op => val[1], :expr => val[2]}
91
+ }
92
+ | NOT id op value
93
+ {
94
+ {:name => val[1], :op => val[2], :expr => val[3], :not => true}
95
+ }
96
+ | id op '(' value_list ')'
97
+ {
98
+ {:name => val[0], :op => val[1], :expr => val[3]}
99
+ }
100
+ | NOT id op '(' value_list ')'
101
+ {
102
+ {:name => val[1], :op => val[2], :expr => val[4], :not => true}
103
+ }
104
+ | between_predicate
105
+ | not_in_predicate
106
+
107
+ between_predicate : id BETWEEN value AND value
108
+ {
109
+ {:name => val[0], :op => '$bt', :expr => [val[2], val[4]]}
110
+ }
111
+
112
+ not_in_predicate : id NOT IN '(' value_list ')'
113
+ {
114
+ {:name => val[0], :op => '$in', :expr => val[4], :not => true}
115
+ }
116
+
117
+ order_by_clause :
118
+ {
119
+ nil
120
+ }
121
+ | ORDER BY id ordering_spec
122
+ {
123
+ {:name => val[2], :type => val[3]}
124
+ }
125
+
126
+ ordering_spec :
127
+ {
128
+ :asc
129
+ }
130
+ | order_spec
131
+
132
+ limit_clause :
133
+ {
134
+ nil
135
+ }
136
+ | LIMIT NUMBER
137
+ {
138
+ val[1].to_i
139
+ }
140
+ | LIMIT STRING
141
+ {
142
+ val[1]
143
+ }
144
+
145
+ offset_clause :
146
+ {
147
+ nil
148
+ }
149
+ | OFFSET NUMBER
150
+ {
151
+ val[1].to_i
152
+ }
153
+ | OFFSET STRING
154
+ {
155
+ val[1]
156
+ }
157
+
158
+ update_statemant : UPDATE id SET set_clause_list where_clause
159
+ {
160
+ {:command => :update, :table => val[1], :set_clause_list => val[3], :condition => val[4]}
161
+ }
162
+
163
+ set_clause_list : set_clause
164
+ | set_clause_list ',' set_clause
165
+ {
166
+ val[0].merge val[2]
167
+ }
168
+
169
+ set_clause : id '=' value
170
+ {
171
+ {val[0] => val[2]}
172
+ }
173
+
174
+ delete_statemant : DELETE FROM id where_clause
175
+ {
176
+ {:command => :delete, :table => val[2], :condition => val[3]}
177
+ }
178
+
179
+ id : IDENTIFIER
180
+
181
+ id_list : id
182
+ {
183
+ [val[0]]
184
+ }
185
+ | id_list ',' id
186
+ {
187
+ val[0] << val[2]
188
+ }
189
+
190
+ value : STRING
191
+ | NUMBER
192
+ | NULL
193
+
194
+ value_list : value
195
+ {
196
+ [val[0]]
197
+ }
198
+ | value_list ',' value
199
+ {
200
+ val[0] << val[2]
201
+ }
202
+
203
+ op : IN { '$in' }
204
+ | REGEXP { '$regexp' }
205
+ | '<>' { :'!=' }
206
+ | '!=' { :'!=' }
207
+ | '>=' { :'>=' }
208
+ | '<=' { :'<=' }
209
+ | '>' { :'>' }
210
+ | '<' { :'<' }
211
+ | '=' { :'==' }
212
+
213
+ order_spec : ASC { :asc }
214
+ | DESC { :desc }
215
+
216
+ end
217
+
218
+ ---- header
219
+
220
+ require 'strscan'
221
+
222
+ module ActiveCassandra
223
+
224
+ ---- inner
225
+
226
+ KEYWORDS = %w(
227
+ AND
228
+ AS
229
+ ASC
230
+ BETWEEN
231
+ BY
232
+ COUNT
233
+ DELETE
234
+ DESC
235
+ DISTINCT
236
+ FROM
237
+ IN
238
+ INSERT
239
+ INTO
240
+ LIMIT
241
+ NOT
242
+ OFFSET
243
+ ORDER
244
+ REGEXP
245
+ SELECT
246
+ SET
247
+ UPDATE
248
+ VALUES
249
+ WHERE
250
+ )
251
+
252
+ KEYWORD_REGEXP = Regexp.compile("(?:#{KEYWORDS.join '|'})\\b", Regexp::IGNORECASE)
253
+
254
+ def initialize(obj)
255
+ src = obj.is_a?(IO) ? obj.read : obj.to_s
256
+ @ss = StringScanner.new(src)
257
+ end
258
+
259
+ def scan
260
+ piece = nil
261
+
262
+ until @ss.eos?
263
+ if (tok = @ss.scan /\s+/)
264
+ # nothing to do
265
+ elsif (tok = @ss.scan /(?:<>|!=|>=|<=|>|<|=)/)
266
+ yield tok, tok
267
+ elsif (tok = @ss.scan KEYWORD_REGEXP)
268
+ yield tok.upcase.to_sym, tok
269
+ elsif (tok = @ss.scan /NULL\b/i)
270
+ yield :NULL, nil
271
+ elsif (tok = @ss.scan /'(?:[^']|'')*'/) #'
272
+ yield :STRING, tok.slice(1...-1).gsub(/''/, "'")
273
+ elsif (tok = @ss.scan /-?(?:0|[1-9]\d*)(?:\.\d+)/)
274
+ yield :NUMBER, tok.to_f
275
+ elsif (tok = @ss.scan /-?(?:0|[1-9]\d*)/)
276
+ yield :NUMBER, tok.to_i
277
+ elsif (tok = @ss.scan /[,\(\)\*]/)
278
+ yield tok, tok
279
+ elsif (tok = @ss.scan /(?:[a-z_]\w+\.|[a-z]\.)*ID\b/i)
280
+ yield :ID, tok
281
+ elsif (tok = @ss.scan /(?:[a-z_]\w+\.|[a-z]\.)*(?:[a-z_]\w+|[a-z])/i)
282
+ yield :IDENTIFIER, tok
283
+ else
284
+ raise Racc::ParseError, ('parse error on value "%s"' % @ss.rest.inspect)
285
+ end
286
+ end
287
+
288
+ yield false, '$'
289
+ end
290
+ private :scan
291
+
292
+ def parse
293
+ yyparse self, :scan
294
+ end
295
+
296
+ ---- footer
297
+
298
+ end # module ActiveCassandra
@@ -0,0 +1,269 @@
1
+ require 'active_record/base'
2
+ require 'active_record/connection_adapters/abstract_adapter'
3
+ require 'cassandra'
4
+ require 'active_cassandra/cf'
5
+ require 'active_cassandra/sqlparser.tab'
6
+
7
+ module ActiveRecord
8
+ class Base
9
+ def self.cassandra_connection(config)
10
+ config.symbolize_keys!
11
+ host = config[:host] || '127.0.0.1'
12
+ port = config[:port] || 9160
13
+
14
+ unless (keyspace = config[:keyspace] || config[:database])
15
+ raise ArgumentError, "No database file specified. Missing argument: keyspace"
16
+ end
17
+
18
+ thrift_client_options = config.dup
19
+ [:adapter, :host, :port, :keyspace, :database].each {|i| thrift_client_options.delete(i) }
20
+
21
+ client = Cassandra.new(keyspace, "#{host}:#{port}", thrift_client_options)
22
+ ConnectionAdapters::CassandraAdapter.new(client, logger, config)
23
+ end
24
+ end # class Base
25
+
26
+ module ConnectionAdapters
27
+ class CassandraAdapter < AbstractAdapter
28
+ def initialize(client, logger, config)
29
+ super(client, logger)
30
+ @config = config
31
+ end
32
+
33
+ def supports_count_distinct?
34
+ false
35
+ end
36
+
37
+ def select(sql, name = nil)
38
+ log(sql, name)
39
+
40
+ parsed_sql = ActiveCassandra::SQLParser.new(sql).parse
41
+
42
+ cf = parsed_sql[:table].to_sym
43
+ cond = parsed_sql[:condition]
44
+ count = parsed_sql[:count]
45
+ # not implemented:
46
+ # distinct = parsed_sql[:distinct]
47
+ sqlopts, casopts = rowopts(parsed_sql)
48
+
49
+ if count and cond.empty? and sqlopts.empty?
50
+ [{count => @connection.count_range(cf, casopts)}]
51
+ elsif is_id?(cond)
52
+ ks = [cond].flatten
53
+ @connection.multi_get(cf, ks, casopts).values
54
+ else
55
+ rows = @connection.get_range(cf, casopts).select {|i| i.columns.length > 0 }.map do |key_slice|
56
+ key_slice_to_hash(key_slice)
57
+ end
58
+
59
+ unless cond.empty?
60
+ rows = filter(cond).call(rows)
61
+ end
62
+
63
+ if (offset = sqlopts[:offset])
64
+ rows = rows.slice(offset..-1)
65
+ end
66
+
67
+ if (limit = sqlopts[:limit])
68
+ rows = rows.slice(0, limit)
69
+ end
70
+
71
+ count ? [{count => rows.length}] : rows
72
+ end
73
+ end
74
+
75
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
76
+ log(sql, name)
77
+
78
+ parsed_sql = ActiveCassandra::SQLParser.new(sql).parse
79
+ table = parsed_sql[:table]
80
+ cf = table.to_sym
81
+ column_list = parsed_sql[:column_list]
82
+ value_list = parsed_sql[:value_list]
83
+
84
+ class_name = ActiveRecord::Base.class_name(table)
85
+ rowid = Module.const_get(class_name).__identify.to_s
86
+
87
+ nvs = {}
88
+ column_list.zip(value_list).each {|n, v| nvs[n] = v.to_s }
89
+
90
+ @connection.insert(cf, rowid, nvs)
91
+
92
+ return rowid
93
+ end
94
+
95
+ def update_sql(sql, name = nil)
96
+ log(sql, name)
97
+ parsed_sql = ActiveCassandra::SQLParser.new(sql).parse
98
+ cf = parsed_sql[:table].to_sym
99
+ cond = parsed_sql[:condition]
100
+
101
+ nvs = {}
102
+ parsed_sql[:set_clause_list].each do |n, v|
103
+ n = n.split('.').last
104
+ nvs[n] = v.to_s
105
+ end
106
+
107
+ n = 0
108
+
109
+ if is_id?(cond)
110
+ ks = [cond].flatten
111
+ rs = @connection.multi_get(cf, ks)
112
+
113
+ ks.each do |key|
114
+ row = rs[key]
115
+ @connection.insert(cf, key, row.merge(nvs))
116
+ n += 1
117
+ end
118
+ else
119
+ rows = @connection.get_range(cf).select {|i| i.columns.length > 0 }.map do |key_slice|
120
+ key_slice_to_hash(key_slice)
121
+ end
122
+
123
+ unless cond.empty?
124
+ rows = filter(cond).call(rows)
125
+ end
126
+
127
+ rows.each do |row|
128
+ @connection.insert(cf, row['id'], row.merge(nvs))
129
+ n += 1
130
+ end
131
+ end
132
+
133
+ return n
134
+ end
135
+
136
+ def delete_sql(sql, name = nil)
137
+ log(sql, name)
138
+
139
+ parsed_sql = ActiveCassandra::SQLParser.new(sql).parse
140
+ cf = parsed_sql[:table].to_sym
141
+ cond = parsed_sql[:condition]
142
+
143
+ n = 0
144
+
145
+ if is_id?(cond)
146
+ [cond].flatten.each do |key|
147
+ @connection.remove(cf, key)
148
+ n += 1
149
+ end
150
+ else
151
+ rows = @connection.get_range(cf).select {|i| i.columns.length > 0 }
152
+
153
+ unless cond.empty?
154
+ rows = rows.map {|i| key_slice_to_hash(i) }
155
+ rows = filter(cond).call(rows)
156
+
157
+ rows.each do |row|
158
+ @connection.remove(cf, row['id'])
159
+ n += 1
160
+ end
161
+ else
162
+ rows.each do |key_slice|
163
+ @connection.remove(cf, key_slice.key)
164
+ n += 1
165
+ end
166
+ end
167
+ end
168
+
169
+ return n
170
+ end
171
+
172
+ def add_limit_offset!(sql, options)
173
+ if (limit = options[:limit])
174
+ if limit.kind_of?(Numeric)
175
+ sql << " LIMIT #{limit.to_i}"
176
+ else
177
+ sql << " LIMIT #{quote(limit)}"
178
+ end
179
+ end
180
+
181
+ if (offset = options[:offset])
182
+ if offset.kind_of?(Numeric)
183
+ sql << " OFFSET #{offset.to_i}"
184
+ else
185
+ sql << " OFFSET #{quote(offset)}"
186
+ end
187
+ end
188
+ end
189
+
190
+ private
191
+ def key_slice_to_hash(key_slice)
192
+ hash = {'id' => key_slice.key}
193
+
194
+ key_slice.columns.each do |i|
195
+ column = i.column
196
+ hash[column.name] = column.value
197
+ end
198
+
199
+ return hash
200
+ end
201
+
202
+ def is_id?(cond)
203
+ not cond.kind_of?(Array) or not cond.all? {|i| i.kind_of?(Hash) }
204
+ end
205
+
206
+ def filter(cond)
207
+ fs = []
208
+
209
+ cond.each do |c|
210
+ name, op, expr, has_not = c.values_at(:name, :op, :expr, :not)
211
+ name = name.split('.').last
212
+ expr = Regexp.compile(expr) if op == '$regexp'
213
+
214
+ func = case op
215
+ when '$in'
216
+ lambda {|i| expr.include?(i) }
217
+ when '$bt'
218
+ lambda {|i| expr[0] <= i and i <= expr[1] }
219
+ when '$regexp'
220
+ lambda {|i| i =~ Regexp.compile(expr) }
221
+ when :'>=', :'<=', :'>', :'<'
222
+ lambda {|i| i.to_i.send(op, expr.to_i) }
223
+ else
224
+ lambda {|i| i.send(op, expr) }
225
+ end
226
+
227
+ fs << (has_not ? lambda {|row| not func.call(row[name]) } : lambda {|row| func.call(row[name])})
228
+ end
229
+
230
+ lambda do |rows|
231
+ fs.inject(rows) {|r, f| r.select {|i| f.call(i) } }
232
+ end
233
+ end
234
+
235
+ def rowopts(parsed_sql)
236
+ order, limit, offset = parsed_sql.values_at(:order, :limit, :offset)
237
+ sqlopts = {}
238
+ casopts = {}
239
+
240
+ # not implemented:
241
+ # if order
242
+ # name, type = order.values_at(:name, :type)
243
+ # ...
244
+ # end
245
+
246
+ if offset
247
+ if offset.kind_of?(Numeric)
248
+ sqlopts[:offset] = offset
249
+ else
250
+ # XXX: offset is not equals to SQL OFFSET
251
+ casopts[:start] = offset
252
+ end
253
+ end
254
+
255
+ if limit
256
+ if limit.kind_of?(Numeric)
257
+ sqlopts[:limit] = limit
258
+ else
259
+ # XXX: limit is not equals to SQL LIMIT
260
+ casopts[:finish] = limit
261
+ end
262
+ end
263
+
264
+ return [sqlopts, casopts]
265
+ end
266
+
267
+ end # class CassandraAdapter
268
+ end # module ConnectionAdapters
269
+ end # module ActiveRecord