philtre 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,195 @@
1
+ require 'ripar'
2
+
3
+ require 'philtre/filter.rb'
4
+ require 'philtre/place_holder.rb'
5
+ require 'philtre/empty_expression.rb'
6
+
7
+ # Using the expressions in the filter, transform a dataset with
8
+ # placeholders into a real dataset with expressions, for example:
9
+ #
10
+ # ds = Personage.filter( :brief.lieu, :title.lieu ).order( :age.lieu )
11
+ # g = Grinder.new( Philtre.new(title: 'Grand High Poobah', :order => :age.desc ) )
12
+ # nds = g.transform( ds )
13
+ # nds.sql
14
+ #
15
+ # => SELECT * FROM "personages" WHERE (("title" = 'Grand High Poobah'))
16
+ #
17
+ # In a sense, this is a means to defining SQL functions with
18
+ # optional keyword arguments.
19
+ class Philtre::Grinder < Sequel::ASTTransformer
20
+ # filter must respond to expr_for( key, sql_field = nil ), expr_hash and order_hash
21
+ def initialize( filter = Philtre::Filter.new )
22
+ @filter = filter
23
+ end
24
+
25
+ attr_reader :filter
26
+
27
+ # pass in a dataset containing PlaceHolder expressions.
28
+ # you'll get back a modified dataset with the filter values
29
+ # filled in.
30
+ def transform( dataset, apply_unknown: true )
31
+ @unknown = []
32
+ @places = {}
33
+ @subsets = []
34
+
35
+ # handy for debugging
36
+ @original_dataset = dataset
37
+
38
+ # the transformed dataset with placeholders that
39
+ # exist in filter replaced. unknown might have values
40
+ # after this.
41
+ t_dataset = super(dataset)
42
+ unknown_placeholders
43
+
44
+ if unknown.any?
45
+ if apply_unknown
46
+ # now filter by whatever predicates are left over
47
+ # ie those not in the incoming dataset. Leftover
48
+ # order parameters will overwrite existing ones
49
+ # that are not protected by an outer select.
50
+ filter.subset( *unknown ).apply t_dataset
51
+ else
52
+ raise "unknown values #{unknown.inspect} for\n#{dataset.sql}"
53
+ end
54
+ else
55
+ t_dataset
56
+ end
57
+ end
58
+
59
+ alias [] transform
60
+
61
+ # Grouped hash of place holders in the original dataset from the last transform.
62
+ # Only has values after transform has been called.
63
+ def places
64
+ @places || raise("Call transform to find place holders.")
65
+ end
66
+
67
+ # collection of values in the filter that were not found as placeholders
68
+ # in the original dataset.
69
+ def unknown
70
+ @unknown || raise("Call transform to find placeholders not provided by the filter.")
71
+ end
72
+
73
+ protected
74
+
75
+ # Set of all keys that are not in the placeholders
76
+ def extra_keys( keys )
77
+ incoming = keys
78
+ existing = places.flat_map{|subset, placeholders| placeholders.keys}
79
+
80
+ # find the elements in incoming that are not in existing
81
+ incoming - existing & incoming
82
+ end
83
+
84
+ def unknown_placeholders
85
+ unknown.concat extra_keys( filter.expr_hash.keys )
86
+ unknown.concat extra_keys( filter.order_hash.keys )
87
+ end
88
+
89
+ def context_places
90
+ @places ||= {}
91
+ @places[subset] ||= {}
92
+ end
93
+
94
+ # TODO rename subset to clause
95
+ def subset
96
+ @subsets.last || :none
97
+ end
98
+
99
+ def subset_stack
100
+ @subsets ||= []
101
+ end
102
+
103
+ def push_subset( latest_subset, &block )
104
+ subset_stack.push latest_subset
105
+ rv = yield
106
+ subset_stack.pop
107
+ rv
108
+ end
109
+
110
+ # Override the ASTTransformer method, which is where the work
111
+ # is done to transform the dataset containing placeholders into
112
+ # a dataset containing a proper SQL statement.
113
+ # Yes, this is in fact every OO purist's worst nightware - a Giant Switch Statement.
114
+ def v( obj )
115
+ case obj
116
+ when Sequel::Dataset
117
+ # transform empty expressions to false (or nil, but false is more debuggable)
118
+ # can't use nil for all kinds of expressions because nil mean NULL for
119
+ # most of the Sequel::SQL expressions.
120
+ obj.clone Hash[ v(obj.opts).map{|k,val| [k, val.is_a?(Philtre::EmptyExpression) ? false : val]} ]
121
+
122
+ # for Sequel::Models
123
+ when ->(obj){obj.respond_to? :dataset}
124
+ v obj.dataset
125
+
126
+ # for other things that are convertible to dataset
127
+ when ->(obj){obj.respond_to? :to_dataset}
128
+ v obj.to_dataset
129
+
130
+ # Keep the context for place holders,
131
+ # so we know what kind of expression to insert later.
132
+ # Each of :where, :order, :having, :select etc will come as a hash.
133
+ # There are some other top-level options too.
134
+ when Hash
135
+ rv = {}
136
+ obj.each do |key, val|
137
+ push_subset key do
138
+ rv[v(key)] = v(val)
139
+ end
140
+ end
141
+ rv
142
+
143
+ when Philtre::PlaceHolder
144
+ # get the expression for the placeholder.
145
+ # use the placeholder's field name if given
146
+ expr =
147
+ case subset
148
+ # substitute a comparison or some other predicate
149
+ when :where, :having
150
+ filter.expr_for obj.name, obj.sql_field
151
+
152
+ # substitute an order by expression
153
+ when :order
154
+ filter.order_for obj.name
155
+
156
+ # Substitute the field name only if it has a value.
157
+ # nil when the name doesn't have a value. This way,
158
+ # select :some_field.lieu
159
+ # is left out when some_field does not have a value.
160
+ when :select
161
+ if filter[obj.name]
162
+ obj.sql_field || obj.name
163
+ end
164
+
165
+ else
166
+ raise "don't understand subset #{subset}"
167
+ end
168
+
169
+ # keep it, just in case
170
+ context_places[obj.name] = expr
171
+
172
+ # transform
173
+ expr || Philtre::EmptyExpression.new
174
+
175
+ when Array
176
+ # sometimes things are already an empty array, in which
177
+ # case just leave them alone.
178
+ return super if obj.empty?
179
+
180
+ # collect expressions, some may be empty.
181
+ exprs = super.reject{|e| e.is_a? Philtre::EmptyExpression}
182
+
183
+ # an empty array of expressions must be translated
184
+ # to an empty expression at this point.
185
+ exprs.empty? ? Philtre::EmptyExpression.new : exprs
186
+
187
+ when Sequel::SQL::ComplexExpression
188
+ # use the Array case above, otherwise copy the expression itself
189
+ v( obj.args ).empty? ? Philtre::EmptyExpression.new : super
190
+
191
+ else
192
+ super
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,41 @@
1
+ module Philtre
2
+ class PlaceHolder < Sequel::SQL::Expression
3
+ # name is what gets replaced by the operation and correspondingly named value in the filter
4
+ # sql_field is the name of the field that the operation will compare the named value to.
5
+ def initialize( name, sql_field = nil, bt = caller )
6
+ # backtrace
7
+ @bt = bt
8
+
9
+ @name = name
10
+ @sql_field = sql_field
11
+ end
12
+
13
+ attr_reader :bt
14
+ attr_reader :name
15
+ attr_reader :sql_field
16
+
17
+ # this is inserted into the generated SQL from a dataset that
18
+ # contains PlaceHolder instances.
19
+ def to_s_append( ds, s )
20
+ s << '$' << name.to_s
21
+ s << ':' << sql_field.to_s if sql_field
22
+ s << '/*' << small_source << '*/'
23
+ end
24
+
25
+ def source
26
+ bt[1]
27
+ end
28
+
29
+ def small_source
30
+ source.split('/').last(2).join('/').split(':')[0..1].join(':')
31
+ end
32
+
33
+ def inspect
34
+ "#<#{self.class} #{name}:#{sql_field} @#{source}>"
35
+ end
36
+
37
+ def to_s
38
+ "#{name}:#{sql_field} @#{small_source}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ module Philtre
2
+ # This is a specialised module that also understands a simple
3
+ # DSL for creating predicates as methods.
4
+ #
5
+ # This is how the DSL works:
6
+ # each meth is a predicate, args is a set of alternatives
7
+ # and the block must return something convertible to a Sequel.expr
8
+ # to create the expression for that predicate.
9
+ class PredicateDsl < Module
10
+ def initialize( &bloc )
11
+ if bloc
12
+ if bloc.arity == 0
13
+ module_eval &bloc
14
+ else
15
+ bloc.call self
16
+ end
17
+ end
18
+ end
19
+
20
+ def method_missing(meth, *args, &bloc)
21
+ define_method meth, &bloc
22
+ args.each{|arg| send :alias_method, arg, meth }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ require 'fastandand'
2
+
3
+ module Philtre
4
+ # This is to split things like birth_year_gt into
5
+ # - birth_year (the field)
6
+ # - gt (the predicate)
7
+ # Yes, there are side effects.
8
+ # === is provided so it can be used in case statements
9
+ # (which doesn't really work cos they're backwards).
10
+ class PredicateSplitter
11
+ def initialize( key, value )
12
+ @key, @value = key, value
13
+ end
14
+
15
+ attr_reader :key, :value
16
+
17
+ # split suffix from the key and store the two values as name and op
18
+ # return truthy if successful
19
+ def split_key( suffix )
20
+ rv = @key =~ /(.*?)_?(#{suffix})$/
21
+ @field, @op = $1, $2
22
+ rv
23
+ end
24
+
25
+ alias_method :===, :split_key
26
+ alias_method :=~, :split_key
27
+
28
+ # return name if the split was successful, or fall back to key
29
+ # which is handy when none of the predicates match and so key
30
+ # is probably just a field name.
31
+ def field
32
+ (@field || @key).andand.to_sym
33
+ end
34
+
35
+ # the operator, or predicate
36
+ def op
37
+ @op.andand.to_sym
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,109 @@
1
+ module Philtre
2
+ # Container for methods which return Sequel::SQL::Expression or something
3
+ # that can become one through Sequel.expr, eg {year: 2013}
4
+ #
5
+ # Reminder: they're defined as methods so we can benefit from
6
+ # using them inside this class to define other predicates.
7
+ #
8
+ # This can be extended in all the usual ruby ways: by including a module,
9
+ # reopening the class. Also using extend_with which takes a PredicateDsl block.
10
+ class Predicates
11
+ def initialize( &bloc )
12
+ extend_with &bloc
13
+ end
14
+
15
+ # pass a block that can contain a combination of def meth_name() or the
16
+ # DSL defined by PredicateDsl.
17
+ def extend_with( &bloc )
18
+ extend PredicateDsl.new(&bloc)
19
+ end
20
+
21
+ # Convert a field_predicate (field_op format), a value and a SQL field
22
+ # name using the set of predicates, to something convertible to a Sequel
23
+ # expression.
24
+ #
25
+ # field_predicate:: is a key from the parameter hash. Usually name_pred, eg birth_year_gt
26
+ # value:: is the value from the parameter hash. Could be a collection.
27
+ # field:: is the name of the SQL field to use, or nil where it would default to key without its predicate.
28
+ def define_name_predicate( field_predicate, value, field = nil )
29
+ splitter = PredicateSplitter.new field_predicate, value
30
+
31
+ # the default / else / fall-through is just an equality
32
+ default_proc = ->{:eq}
33
+
34
+ # find a better predicate, if there is one
35
+ suffix = predicate_names.find default_proc do |suffix|
36
+ splitter =~ suffix
37
+ end
38
+
39
+ # wrap the field name first, to infect the expressions so all the
40
+ # operators work.
41
+ field_name = Sequel.expr(field || splitter.field)
42
+
43
+ define_singleton_method field_predicate do |value|
44
+ send suffix, field_name, value
45
+ end
46
+ end
47
+
48
+ protected :define_name_predicate
49
+
50
+ # TODO this should probably also be method_missing?
51
+ # field is only used once for any given field_predicate
52
+ def call( field_predicate, value, field = nil )
53
+ unless respond_to? field_predicate
54
+ define_name_predicate field_predicate, value, field
55
+ end
56
+ send field_predicate, value
57
+ end
58
+
59
+ # The main interface from Filter#to_expr
60
+ alias [] call
61
+
62
+ def predicate_names
63
+ DefaultPredicates.instance_methods
64
+ end
65
+
66
+ # Define the set of default predicates.
67
+ DefaultPredicates = PredicateDsl.new do
68
+ # longer suffixes first, so they match first in define_name_predicate
69
+ not_eq {|expr, val| ~Sequel.expr( expr => val) }
70
+
71
+ def not_like( expr, val )
72
+ Sequel.~(like expr, val)
73
+ end
74
+
75
+ matches( :like ) {|expr, val| Sequel.expr( expr => /#{val}/i) }
76
+
77
+ def not_blank(expr, val)
78
+ Sequel.~(blank expr, val)
79
+ end
80
+
81
+ def blank(expr, _)
82
+ is_nil = Sequel.expr(expr => nil)
83
+ is_empty = Sequel.expr(expr => '')
84
+ Sequel.| is_nil, is_empty
85
+ end
86
+
87
+ # and now the shorter suffixes
88
+ eq {|expr, val| Sequel.expr( expr => val) }
89
+ gt {|expr, val| expr > val }
90
+ gte( :gteq ) {|expr, val| expr >= val }
91
+ lt {|expr, val| expr < val }
92
+ lte( :lteq ) {|expr, val| expr <= val }
93
+
94
+ # more complex predicates
95
+ def like_all( expr, arg )
96
+ exprs = Array(arg).map {|value| like expr, value }
97
+ Sequel.& *exprs
98
+ end
99
+
100
+ def like_any( expr, arg )
101
+ exprs = Array(arg).map {|value| like expr, value }
102
+ Sequel.| *exprs
103
+ end
104
+ end
105
+
106
+ # make the available to Predicates instances.
107
+ include DefaultPredicates
108
+ end
109
+ end
@@ -0,0 +1,30 @@
1
+ # TODO docs for this
2
+ class Sequel::Dataset
3
+ include Ripar
4
+
5
+ # make the roller understand dataset method
6
+ def roller
7
+ rv = super
8
+ class << rv
9
+ def to_dataset; riven end
10
+ end
11
+ rv
12
+ end
13
+
14
+ # roll the block and return the resulting dataset immediately
15
+ def rolled( &blk )
16
+ roller.rive &blk
17
+ end
18
+ end
19
+
20
+ class Sequel::Dataset
21
+ # filter must respond_to expr_hash and order_hash
22
+ # will optionally yield a Grinder instance to the block
23
+ def grind( filter = Philtre::Filter.new, apply_unknown: true )
24
+ grinder = Philtre::Grinder.new filter
25
+ t_dataset = grinder.transform self, apply_unknown: apply_unknown
26
+ # only yield after the transform, so the grinder has the place holders
27
+ yield grinder if block_given?
28
+ t_dataset
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
- module Philtre
2
- VERSION = "0.0.0"
1
+ module Philtre #:nodoc:
2
+ VERSION = "0.0.1"
3
3
  end