HANA Bind JS Array as Parameter Value to WHERE IN(...) Clause - hana

I'm using the #sap/hana-client npm module in a NodeJS project to connect to a HANA database and run queries.
I have a list of IDs that I want to include in a WHERE ID IN(...) SQL clause via parameterized queries, but cannot seem to figure out the syntax to do it.
Here's what I imagine it would look like (but this does not work, fails at the parameter binding stage)
const ids = [1,2,3,4];
const params = [ids];
const sql = "SELECT * FROM T WHERE ID IN (?)";
// this fails with => code: -20007, message: 'Can not bind parameter(0).', sqlState: 'HY000'
conn.query(sql, params, (err, result) => {
// process query results or errors
});
I know that in Postgres I can do this by using the UNNEST(...) 1 array function, but the same does not seem to work in HANA

That's a well-known difficulty with HANA.
ARRAY-like types are not natively supported in the client software.
Your (special) case of this, namely turning an array into a list of parameters for an IN clause requires some additional efforts.
See e.g. Errors with declared array/table variable values in SAP HanaDB SQL
The bottom line is that Postgres handles this special case specifically by replacing the single IN-clause parameter ? with a whole list of delimited values.
HANA does (sadly) not do something like that.
Instead, if you have to know in advance how many elements (at max) the IN-list will have so that you can prepare a statement with a parameter ? for each of those elements.
Alternatively, you can use SQLScript and the UNNEST construct that I've shown in the linked question, or you can create a temporary table, fill it with the IN-list elements and use it in the IN-clause (or join it).
Either way, it's rather cumbersome to manually do this, and I'd probably look for a framework that does that sort of stuff.

Related

Is this parameterized query open to SQL injection?

Someone looking at my code said that the SQL query below (SELECT * FROM...) was obviously open to attack. I have researched this and it seems I'm doing this correctly by using a parameterized query, but clearly I'm missing something.
app.get("/api/v1/:userId", async (req, res) => {
try {
const teammate = await db.query("SELECT * FROM teammates WHERE uid = $1", [
req.params.userId,
]);
This query is not open to SQL injection, because it uses a parameterized query. The data is not substituted for the parameter ($1), but sent separately in a “bind” message, so no matter what the data contain, it is not interpreted as part of the SQL statement.
Moreover, it looks like the argument is an integer, and SQL injection can only happen with string arguments.
Someone at that company may have assumed that the $1 was going to be used for string interpolation, instead of a SQL query parameter.
They might not be aware that PostgreSQL uses the dollar-sign sigil for a query parameter placeholder. Other databases like MySQL use ? for a placeholder, and Oracle uses a : sigil in front of a named parameter.
You wrote the code correctly. See a similar example for node.js and PostgreSQL here: https://node-postgres.com/features/queries The section on "Parameterized query" specifically shows that style of code as the defense against SQL injection.
You might be better off not getting that job, in retrospect.

Knex not properly escaping raw postgres queries

I am using Knex (with typescript) to try to query a postgres database. My database table products has a column name that I want to search through as the user types into a search box. For example, a query of just the letter 'p' should return all products with a name that contains a word that begins with 'p'. For this, I am using the ts_vector and ts_query functions. My query looks like this:
const query = ... // got from user input
const result = await knex(knex.raw('products'))
.whereRaw(`to_tsvector(name) ## to_tsquery('?:*')`, query)
.select('*')
When I run this query, I get the following error:
Unhandled error { error: select * from products where to_tsvector(name) ## to_tsquery('$1:*') - bind message supplies 1 parameters, but prepared statement "" requires 0
If I replace the whereRaw line with: .whereRaw(`to_tsvector(name) ## to_tsquery('p:*')`), it correctly runs the query, selecting products whose names contain words beginning with a P.
It seems like there is some conflict with the postgres syntax and knex's raw queries. I want to use a raw query over using `${query}:*` because I want my inputs to be sanitized and protected from SQL injection. How can I get Knex to properly escape this?
I have tried various combinations of quotes, slashes and colons, but none seem to work. Any help would be appreciated.
PostgreSQL doesn't process placeholders when they are inside quotes (and I am a little surprised that knex does).
You need to do the concatenation explicitly, either inside PostgreSQL:
.whereRaw(`to_tsvector(name) ## to_tsquery(? ||':*')`,query)
Or inside typescript:
.whereRaw(`to_tsvector(name) ## to_tsquery(?)`, query+":*")

SQL Parameters - where does expansion happens

I'm getting a little confused about using parameters with SQL queries, and seeing some things that I can't immediately explain, so I'm just after some background info at this point.
First, is there a standard format for parameter names in queries, or is this database/middleware dependent ? I've seen both this:-
DELETE * FROM #tablename
and...
DELETE * FROM :tablename
Second - where (typically) does the parameter replacement happen? Are parameters replaced/expanded before the query is sent to the database, or does the database receive params and query separately, and perform the expansion itself?
Just as background, I'm using the DevArt UniDAC toolkit from a C++Builder app to connect via ODBC to an Excel spreadsheet. I know this is almost pessimal in a few ways... (I'm trying to understand why a particular command works only when it doesn't use parameters)
With such data access libraries, like UniDAC or FireDAC, you can use macros. They allow you to use special markers (called macro) in the places of a SQL command, where parameter are disallowed. I dont know UniDAC API, but will provide a sample for FireDAC:
ADQuery1.SQL.Text := 'DELETE * FROM &tablename';
ADQuery1.MacroByName('tablename').AsRaw := 'MyTab';
ADQuery1.ExecSQL;
Second - where (typically) does the parameter replacement happen?
It doesn't. That's the whole point. Data elements in your query stay data items. Code elements stay code elements. The two never intersect, and thus there is never an opportunity for malicious data to be treated as code.
connect via ODBC to an Excel spreadsheet... I'm trying to understand why a particular command works only when it doesn't use parameters
Excel isn't really a database engine, but if it were, you still can't use a parameter for the name a table.
SQL parameters are sent to the database. The database performs the expansion itself. That allows the database to set up a query plan that will work for different values of the parameters.
Microsoft always uses #parname for parameters. Oracle uses :parname. Other databases are different.
No database I know of allows you to specify the table name as a parameter. You have to expand that client side, like:
command.CommandText = string.Format("DELETE FROM {0}", tableName);
P.S. A * is not allowed after a DELETE. After all, you can only delete whole rows, not a set of columns.

SSIS Variable SQL command for ADO.net source

While this seems like a basic problem, I've been ripping my hair out FOR DAYS trying to get an efficient solution to this.
I have a lookup table of values on a server that I read from and assemble into a string using a C# Script task. I write this string into a variable that I want to pass in as my WHERE parameters inside a large SQL query on a ADO.NET data source (from a different server which I only have read access to) in my data flow. For example, this string would just be something like
('Frank', 'John', 'Markus', 'Tom')
and I would append that as my WHERE clause.
I can't read from a variable directly for an ADO.NET data source AND I can't use the 'Expression' property to set my SQL either as my SQL query is over 4000 characters. I could use an Execute SQL Task to run my query, load the results into a recordset and I assume, then loop through the recordset but that's extremely inefficient.
What would be the best way to do this? My end goal is to put these results inside a table on the first server.
You could try to set up Script Component as source - variables and strings inside scripts can be longer than 4000 characters so you can fit your query inside.
Setup your component similar to this article: http://beyondrelational.com/modules/2/blogs/106/posts/11119/script-componentsource-part1.aspx
In this one you have example how to fetch data using ExecuteReader and put it to output of script component: http://beyondrelational.com/modules/2/blogs/106/posts/11124/ssis-script-component-as-source-adonet.aspx In this one you have instructions how to aquire connection properly: http://www.toadworld.com/platforms/sql-server/b/weblog/archive/2011/05/30/use-connections-properly-in-an-ssis-script-task
By joining this pieces of information you should be able to write your source Script Component which can fetch data using any length dynamically constructed query.
Good luck :)
You can do a simple select statement to return a list of values that will include ('Frank', 'John', 'Markus', 'Tom'). So your select would return :
Name
----------
Frank
John
Markus
Tom
Then, in SSIS, use a Merge Join Component (that will act as a INNER JOIN) instead of a where clause in your main query.
This is the cleaniest way to achieve what you want.

Calling a stored function (that returns an array of a user-defined type) in oracle across a database link

Normally, I call my function like so:
SELECT *
FROM TABLE(
package_name.function(parameters)
)
I'm trying to call this function across a database link. My intuition is that the following is the correct syntax, but I haven't gotten it to work:
SELECT *
FROM TABLE(
package_name.function#DBLINK(parameters)
)
> ORA-00904: "PACKAGE_NAME"."FUNCTION": invalid identifier
I've tried moving around the database link to no effect. I've tried putting it after the parameter list, after the last parenthesis, after the package name...I've also tried all of the above permutations including the schema name before the package name. I'm running out of ideas.
This is oracle 10g. I'm suspicious that the issue may be that the return type of the function is not defined in the schema in which I'm calling it, but I feel like I should be getting a different error if that were the case.
Thanks for your help!
What you're trying is the correct syntax as far as I know, but in any case it would not work due to the return type being user-defined, as you suspect.
Here's an example with a built-in pipelined function. Calling it locally works, of course:
SELECT * FROM TABLE(dbms_xplan.display_cursor('a',1,'ALL'));
Returns:
SQL_ID: a, child number: 1 cannot be found
Calling it over a database link:
SELECT * FROM TABLE(dbms_xplan.display_cursor#core('a',1,'ALL'));
fails with this error:
ORA-30626: function/procedure parameters of remote object types are not supported
Possibly you are getting the ORA-904 because the link goes to a specific schema that does not have access to the package. But in any case, this won't work, even if you define an identical type with the same name in your local schema, because they're still not the same type from Oracle's point of view.
You can of course query a view remotely, so if there is a well-defined set of possible parameters, you could create one view for each parameter combination and then query that, e.g.:
CREATE VIEW display_cursor_a_1_all AS
SELECT * FROM TABLE(dbms_xplan.display_cursor('a',1,'ALL'))
;
If the range of possible parameter values is too large, you could create a procedure that creates the needed view dynamically given any set of parameters. Then you have a two-step process every time you want to execute the query:
EXECUTE package.create_view#remote(parameters)
SELECT * FROM created_view#remote;
You have to then think about whether multiple sessions might call this in parallel and if so how to prevent them from stepping on each other.