smql 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ Idea
2
+ ====
3
+
4
+ Similar to MQL: SMQL allowes SQL-queries on your database but in a JSON-based language.
5
+
6
+ This query language is SQL-injection-safe.
7
+ Only expencive queries can slow down your machine.
8
+
9
+ Usage
10
+ =====
11
+
12
+ Easy query in ruby:
13
+ User is a AR-Model and has a column username.
14
+ We want to find all users which has the username "auser".
15
+
16
+ require 'smql'
17
+
18
+ SmqlToAR.to_ar User, '{"username": "auser"}' # Query in JSON
19
+ SmqlToAR.to_ar User, username: "auser" # Query in Ruby
20
+
21
+ In Rails:
22
+
23
+ SmqlToAR.to_ar User, params[:smql]
24
+
25
+ Don't forget to add gem to Gemfile:
26
+
27
+ gem 'smql'
data/TODO ADDED
@@ -0,0 +1 @@
1
+ 1) gem-plugin
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/lib/smql.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'active_support/benchmarkable'
2
+ require 'active_support/core_ext/array.rb'
3
+ require 'active_record'
4
+ require 'json'
5
+ require 'smql_to_ar'
6
+ require 'smql_to_ar/condition_types'
7
+ require 'smql_to_ar/query_builder'
@@ -0,0 +1,327 @@
1
+ # SmqlToAR - Parser: Converts SMQL to ActiveRecord
2
+ # Copyright (C) 2011 Denis Knauf
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ class SmqlToAR
18
+ #############################################################################
19
+ # Alle Subklassen (qualitativ: ConditionTypes::*), die als Superklasse Condition haben,
20
+ # stellen eine Regel dar, unter diesen sie das gesuchte Objekt annehmen.
21
+ # Nimmt eine solche Klasse ein Object nicht an, so wird die naechste Klasse ausprobiert.
22
+ # Es wird in der Reihenfolge abgesucht, in der #constants die Klassen liefert,
23
+ # wobei angenommen wird, dass diese nach dem Erstellungszeitpunkt sortiert sind,
24
+ # aeltere zuerst.
25
+ # Nimmt eine Klasse ein Objekt an, so soll diese Klasse instanziert werden.
26
+ # Alles weitere siehe Condition.
27
+ module ConditionTypes
28
+ class <<self
29
+ # Ex: 'givenname|surname|nick' => [:givenname, :surname, :nick]
30
+ def split_keys k
31
+ k.split( '|').collect &:to_sym
32
+ end
33
+
34
+ # Eine Regel parsen.
35
+ # Ex: Person, "givenname=", "Peter"
36
+ def try_parse_it model, colop, val
37
+ r = nil
38
+ #p :try_parse => { :model => model, :colop => colop, :value => val }
39
+ constants.each do |c|
40
+ next if :Condition == c
41
+ c = const_get c
42
+ next if Condition === c
43
+ raise UnexpectedColOpError.new( model, colop, val) unless colop =~ /^(?:\d*:)?(.*?)(\W*)$/
44
+ col, op = $1, $2
45
+ col = split_keys( col).collect {|c| Column.new model, c }
46
+ r = c.try_parse model, col, op, val
47
+ break if r
48
+ end
49
+ raise UnexpectedError.new( model, colop, val) unless r
50
+ r
51
+ end
52
+
53
+ # Alle Regeln parsen. Die Regeln sind in einem Hash der Form {colop => val}
54
+ # Ex: Person, {"givenname=", "Peter", "surname=", "Mueller"}
55
+ def try_parse model, colopvals
56
+ colopvals.collect do |colop, val|
57
+ #p :try_parse => { colop: colop, val: val, model: model }
58
+ try_parse_it model, colop, val
59
+ end
60
+ rescue SMQLError => e
61
+ raise SubSMQLError.new( colopvals, model, e)
62
+ end
63
+
64
+ # Erstellt eine Condition fuer eine Regel.
65
+ def simple_condition superclass, op = nil, where = nil, expected = nil
66
+ cl = Class.new superclass
67
+ cl.const_set :Operator, op if op
68
+ cl.const_set :Where, where if where
69
+ cl.const_set :Expected, expected if expected
70
+ cl
71
+ end
72
+ end
73
+
74
+ class Condition
75
+ attr_reader :value, :cols
76
+ Operator = nil
77
+ Expected = []
78
+ Where = nil
79
+
80
+ # Versuche das Objekt zu erkennen. Operator und Expected muessen passen.
81
+ # Passt das Object, die Klasse instanzieren.
82
+ def self.try_parse model, cols, op, val
83
+ #p :self => name, :try_parse => op, :cols => cols, :with => self::Operator, :value => val, :expected => self::Expected, :model => model.name
84
+ new model, cols, val if self::Operator === op and self::Expected.any? {|i| i === val }
85
+ end
86
+
87
+ def initialize model, cols, val
88
+ @model, @cols = model, cols
89
+ @value = case val
90
+ when Hash, Range then val
91
+ else Array.wrap val
92
+ end
93
+ verify
94
+ end
95
+
96
+ def verify
97
+ @cols.each do |col|
98
+ verify_column col
99
+ verify_allowed col
100
+ end
101
+ end
102
+
103
+ # Gibt es eine Spalte diesen Namens?
104
+ # Oder: Gibt es eine Relation diesen Namens? (Hier nicht der Fall)
105
+ def verify_column col
106
+ raise NonExistingColumnError.new( %w[Column], col) unless col.exist_in?
107
+ end
108
+
109
+ # Modelle koennen Spalten/Relationen verbieten mit Model#smql_protected.
110
+ # Dieses muss ein Object mit #include?( name_als_string) zurueckliefern,
111
+ # welches true fuer verboten und false fuer, erlaubt steht.
112
+ def verify_allowed col
113
+ raise ProtectedColumnError.new( col) if col.protected?
114
+ end
115
+
116
+ # Erstelle alle noetigen Klauseln. builder nimmt diese entgegen,
117
+ # wobei builder.join, builder.select, builder.where und builder.wobs von interesse sind.
118
+ # mehrere Schluessel bedeuten, dass die Values _alle_ zutreffen muessen, wobei die Schluessel geodert werden.
119
+ # Ex:
120
+ # 1) {"givenname=", "Peter"} #=> givenname = 'Peter'
121
+ # 2) {"givenname=", ["Peter", "Hans"]} #=> ( givenname = 'Peter' OR givenname = 'Hans' )
122
+ # 3) {"givenname|surname=", ["Peter", "Mueller"]}
123
+ # #=> ( givenname = 'Peter' OR surname = 'Peter' ) AND ( givenname = 'Mueller' OR surname = 'Mueller' )
124
+ def build builder, table
125
+ values = Hash[ @value.collect {|value| [ builder.vid, value ] } ]
126
+ values.each {|k, v| builder.wobs k.sym => v }
127
+ if 1 == @cols.length
128
+ @cols.each do |col|
129
+ col.joins builder, table
130
+ col = builder.column table+col.path, col.col
131
+ builder.where *values.keys.collect {|vid| self.class::Where % [ col, vid.to_s ] }
132
+ end
133
+ else
134
+ values.keys.each do |vid|
135
+ builder.where *@cols.collect {|col|
136
+ col.joins builder, table
137
+ col = builder.column table+col.path, col.col
138
+ self.class::Where % [ col, vid.to_s ]
139
+ }
140
+ end
141
+ end
142
+ self
143
+ end
144
+ end
145
+
146
+ class NotInRange < Condition
147
+ Operator = '!..'
148
+ Where = "%s NOT BETWEEN %s AND %s"
149
+ Expected = [Range, lambda {|val| Array === val && 2 == val.length } ]
150
+
151
+ def initialze model, cols, val
152
+ if Array === val && 2 == val.length
153
+ f, l = val
154
+ f, l = Time.parse(f), Time.parse(l) if f.kind_of? String
155
+ val = f..l
156
+ end
157
+ super model, cols, val
158
+ end
159
+
160
+ def build builder, table
161
+ builder.wobs (v1 = builder.vid) => @value.begin, (v2 = builder.vid) => @value.end
162
+ @cols.each do |col|
163
+ col.joins builder, table
164
+ builder.where self.class::Where % [ builder.column( table+col.path, col.col), v1, v2]
165
+ end
166
+ self
167
+ end
168
+ end
169
+ InRange = simple_condition NotInRange, '..', "%s BETWEEN %s AND %s"
170
+
171
+ class NotIn < Condition
172
+ Operator = '!|='
173
+ Where = "%s NOT IN (%s)"
174
+ Expected = [Array]
175
+
176
+ def build builder, table
177
+ builder.wobs (v = builder.vid).to_sym => @value
178
+ @cols.each do |col|
179
+ col.joins builder, table
180
+ builder.where self.class::Where % [ builder.column( table, col), v.to_s]
181
+ end
182
+ self
183
+ end
184
+ end
185
+
186
+ In = simple_condition NotIn, '|=', '%s IN (%s)', [Array]
187
+ In2 = simple_condition In, '', nil, [Array]
188
+ NotEqual = simple_condition Condition, /\!=|<>/, "%s <> %s", [Array, String, Numeric]
189
+ GreaterThanOrEqual = simple_condition Condition, '>=', "%s >= %s", [Array, Numeric]
190
+ LesserThanOrEqual = simple_condition Condition, '<=', "%s <= %s", [Array, Numeric]
191
+ class EqualJoin <Condition
192
+ Operator = '='
193
+ Expected = [Hash]
194
+
195
+ def initialize *pars
196
+ super( *pars)
197
+ cols = {}
198
+ @cols.each do |col|
199
+ col_model = SmqlToAR.model_of col.last_model, col.col
200
+ #p col_model: col_model.to_s, value: @value
201
+ cols[col] = [col_model] + ConditionTypes.try_parse( col_model, @value)
202
+ end
203
+ @cols = cols
204
+ end
205
+
206
+ def verify_column col
207
+ refl = SmqlToAR.model_of col.last_model, col.col
208
+ #p refl: refl, model: @model.name, col: col, :reflections => @model.reflections.keys
209
+ raise NonExistingRelationError.new( %w[Relation], col) unless refl
210
+ end
211
+
212
+ def build builder, table
213
+ @cols.each do |col, sub|
214
+ t = table + col.path + [col.col]
215
+ #p sub: sub
216
+ p col: col, joins: col.joins
217
+ col.joins.each {|j, m| builder.join table+j, m }
218
+ builder.join t, SmqlToAR.model_of( col.last_model, col.col)
219
+ sub[1..-1].each {|one| one.build builder, t }
220
+ end
221
+ self
222
+ end
223
+ end
224
+ Equal = simple_condition Condition, '=', "%s = %s", [Array, String, Numeric]
225
+ Equal2 = simple_condition Equal, '', "%s = %s", [String, Numeric]
226
+ GreaterThan = simple_condition Condition, '>', "%s > %s", [Array, Numeric]
227
+ LesserThan = simple_condition Condition, '<', "%s < %s", [Array, Numeric]
228
+ NotIlike = simple_condition Condition, '!~', "%s NOT ILIKE %s", [Array, String]
229
+ Ilike = simple_condition Condition, '~', "%s ILIKE %s", [Array, String]
230
+
231
+ ####### No Operator #######
232
+ Join = simple_condition EqualJoin, '', nil, [Hash]
233
+ InRange2 = simple_condition InRange, '', nil, [Range]
234
+ class Select < Condition
235
+ Operator = ''
236
+ Expected = [nil]
237
+
238
+ def verify_column col
239
+ raise NonExistingSelectableError.new( col) unless col.exist_in? or SmqlToAR.model_of( col.last_model, col.col)
240
+ end
241
+
242
+ def build builder, table
243
+ @cols.each do |col|
244
+ if col.exist_in?
245
+ col.joins builder, table
246
+ builder.select table+col.to_a
247
+ else
248
+ col.joins {|j, m| builder.includes table+j }
249
+ builder.includes table+col.to_a
250
+ end
251
+ end
252
+ self
253
+ end
254
+ end
255
+
256
+ class Functions < Condition
257
+ Operator = ':'
258
+ Expected = [String, Array, Hash, Numeric, nil]
259
+
260
+ class Function
261
+ Name = nil
262
+ Expected = []
263
+ attr_reader :model, :func, :args
264
+
265
+ def self.try_parse model, func, args
266
+ SmqlToAR.logger.info( { try_parse: [func,args]}.inspect)
267
+ self.new model, func, args if self::Name === func and self::Expected.any? {|e| e === args }
268
+ end
269
+
270
+ def initialize model, func, args
271
+ @model, @func, @args = model, func, args
272
+ end
273
+ end
274
+
275
+ class Order < Function
276
+ Name = :order
277
+ Expected = [String, Array, Hash, nil]
278
+
279
+ def initialize model, func, args
280
+ SmqlToAR.logger.info( {args: args}.inspect)
281
+ args = case args
282
+ when String then [args]
283
+ when Array, Hash then args.to_a
284
+ when nil then nil
285
+ else raise 'Oops'
286
+ end
287
+ SmqlToAR.logger.info( {args: args}.inspect)
288
+ args.andand.collect! do |o|
289
+ o = Array.wrap o
290
+ col = Column.new model, o.first
291
+ o = 'desc' == o.last.to_s.downcase ? :DESC : :ASC
292
+ raise NonExistingColumnError.new( [:Column], col) unless col.exist_in?
293
+ [col, o]
294
+ end
295
+ SmqlToAR.logger.info( {args: args}.inspect)
296
+ super model, func, args
297
+ end
298
+
299
+ def build builder, table
300
+ return if @args.blank?
301
+ @args.each do |o|
302
+ col, o = o
303
+ col.joins builder, table
304
+ t = table + col.path
305
+ raise OnlyOrderOnBaseError.new( t) unless 1 == t.length
306
+ builder.order t, col.col, o
307
+ end
308
+ end
309
+ end
310
+
311
+ def self.new model, col, val
312
+ SmqlToAR.logger.info( { function: col.first.to_sym }.inspect)
313
+ r = nil
314
+ constants.each do |c|
315
+ next if [:Function, :Where, :Expected, :Operator].include? c
316
+ c = const_get c
317
+ next if Function === c or not c.respond_to?( :try_parse)
318
+ SmqlToAR.logger.info( {f: c}.inspect)
319
+ r = c.try_parse model, col.first.to_sym, val
320
+ SmqlToAR.logger.info( {r: r}.inspect)
321
+ break if r
322
+ end
323
+ r
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,160 @@
1
+ # SmqlToAR - Builds AR-querys: Converts SMQL to ActiveRecord
2
+ # Copyright (C) 2011 Denis Knauf
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ class SmqlToAR
18
+ #######################################################################################
19
+ # Baut die Queries zusammen.
20
+ class QueryBuilder
21
+ # Erzeugt einen eindeutigen Identikator "cX", wobei X iteriert wird.
22
+ class Vid
23
+ attr_reader :vid
24
+ def initialize( vid) @vid = vid end
25
+ def to_s() ":c#{@vid}" end
26
+ def to_sym() "c#{@vid}".to_sym end
27
+ alias sym to_sym
28
+ def to_i() @vid end
29
+ end
30
+
31
+ attr_reader :table_alias, :model, :table_model, :base_table, :_where, :_select, :_wobs, :_joins
32
+ attr_accessor :logger
33
+ @@logger = SmqlToAR.logger
34
+
35
+ def initialize model
36
+ @logger = @@logger
37
+ @table_alias = Hash.new do |h, k|
38
+ k = Array.wrap k
39
+ h[k] = "smql,#{k.join(',')}"
40
+ end
41
+ @_vid, @_where, @_wobs, @model, @quoter = 0, [], {}, model, model.connection
42
+ @base_table = [model.table_name.to_sym]
43
+ @table_alias[ @base_table] = @base_table.first
44
+ t = quote_table_name @table_alias[ @base_table]
45
+ @_select, @_joins, @_joined, @_includes, @_order = ["DISTINCT #{t}.*"], "", [], [], []
46
+ @table_model = {@base_table => @model}
47
+ end
48
+
49
+ def vid() Vid.new( @_vid+=1) end
50
+
51
+ # Jede via where uebergebene Condition wird geodert und alle zusammen werden geundet.
52
+ # "Konjunktive Normalform". Allerdings duerfen Conditions auch Komplexe Abfragen enthalten.
53
+ # Ex: builder.where( 'a = a', 'b = c').where( 'c = d', 'e = e').where( 'x = y').where( '( m = n AND o = p )', 'f = g')
54
+ # #=> WHERE ( a = a OR b = c ) AND ( c = d OR e = e ) AND x = y ( ( m = n AND o = p ) OR f = g )
55
+ def where *cond
56
+ @_where.push cond
57
+ self
58
+ end
59
+
60
+ def wobs vals
61
+ @_wobs.update vals
62
+ self
63
+ end
64
+
65
+ def quote_column_name name
66
+ @quoter.quote_column_name( name).gsub /"\."/, ','
67
+ end
68
+
69
+ def quote_table_name name
70
+ @quoter.quote_table_name( name).gsub /"\."/, ','
71
+ end
72
+
73
+ def column table, name
74
+ "#{quote_table_name table.kind_of?(String) ? table : @table_alias[table]}.#{quote_column_name name}"
75
+ end
76
+
77
+ def build_join orig, pretable, table, prekey, key
78
+ " JOIN #{quote_table_name orig.to_sym} AS #{quote_table_name table} ON #{column pretable, prekey} = #{column table, key} "
79
+ end
80
+
81
+ def join table, model
82
+ return self if @_joined.include? table # Already joined
83
+ pretable = table[0...-1]
84
+ @table_model[ table] = model
85
+ premodel = @table_model[ pretable]
86
+ t = @table_alias[ table]
87
+ pt = quote_table_name @table_alias[ table[ 0...-1]]
88
+ refl = premodel.reflections[table.last]
89
+ case refl.macro
90
+ when :has_many
91
+ @_joins += build_join model.table_name, pretable, t, premodel.primary_key, refl.primary_key_name
92
+ when :belongs_to
93
+ @_joins += build_join model.table_name, pretable, t, refl.primary_key_name, premodel.primary_key
94
+ when :has_and_belongs_to_many
95
+ jointable = [','] + table
96
+ @_joins += build_join refl.options[:join_table], pretable, @table_alias[jointable], premodel.primary_key, refl.primary_key_name
97
+ @_joins += build_join model.table_name, jointable, t, refl.association_foreign_key, refl.association_primary_key
98
+ else raise BuilderError, "Unkown reflection macro: #{refl.macro.inspect}"
99
+ end
100
+ @_joined.push table
101
+ self
102
+ end
103
+
104
+ def includes table
105
+ @_includes.push table
106
+ self
107
+ end
108
+
109
+ def select col
110
+ @_select.push quote_column_name( @table_alias[col])
111
+ self
112
+ end
113
+
114
+ def order table, col, o
115
+ @_order.push "#{column table, col} #{:DESC == o ? :DESC : :ASC}"
116
+ end
117
+
118
+ class Dummy
119
+ def method_missing m, *a, &e
120
+ #p :dummy => m, :pars => a, :block => e
121
+ self
122
+ end
123
+ end
124
+
125
+ def build_ar
126
+ where_str = @_where.collect do |w|
127
+ w = Array.wrap w
128
+ 1 == w.length ? w.first : "( #{w.join( ' OR ')} )"
129
+ end.join ' AND '
130
+ incls = {}
131
+ @_includes.each do |inc|
132
+ b = incls
133
+ inc[1..-1].collect {|rel| b = b[rel] ||= {} }
134
+ end
135
+ @logger.debug incls: incls, joins: @_joins
136
+ @model = @model.
137
+ select( @_select.join( ', ')).
138
+ joins( @_joins).
139
+ where( where_str, @_wobs).
140
+ order( @_order.join( ', ')).
141
+ includes( incls)
142
+ end
143
+
144
+ def fix_calculate
145
+ def @model.calculate operation, column_name, options = nil
146
+ options = options.try(:dup) || {}
147
+ options[:distinct] = true unless options.except(:distinct).present?
148
+ column_name = klass.primary_key unless column_name.present?
149
+ super operation, column_name, options
150
+ end
151
+ self
152
+ end
153
+
154
+ def to_ar
155
+ build_ar
156
+ fix_calculate
157
+ @model
158
+ end
159
+ end
160
+ end