smql 0.0.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.
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