praxis 2.0.pre.5 → 2.0.pre.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +0 -1
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +22 -0
  5. data/Gemfile +1 -1
  6. data/Guardfile +2 -1
  7. data/Rakefile +1 -7
  8. data/TODO.md +28 -0
  9. data/lib/api_browser/package-lock.json +7110 -0
  10. data/lib/praxis.rb +6 -4
  11. data/lib/praxis/action_definition.rb +9 -16
  12. data/lib/praxis/application.rb +1 -2
  13. data/lib/praxis/bootloader_stages/routing.rb +2 -4
  14. data/lib/praxis/extensions/attribute_filtering.rb +2 -0
  15. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
  16. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
  17. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
  18. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
  19. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
  20. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
  21. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +9 -12
  22. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
  23. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
  24. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
  25. data/lib/praxis/extensions/pagination.rb +130 -0
  26. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
  27. data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
  28. data/lib/praxis/extensions/pagination/ordering_params.rb +234 -0
  29. data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
  30. data/lib/praxis/extensions/pagination/pagination_params.rb +374 -0
  31. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
  32. data/lib/praxis/handlers/json.rb +2 -0
  33. data/lib/praxis/handlers/www_form.rb +5 -0
  34. data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
  35. data/lib/praxis/mapper/active_model_compat.rb +23 -5
  36. data/lib/praxis/mapper/resource.rb +16 -9
  37. data/lib/praxis/mapper/sequel_compat.rb +1 -0
  38. data/lib/praxis/media_type.rb +1 -56
  39. data/lib/praxis/plugins/mapper_plugin.rb +1 -1
  40. data/lib/praxis/plugins/pagination_plugin.rb +71 -0
  41. data/lib/praxis/resource_definition.rb +4 -12
  42. data/lib/praxis/route.rb +2 -4
  43. data/lib/praxis/routing_config.rb +4 -8
  44. data/lib/praxis/tasks/routes.rb +9 -14
  45. data/lib/praxis/validation_handler.rb +1 -2
  46. data/lib/praxis/version.rb +1 -1
  47. data/praxis.gemspec +2 -3
  48. data/spec/functional_spec.rb +9 -6
  49. data/spec/praxis/action_definition_spec.rb +4 -16
  50. data/spec/praxis/api_general_info_spec.rb +6 -6
  51. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
  52. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
  53. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
  54. data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
  55. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
  56. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
  57. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
  58. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
  59. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
  60. data/spec/praxis/media_type_spec.rb +5 -129
  61. data/spec/praxis/request_spec.rb +3 -22
  62. data/spec/praxis/resource_definition_spec.rb +1 -1
  63. data/spec/praxis/response_definition_spec.rb +1 -5
  64. data/spec/praxis/route_spec.rb +2 -9
  65. data/spec/praxis/routing_config_spec.rb +4 -13
  66. data/spec/praxis/types/multipart_array_spec.rb +4 -21
  67. data/spec/spec_app/config/environment.rb +0 -2
  68. data/spec/spec_app/design/api.rb +1 -1
  69. data/spec/spec_app/design/media_types/instance.rb +0 -8
  70. data/spec/spec_app/design/media_types/volume.rb +0 -12
  71. data/spec/spec_app/design/resources/instances.rb +1 -2
  72. data/spec/spec_helper.rb +6 -0
  73. data/spec/support/spec_media_types.rb +0 -73
  74. metadata +35 -45
  75. data/spec/praxis/handlers/xml_spec.rb +0 -177
  76. data/spec/praxis/links_spec.rb +0 -68
@@ -50,7 +50,6 @@ module Praxis
50
50
  autoload :RestfulDocGenerator, 'praxis/restful_doc_generator'
51
51
  module Docs
52
52
  autoload :Generator, 'praxis/docs/generator'
53
- autoload :LinkBuilder, 'praxis/docs/link_builder'
54
53
  autoload :OpenApiGenerator, 'praxis/docs/open_api_generator'
55
54
  end
56
55
 
@@ -61,7 +60,6 @@ module Praxis
61
60
  autoload :MultipartArray, 'praxis/types/multipart_array'
62
61
  end
63
62
 
64
- autoload :Links, 'praxis/links'
65
63
  autoload :MediaType, 'praxis/media_type'
66
64
  autoload :MediaTypeIdentifier, 'praxis/media_type_identifier'
67
65
  autoload :Multipart, 'praxis/types/multipart'
@@ -89,15 +87,19 @@ module Praxis
89
87
  autoload :MapperSelectors, 'praxis/extensions/mapper_selectors'
90
88
  autoload :Rendering, 'praxis/extensions/rendering'
91
89
  autoload :FieldExpansion, 'praxis/extensions/field_expansion'
90
+ autoload :AttributeFiltering, 'praxis/extensions/attribute_filtering'
92
91
  autoload :ActiveRecordFilterQueryBuilder, 'praxis/extensions/attribute_filtering/active_record_filter_query_builder'
93
92
  autoload :SequelFilterQueryBuilder, 'praxis/extensions/attribute_filtering/sequel_filter_query_builder'
93
+ autoload :Pagination, 'praxis/extensions/pagination'
94
+ module Pagination
95
+ autoload :ActiveRecordPaginationHandler, 'praxis/extensions/pagination/active_record_pagination_handler'
96
+ autoload :SequelPaginationHandler, 'praxis/extensions/pagination/sequel_pagination_handler'
97
+ end
94
98
  end
95
99
 
96
100
  module Handlers
97
101
  autoload :Plain, 'praxis/handlers/plain'
98
102
  autoload :JSON, 'praxis/handlers/json'
99
- autoload :WWWForm, 'praxis/handlers/www_form'
100
- autoload :XML, 'praxis/handlers/xml'
101
103
  end
102
104
 
103
105
  module BootloaderStages
@@ -12,9 +12,8 @@ module Praxis
12
12
 
13
13
  attr_reader :name
14
14
  attr_reader :resource_definition
15
- attr_reader :routes
16
- attr_reader :primary_route
17
- attr_reader :named_routes
15
+ attr_reader :api_definition
16
+ attr_reader :route
18
17
  attr_reader :responses
19
18
  attr_reader :traits
20
19
 
@@ -37,7 +36,7 @@ module Praxis
37
36
  @resource_definition = resource_definition
38
37
  @responses = Hash.new
39
38
  @metadata = Hash.new
40
- @routes = []
39
+ @route = nil
41
40
  @traits = []
42
41
 
43
42
  if (media_type = resource_definition.media_type)
@@ -179,12 +178,7 @@ module Praxis
179
178
  def routing(&block)
180
179
  @routing_config.instance_eval &block
181
180
 
182
- @routes = @routing_config.routes
183
- @primary_route = @routing_config.routes.first
184
- @named_routes = @routing_config.routes.each_with_object({}) do |route, hash|
185
- next if route.name.nil?
186
- hash[route.name] = route
187
- end
181
+ @route = @routing_config.route
188
182
  end
189
183
 
190
184
 
@@ -232,9 +226,8 @@ module Praxis
232
226
  end
233
227
  hash[:traits] = traits if traits.any?
234
228
  # FIXME: change to :routes along with api browser
235
- hash[:urls] = routes.collect do |route|
236
- ActionDefinition.url_description(route: route, params: self.params, params_example: params_example)
237
- end.compact
229
+ # FIXME: change urls to url ... (along with the browser)
230
+ hash[:urls] = [ ActionDefinition.url_description(route: route, params: self.params, params_example: params_example) ]
238
231
  self.class.doc_decorations.each do |callback|
239
232
  callback.call(self, hash)
240
233
  end
@@ -252,10 +245,10 @@ module Praxis
252
245
 
253
246
  def params_description(example:)
254
247
  route_params = []
255
- if primary_route.nil?
256
- warn "Warning: No routes defined for #{resource_definition.name}##{name}."
248
+ if route.nil?
249
+ warn "Warning: No route defined for #{resource_definition.name}##{name}."
257
250
  else
258
- route_params = primary_route.path.
251
+ route_params = route.path.
259
252
  named_captures.
260
253
  keys.
261
254
  collect(&:to_sym)
@@ -61,8 +61,7 @@ module Praxis
61
61
 
62
62
  builtin_handlers = {
63
63
  'plain' => Praxis::Handlers::Plain,
64
- 'json' => Praxis::Handlers::JSON,
65
- 'x-www-form-urlencoded' => Praxis::Handlers::WWWForm
64
+ 'json' => Praxis::Handlers::JSON
66
65
  }
67
66
  # Register built-in handlers unless the app already provided its own
68
67
  builtin_handlers.each_pair do |name, handler|
@@ -23,10 +23,8 @@ module Praxis
23
23
  def execute
24
24
  application.controllers.each do |controller|
25
25
  controller.definition.actions.each do |action_name, action|
26
- action.routes.each do |route|
27
- target = target_factory(controller, action_name)
28
- application.router.add_route target, route
29
- end
26
+ target = target_factory(controller, action_name)
27
+ application.router.add_route target, action.route
30
28
  end
31
29
  end
32
30
  end
@@ -0,0 +1,2 @@
1
+ require 'praxis/extensions/attribute_filtering/filtering_params'
2
+ require 'praxis/extensions/attribute_filtering/filter_tree_node'
@@ -1,178 +1,169 @@
1
+
2
+
1
3
  module Praxis
2
4
  module Extensions
3
- class ActiveRecordFilterQueryBuilder
4
- attr_reader :query, :table, :model
5
-
6
- # Abstract class, which needs to be used by subclassing it through the .for method, to set the mapping of attributes
7
- class << self
8
- def for(definition)
9
- Class.new(self) do
10
- @attr_to_column = case definition
11
- when Hash
12
- definition
13
- when Array
14
- definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
15
- else
16
- raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
17
- end
18
- class << self
19
- attr_reader :attr_to_column
20
- end
21
- end
5
+ module AttributeFiltering
6
+ ALIAS_TABLE_PREFIX = ''
7
+ require_relative 'active_record_patches'
8
+
9
+ class ActiveRecordFilterQueryBuilder
10
+ attr_reader :query, :model, :attr_to_column
11
+
12
+ # Base query to build upon
13
+ def initialize(query: , model:, filters_map:, debug: false)
14
+ @query = query
15
+ @model = model
16
+ @attr_to_column = filters_map
17
+ @logger = debug ? Logger.new(STDOUT) : nil
18
+ end
19
+
20
+ def debug(msg)
21
+ @logger && @logger.puts(msg)
22
22
  end
23
- end
24
23
 
25
- # Base query to build upon
26
- def initialize(query: , model:)
27
- @query = query
28
- @table = model.table_name
29
- @last_join_alias = model.table_name
30
- @alias_counter = 0;
31
- end
24
+ def generate(filters)
25
+ # Resolve the names and values first, based on filters_map
26
+ root_node = _convert_to_treenode(filters)
27
+ craft_filter_query(root_node, for_model: @model)
28
+ debug("SQL due to filters: #{@query.all.to_sql}")
29
+ @query
30
+ end
32
31
 
33
- def pick_alias( name )
34
- @alias_counter += 1
35
- "#{name}#{@alias_counter}"
36
- end
32
+ def craft_filter_query(nodetree, for_model:)
33
+ result = _compute_joins_and_conditions_data(nodetree, model: for_model)
34
+ @query = query.joins(result[:associations_hash]) unless result[:associations_hash].empty?
37
35
 
38
- def build_clause(filters)
39
- filters.each do |item|
40
- attr = item[:name]
41
- spec = item[:specs]
42
- column_name = attr_to_column[attr]
43
- raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
44
- if column_name.is_a?(Proc)
45
- bindings = column_name.call(spec)
46
- # A hash of bindings, consisting of a key with column name and a value to the query value
47
- bindings.each do|col,val|
48
- assoc_or_field, *rest = col.to_s.split('.')
49
- expand_binding(column_name: assoc_or_field, rest: rest, op: spec[:op], value: val, use_this_name_for_clause: @last_join_alias)
50
- end
51
- else
52
- assoc_or_field, *rest = column_name.to_s.split('.')
53
- expand_binding(column_name: assoc_or_field, rest: rest, **spec, use_this_name_for_clause: @last_join_alias)
36
+ result[:conditions].each do |condition|
37
+ filter_name = condition[:name]
38
+ filter_value = condition[:value]
39
+ column_prefix = condition[:column_prefix]
40
+
41
+ colo = condition[:model].columns_hash[filter_name.to_s]
42
+ add_clause(column_prefix: column_prefix, column_object: colo, op: condition[:op], value: filter_value)
54
43
  end
55
44
  end
56
- query
57
- end
58
45
 
59
- # TODO: Support more relationship types (including things like polymorphic..etc)
60
- def do_join(query, assoc , source_alias, table_alias)
61
- reflection = query.reflections[assoc.to_s]
62
- do_join_reflection( query, reflection, source_alias, table_alias )
63
- end
46
+ private
64
47
 
65
- def do_join_reflection( query, reflection, source_alias, table_alias )
66
- c = query.connection
67
- case reflection
68
- when ActiveRecord::Reflection::BelongsToReflection
69
- join_clause = "INNER JOIN %s as %s ON %s.%s = %s.%s " % \
70
- [c.quote_table_name(reflection.klass.table_name),
71
- c.quote_table_name(table_alias),
72
- c.quote_table_name(table_alias),
73
- c.quote_column_name(reflection.association_primary_key),
74
- c.quote_table_name(source_alias),
75
- c.quote_column_name(reflection.association_foreign_key)
76
- ]
77
- query.joins(join_clause)
78
- when ActiveRecord::Reflection::HasManyReflection
79
- # join_clause = "INNER JOIN #{reflection.klass.table_name} as #{table_alias} ON" + \
80
- # " \"#{source_alias}\".\"id\" = \"#{table_alias}\".\"#{reflection.foreign_key}\" "
81
- join_clause = "INNER JOIN %s as %s ON %s.%s = %s.%s " % \
82
- [c.quote_table_name(reflection.klass.table_name),
83
- c.quote_table_name(table_alias),
84
- c.quote_table_name(source_alias),
85
- c.quote_column_name(reflection.active_record.primary_key),
86
- c.quote_table_name(table_alias),
87
- c.quote_column_name(reflection.foreign_key)
88
- ]
89
-
90
- if reflection.type # && reflection.options[:as]....
91
- # addition = " AND \"#{table_alias}\".\"#{reflection.type}\" = \'#{reflection.active_record.class_name}\'"
92
- addition = " AND %s.%s = %s" % \
93
- [ c.quote_table_name(table_alias),
94
- c.quote_table_name(reflection.type),
95
- c.quote(reflection.active_record.class_name)]
96
-
97
- join_clause += addition
48
+ # Resolve and convert from filters, to a more manageable and param-type-independent structure
49
+ def _convert_to_treenode(filters)
50
+ # Resolve the names and values first, based on filters_map
51
+ resolved_array = []
52
+ filters.parsed_array.each do |filter|
53
+ mapped_value = attr_to_column[filter[:name]]
54
+ raise "Filtering by #{filter[:name]} not allowed (no mapping found)" unless mapped_value
55
+ bindings_array = \
56
+ if mapped_value.is_a?(Proc)
57
+ result = mapped_value.call(filter)
58
+ # Result could be an array of hashes (each hash has name/op/value to identify a condition)
59
+ result.is_a?(Array) ? result : [result]
60
+ else
61
+ # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
62
+ [filter.merge( name: mapped_value)]
63
+ end
64
+ resolved_array = resolved_array + bindings_array
98
65
  end
99
- query.joins(join_clause)
100
- when ActiveRecord::Reflection::ThroughReflection
101
- #puts "TODO: choose different alias (based on matching table type...)"
102
- talias = pick_alias(reflection.through_reflection.table_name)
103
- salias = source_alias
104
-
105
- query = do_join_reflection(query, reflection.through_reflection, salias, talias)
106
- #puts "TODO: choose different alias ?????????"
107
- salias = talias
108
-
109
- through_model = reflection.through_reflection.klass
110
- through_assoc = reflection.name
111
- final_reflection = reflection.source_reflection
112
-
113
- do_join_reflection(query, final_reflection, salias, table_alias)
114
- else
115
- raise "Joins for this association type are currently UNSUPPORTED: #{reflection.inspect}"
66
+ FilterTreeNode.new(resolved_array, path: [ALIAS_TABLE_PREFIX])
116
67
  end
117
- end
118
68
 
119
- def expand_binding(column_name:,rest: , op:,value:, use_this_name_for_clause: column_name)
120
- unless rest.empty?
121
- joined_alias = pick_alias(column_name)
122
- @query = do_join(query, column_name, @last_join_alias, joined_alias)
123
- saved_join_alias = @last_join_alias
124
- @last_join_alias = joined_alias
125
- new_column_name, *new_rest = rest
126
- expand_binding(column_name: new_column_name, rest: new_rest, op: op, value: value, use_this_name_for_clause: joined_alias)
127
- @last_join_alias = saved_join_alias
128
- else
129
- column_name = "#{use_this_name_for_clause}.#{column_name}"
130
- add_clause(column_name: column_name, op: op, value: value)
69
+ # Calculate join tree and conditions array for the nodetree object and its children
70
+ def _compute_joins_and_conditions_data(nodetree, model:)
71
+ h = {}
72
+ conditions = []
73
+ nodetree.children.each do |name, child|
74
+ child_model = model.reflections[name.to_s].klass
75
+ result = _compute_joins_and_conditions_data(child, model: child_model)
76
+ h[name] = result[:associations_hash]
77
+ conditions += result[:conditions]
78
+ end
79
+ column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join('/')
80
+ #column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? nil : nodetree.path.join('/')
81
+ nodetree.conditions.each do |condition|
82
+ conditions += [condition.merge(column_prefix: column_prefix, model: model)]
83
+ end
84
+ {associations_hash: h, conditions: conditions}
131
85
  end
132
- end
133
-
134
- def attr_to_column
135
- # Class method defined by the subclassing Class (using .for)
136
- self.class.attr_to_column
137
- end
138
86
 
139
- # Private to try to funnel all column names through `build_clause` that restricts
140
- # the attribute names better (to allow more difficult SQL injections )
141
- private def add_clause(column_name:, op:, value:)
142
- likeval = get_like_value(value)
143
- @query = case op
144
- when '='
145
- if likeval
146
- query.where("#{column_name} LIKE ?", likeval)
147
- else
148
- query.where(column_name => value)
149
- end
150
- when '!='
151
- if likeval
152
- query.where("#{column_name} NOT LIKE ?", likeval)
87
+ def add_clause(column_prefix:, column_object:, op:, value:)
88
+ @query = @query.references(column_prefix) #Mark where clause referencing the appropriate alias
89
+ likeval = get_like_value(value)
90
+ case op
91
+ when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
92
+ op = '!='
93
+ value = nil # Enforce it is indeed nil (should be)
94
+ when '!!'
95
+ op = '='
96
+ value = nil # Enforce it is indeed nil (should be)
97
+ end
98
+ @query = case op
99
+ when '='
100
+ if likeval
101
+ add_safe_where(tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
102
+ else
103
+ quoted_right = quote_right_part(value: value, column_object: column_object, negative: false)
104
+ query.where("#{quote_column_path(column_prefix, column_object)} #{quoted_right}")
105
+ end
106
+ when '!='
107
+ if likeval
108
+ add_safe_where(tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
109
+ else
110
+ quoted_right = quote_right_part(value: value, column_object: column_object, negative: true)
111
+ query.where("#{quote_column_path(column_prefix, column_object)} #{quoted_right}")
112
+ end
113
+ when '>'
114
+ add_safe_where(tab: column_prefix, col: column_object, op: '>', value: value)
115
+ when '<'
116
+ add_safe_where(tab: column_prefix, col: column_object, op: '<', value: value)
117
+ when '>='
118
+ add_safe_where(tab: column_prefix, col: column_object, op: '>=', value: value)
119
+ when '<='
120
+ add_safe_where(tab: column_prefix, col: column_object, op: '<=', value: value)
153
121
  else
154
- query.where.not(column_name => value)
122
+ raise "Unsupported Operator!!! #{op}"
155
123
  end
156
- when '>'
157
- query.where("#{column_name} > ?", value)
158
- when '<'
159
- query.where("#{column_name} < ?", value)
160
- when '>='
161
- query.where("#{column_name} >= ?", value)
162
- when '<='
163
- query.where("#{column_name} <= ?", value)
164
- else
165
- raise "Unsupported Operator!!! #{op}"
166
- end
167
- end
124
+ end
125
+
126
+ def add_safe_where(tab:, col:, op:, value:)
127
+ quoted_value = query.connection.quote_default_expression(value,col)
128
+ query.where("#{quote_column_path(tab, col)} #{op} #{quoted_value}")
129
+ end
168
130
 
169
- # Returns nil if the value was not a fuzzzy pattern
170
- def get_like_value(value)
171
- if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
172
- likeval = value.dup
173
- likeval[-1] = '%' if value[-1] == '*'
174
- likeval[0] = '%' if value[0] == '*'
175
- likeval
131
+ def quote_column_path(prefix, column_object)
132
+ c = query.connection
133
+ quoted_column = c.quote_column_name(column_object.name)
134
+ if prefix
135
+ quoted_table = c.quote_table_name(prefix)
136
+ "#{quoted_table}.#{quoted_column}"
137
+ else
138
+ quoted_column
139
+ end
140
+ end
141
+
142
+ def quote_right_part(value:, column_object:, negative:)
143
+ conn = query.connection
144
+ if value.nil?
145
+ no = negative ? ' NOT' : ''
146
+ "IS#{no} #{conn.quote_default_expression(value,column_object)}"
147
+ elsif value.is_a?(Array)
148
+ no = negative ? 'NOT ' : ''
149
+ list = value.map{|v| conn.quote_default_expression(v,column_object)}
150
+ "#{no}IN (#{list.join(',')})"
151
+ elsif value && value.is_a?(Range)
152
+ raise "TODO!"
153
+ else
154
+ op = negative ? '<>' : '='
155
+ "#{op} #{conn.quote_default_expression(value,column_object)}"
156
+ end
157
+ end
158
+
159
+ # Returns nil if the value was not a fuzzzy pattern
160
+ def get_like_value(value)
161
+ if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
162
+ likeval = value.dup
163
+ likeval[-1] = '%' if value[-1] == '*'
164
+ likeval[0] = '%' if value[0] == '*'
165
+ likeval
166
+ end
176
167
  end
177
168
  end
178
169
  end