1

I have a table called Coupon.

This table has a column called query which holds a string.

The query string has some logical conditions in it formatted for a where statement. For example:

coupon1.query
=> " '/hats' = :url "
coupon2.query
=> " '/pants' = :url OR '/shoes' = :url "

I want to write a stored procedure that takes as input 2 parameters: a list of Coupon ids and a variable (in this example, the current URL).

I want the procedure to look up the value of the query column from each Coupon. Then it should run that string in a where statement, plugging in my other parameter (current url), then return any Coupon ids that matches.

Here's how I would expect the procedure to behave given the two coupons above.

Example 1: 
* Call procedure with ids for coupon1 and coupon2, with @url = '/hats'
* Expect coupon1 to be returned.

Example 2: 
* Call procedure with ids for coupon1 and coupon2, with @url = '/pants'
* Expect coupon2 to be returned.

Example 3: 
* Call procedure with ids for coupon1 and coupon2, with @url = '/shirts'
* Expect no ids returned. URL does not match '/hats' for coupon1, and doesn't match '/pants or /shoes' for coupon2. 

It's easy to test these out in ActiveRecord. Here is just example 1.

@url = '/hats'
@query = coupon1.query 
# "'/hats' = :url"
Coupon.where(@query, url: @url).count
=> 2   
# count is non-zero number because the query matches the url parameter. 
# Coupon1 passes, its id would be returned from the stored procedure.
'/hats' == '/hats'

@query = coupon2.query 
# " '/pants' = :url OR '/shoes' = :url "
Coupon.where(@query, url: @url).count
=> 0
# count is 0 because the query does not match the url parameter. 
# Coupon2 does not pass, its id would not be returned from the stored procedure.
'/pants' != '/hats', '/shoes' != '/hats'

You could write this as a loop (I'm in ruby on rails with activerecord) but I need something that performs better - I could potentially have lots of coupons so I can't just check each one directly with a loop. The queries contain complex AND/OR logic so I can't just compare against a list of urls either. But here's some code of a loop that is essentially what I'm trying to translate into a stored procedure.

# assume coupon1 has id 1, coupon2 has id 2
@coupons = [coupon1, coupon2]
@url = '/hats'
@coupons.map do |coupon|
    if Coupon.where(coupon.query, url: @url).count > 0
        coupon.id
    else
        nil
    end
end
=> [1, nil]
dthegnome
  • 43
  • 7

1 Answers1

0

Ok, I've been pondering this one.

Big picture:

A. You have a @url you want to search for to find a match among many potential Coupons

B. A coupon has a URL that might match @url

If that's the true extent of the problem, I think you've really over-complicated things.

coupon1.query
=> ["/hats"]
coupon2.query
=> ["/pants", "/shoes"]

@url = '/hats'

Coupon.where('FIND_IN_SET(:url, query) <> 0')

Or something similar, I'm not a mySQL user myself.

However, this is very possible to achieve and may even have a much better ActiveRecord way to do the query.


UPDATE

Ok, I'm missing something. I can't actually reproduce this in console.

@url = '/hats'
@query = coupon1.query 
# "'/hats' = :url"
Coupon.where(@query, url: @url).count
> SELECT * FROM 'coupons' WHERE ( '/hats' = '/hats' )

As you can see from the select statement, this will always return all records. It's the same as writing SELECT * FROM 'coupons' WHERE ( true )

How are you actually performing a valid query?

Sorry to post this in my answer, I wanted good formatting.

If I've got something wrong here, maybe we need to move this to a chat room.

I think you have just enough reputation for me to invite you to a room.


UPDATE2

Since you have to compare @query to each record individually, I think you'll have to loop.

But, I don't think you need to use Coupon.where to accomplish this since you are only comparing one record at a time.

@coupons.map do |coupon|
  # don't bother putting nil in the array
  next unless coupon.query == @url
  
  coupon.id
end

However, your original question was about performance when scaled, and you know you aren't going to solve that with a loop.

Maybe JSONB instead of String so that you could actually do some SQL.

But, even with JSONB, this is still complicated by wanting your conditions to be evaluated properly.

{
  "url": {
    "AND": ["/hats", "/shoes"],
    "OR": ["/pants"]
  },
  "logged_in": true,
  "is_gold_member": false
}

{
  "logged_in": false,
  "url": "/hats"
}

{
  "url": {
    "OR": ["/pants", "/shoes"]
  }
}

Ultimately, I think what you're doing with query attributes is going to continue to be your stumbling block. It's very clever, but it's not simple.

If it were my app, I think I would go back to considering my use case and try to find a different strategy to map specific coupons to specific parameters in a more on-the-rails way.

Chiperific
  • 4,428
  • 3
  • 21
  • 41
  • Hey, thanks for your time, but I'm afraid you have the big picture a little over simplified. I tried to just post the simplest possible use case, but the conditions being stored as the query on the coupon get much more complex than just looking for a match, otherwise your strategy would work just fine. All the conditions can be expressed in the form of a dynamic SQL query but it also isn't just comparing against the current URL. For example: ``` coupon1.query => "'/hats' = :url AND 'true' = :logged_in" ``` Each coupon has potentially multiple conditions with multiple variables. – dthegnome Aug 15 '22 at 19:41
  • If queries have a common structure with a finite number of possible conditions, would it be easier to just add more attributes to your class (columns to your table) and keep your queries more in line with normal Active Record capabilities? – Chiperific Aug 15 '22 at 19:45
  • Sorry I was trying to reformat my comment since the code didn't come through. That isn't possible because they don't have a common structure. I have an interface that builds the query dynamically and allows for complex AND/OR logic. – dthegnome Aug 15 '22 at 19:48
  • 1
    In plain english one coupon could have a simple rule like "the url has to equal /hats" but another coupon could have more rules such as "url must be equal to /pants, or /shoes, AND the user must be logged in" – dthegnome Aug 15 '22 at 19:50
  • All of these rules can be summed up in a dynamically constructed 'where'. But I don't see a way that it could be simplified with more table columns. – dthegnome Aug 15 '22 at 19:51
  • See my question via my updated answer. – Chiperific Aug 15 '22 at 22:49
  • This one is stuck in my head. I keep thinking this is different from meta programming in a way that seems overly complicated. Is this an [xy problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378)? – Chiperific Aug 16 '22 at 00:04
  • Hey just responding to your update but we could also chat. You're on the right track, but `@url` changes on any given request, and there could be other variables too. The query is a way of formatting true/false logic on a per coupon basis. We are either going to receive all records or no records. Think of it more like `Coupon.where(@query, url: @url).count == 0`. Try the same thing you just did in the console with `@url` set to `'/shoes'` - you get 0 records returned. For coupon1 we're comparing `@url` to `'/hats'`. But coupon2 might be looking for `'/shoes'`. – dthegnome Aug 16 '22 at 14:07
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247313/discussion-between-chiperific-and-dthegnome). – Chiperific Aug 16 '22 at 15:27