activerecord-cassandra-adapter 0.1.0

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