Example: matching queries with a regular expression

Introduction

Let's assume that we have a request filter that needs to be invoked whenever a query for the purchase_orders table is received, because we want to do something with the notes column.

The first query we see is:

select id, customer_name, status, notes from demo.purchase_orders

So the obvious thing to do is an exact match. If that's the only query we want to match, great -- we're done.

But it's common to need more flexibility. Maybe the queries are more diverse, e.g.:

select id,notes from "demo"."purchase_orders"

select id,notes, status from [demo].[purchase_orders]

SELECT * FROM DEMO.PURCHASE_ORDERS WHERE STATUS <> 'S'

Matching all of these is possible using regular expressions

Note: a SQL parser is also an option

In this article, we're going to cover regular expressions, but there is another option. Regular expressions are fairly easy to put in place, but they are relatively imprecise. If you need to do a complete analysis of the SQL, you can use a SQL parser, as explained in this other example. It's more work, but it's also much more precise.

Basic regular expression

Regular expressions in Gallium Data start with the string "regex:". Any special characters they contain must be escaped to keep their original meaning.

We can turn the first query into a regular expression with:

regex:select id, customer_name, status, notes from demo\.purchase_orders

The only difference is the backslash before the period. That's because a period is a special character in regular expressions: it stands for any one character.

By turning this into a regular expression, we've already gained case independence: this regular expression will match any of the following:

select id, customer_name, status, notes from demo.purchase_orders

SELECT ID, CUSTOMER_NAME, STATUS, NOTES FROM DEMO.PURCHASE_ORDERS

Select ID, customer_name, status, notes from DEMO.Purchase_Orders

Sometimes that's not desirable -- if you want to be case-dependent, you can use the prefix REGEX: instead.

Matching columns

That's a good start, but we have a number of other queries we need to match. In particular, we don't really care which columns are selected, only whether or not they include the notes column. We can broaden our regular expression with:

regex:select.*\bnotes\b.*from demo\.purchase_orders

This will now match any of the following queries:

select id, customer_name, status, notes from demo.purchase_orders

select Notes from demo.purchase_orders

SELECT notes, status from DEMO.PUCHASE_ORDERS

But that's not enough: we also need to handle the * operator with:

regex:select\s+(\*|(.*\bnotes\b.*))\s+from demo\.purchase_orders

This will now also match:

select * from demo.purchase_orders

Note the expression at the beginning:

select\s+(\*|(.*\bnotes\b.*))\s+from

Let's break it down:

We then have an expression between parentheses:

(\*|(.*\bnotes\b.*))

which contains two parts separated by a vertical bar:

The vertical bar means "or", and the parentheses form a group, so this expression means either the first part or the second part.

The first part \* is escaped with a backslash because asterisk is a special character, therefore it simply means the asterisk character. It takes care of the asterisk in select * from...

The second part (.*\bnotes\b.*) is surrounded by parentheses, which group everything they contain, which is:

Matching the table

The table name may be expressed in a variety of ways: 

demo.purchase_orders

"demo.purchase_orders"

[demo].[purchase_orders]

"demo".[purchase_orders]

To match all of these, we need to specify:

["\[]?demo["\]]?\.["\[]?purchase_orders["\]]?

This surrounds the schema name and the table name with:

["\[]? which will match either a double quote, or an open square bracket (which needs to be escaped with a backslash), or nothing (because of the ? operator)

["\]]? which will match either a double quote, or a close square bracket (which needs to be escaped with a backslash), or nothing

Surroundings

So far we haven't taken into account the fact that the query could be more specific, e.g.:

select notes from [demo].[purchase_orders] where id > 3 or notes in ('S', 'T')

The easiest way to match this is to simply accept anything after the table name:

regex:select\s*(\*|(.*\bnotes\b.*))\s+from\s+["[]?demo["\]]?\.["[]?purchase_orders["\]]?.*

We can do the same at the beginning, in case there is anything in front of the query.

What if we don't trust the queries?

In some scenarios, you may not know exactly what the SQL queries look like, which means that they could be formulated in a way to evade the regular expressions.

For instance, if you wanted to catch any queries from the table Customers, you might think that a regular expression like:

regex:.*\bselect\b.*\bfrom\b.*\bcustomers\b.*

would 

Conclusion

Our final expression is:

regex:.*select\s*(\*|(.*\bnotes\b.*))\s+from\s+["[]?demo["\]]?\.["[]?purchase_orders["\]]?.*

which will match a wide variety of select queries.

Whenever you have a regular expression that contains wide open expressions like .*, keep in mind that it will match anything. The more specific the regular expression, the better.