How to use Oracle's JSON_VALUE function with a PreparedStatement - json

I am trying to run a SQL query using Oracle's json_value() function using a PreparedStatement.
Assume the following table setup:
drop table foo cascade constraints purge;
create table foo
(
id integer primary key,
payload clob,
constraint ensure_json check (payload IS JSON STRICT)
);
insert into foo values (1, '{"data": {"k1": 1, "k2": "foo"}}');
The following SQL query works fine:
select *
from foo
where json_value(payload, '$.data.k1') = '1'
and returns the expected row.
However, when I try to run this query using a PreparedStatement like in the the following piece of code:
String sql =
"select *\n" +
"from foo\n" +
"where json_value(payload, ?) = ?";
PreparedStatement pstmt = conection.prepareStatement(sql);
pstmt.setString(1, "$.data.k1");
pstmt.setString(2, "1");
ResultSet rs = pstmt.executeQuery();
(I removed all error checking from the example to keep it simple)
This results in:
java.sql.SQLException: ORA-40454: path expression not a literal
The culprit is passing the json path value (parameter index 1), the second parameter is no problem.
When I replace (only) the first parameter with a String constant json_value(payload, '$.data.k1') = ? the prepared statement works fine.
In a desperate attempt, I also tried including the single quotes in the parameter: pstmt.setString(1, "'$.data.k1'") but not surprisingly, Oracle wouldn't accept it either (same error message).
I also tried using json_value(payload, concat('$.', ?) ) and only passing "data.k1" as the parameter - same result.
So, the question is:
How can I pass a JSON path expression to Oracle's json_value function using a PreparedStatement parameter?
Any ideas? Is this a bug in the driver or in Oracle? (I couldn't find anything on My Oracle Support)
Or is this simply a case of "not implemented"?
Environment:
I am using Oracle 18.0
I tried the 18.3 and 19.3 version of the ojdbc10.jar driver together with OpenJDK 11.

It isn't the driver - you get the same thing with dynamic SQL:
declare
result foo%rowtype;
begin
execute immediate 'select *
from foo
where json_value(payload, :1) = :2'
into result using '$.data.k1', '1';
dbms_output.put_line(result.payload);
end;
/
ORA-40454: path expression not a literal
ORA-06512: at line 4
And it isn't really a bug, it's documented (emphasis added):
JSON_basic_path_expression
Use this clause to specify a SQL/JSON path expression. The function uses the path expression to evaluate expr and find a scalar JSON value that matches, or satisfies, the path expression. The path expression must be a text literal. See Oracle Database JSON Developer's Guide for the full semantics of JSON_basic_path_expression.
So you would have to embed the path literal, rather than bind it, unfortunately:
declare
result foo%rowtype;
begin
execute immediate 'select *
from foo
where json_value(payload, ''' || '$.data.k1' || ''') = :1'
into result using '1';
dbms_output.put_line(result.payload);
end;
/
1 rows affected
dbms_output:
{"data": {"k1": 1, "k2": "foo"}}
or for your JDBC example (keeping the path as a separate string as you presumably want that to be a variable really):
String sql =
"select *\n" +
"from foo\n" +
"where json_value(payload, '" + "$.data.k1" + "') = ?";
PreparedStatement pstmt = conection.prepareStatement(sql);
pstmt.setString(1, "1");
ResultSet rs = pstmt.executeQuery();
Which obviously isn't what you want to do*, but there doesn't seem to be an alternative. Other than turning your query into a function and passing the path variable in to that, but then the function would have to use dynamic SQL, so the effect is much the same - maybe easier to handle SQL injection concerns that way though.
* and I'm aware you know how to do this the embedded way, and know you want to use bind variables because that's the correct thing to do; I've spelled it out more than you need for other visitors *8-)

Related

Can I declare type of parameters for sql command? I cannot insert a boolean value, it is being considered a string

I am trying to insert into MySQL DB using Powershell, the input data is from a REST API call. I am using Prepare Statement approach to optimize the inserts, I am having issues while inserting values into a column (let my_col_bool ) which is of type Boolean (i.e tinyint(1)).
The input data received from REST API would assign values to $myVar1,$myVar3,$myVar3. The values assigned to $myVar3 would be "true / false", as I am adding these values to command parameter and Executing the query, may be it is considering these values as String instead of Boolean as I am having an Error.
Approach 1:
$oMYSQLCommand.CommandText = "INSERT INTO myTable VALUES(#my_col_string,#my_col_int,#my_col_bool)"
cmd.Prepare()
$oMYSQLCommand.Parameters.AddWithValue("#my_col_string", "")
$oMYSQLCommand.Parameters.AddWithValue("#my_col_int", "")
$oMYSQLCommand.Parameters.AddWithValue("#my_col_bool", "")
$oMYSQLCommand.Parameters("#my_col_string").Value = $myVar1
$oMYSQLCommand.Parameters("#my_col_int").Value = $myVar2
$oMYSQLCommand.Parameters("#my_col_bool").Value = $myVar3
$oMYSQLCommand.ExecuteNonQuery() /*Error: Exception calling "ExecuteNonQuery" with "0" argument(s): "Incorrect integer value: 'false' for column 'my_col_bool' at row 1" */
Approach 2:
$oMYSQLCommand.Parameters.Add("#my_col_bool",[System.Data]::$SqlDbType.TinyInt) /*Error: Unable to find type [System.Data] */
Approach 3:
$oMYSQLCommand.Parameters.Add("#my_col_bool",$SqlDbType.TinyInt) /*Error: Cannot find an overload for "Add" and the argument count: "2". */
Approach 4:
$param_var = New-Object MySql.Data.MySqlClient.MySqlParameter("#my_col_bool",$SqlDbType.TinyInt)
$oMYSQLCommand.Parameters.Add($param_var) | Out-Null
$oMYSQLCommand.ExecuteNonQuery()/*Error: Exception calling "ExecuteNonQuery" with "0" argument(s): "Incorrect integer value: 'false' for column 'my_col_bool' at row 1" */
Every .Net driver tries to have exact same interface as MS SQL Connector has.
MS SQL Example:
$sqlCmd = [System.Data.SqlClient.SqlCommand]::new()
[void]$sqlCmd.Parameters.AddWithValue('#param1', [System.Data.SqlTypes.SqlInt16]::new(22))
[void]$sqlCmd.Parameters.Add('#param2', [System.Data.SqlTypes.SqlInt16])
$sqlCmd.Parameters['#param2'].Value = [System.Data.SqlTypes.SqlInt16]::new(22)
Reference: System.Data.SqlTypes
So usually, the methods are same, you just have to use different namespace inside [].
Note that some .Net providers use SQL type system, and some use own type system, which is usually at [VendorName.something] namespace.
For example, MySQL seems to use [MySql.Data.MySqlClient.MySqlDbType]::%typeName% namespace,
Here is update on my approach 3, to make it work $myVar1 should be bool variable
$oMYSQLCommand.Parameters.Add("#my_col_bool",$SqlDbType.TinyInt)
[bool]$myVar3_bool=[boolean]::parse($myVar1)
$oMYSQLCommand.Parameters("#my_col_bool").Value = $myVar3_bool

mysql query using python 3.6 (string variable is in single quotes)

I am new in python as well as mysql. I am having trouble in populating proper query statement for mysql.
sql = "SELECT * FROM Persons WHERE %s"
cur = db.cursor()
cur.execute(sql,(where,))
where is a string variable which creates a string for WHERE clause; this is the point of question. When I print this variable it give the following result:
Gender = True And IsLate = False
(without any quotes) but when I add this variable to the query to execute it, it adds single quotes around the string.
I used the command
print(cur.statement)
and it prints:
SELECT * FROM Persons WHERE 'Gender = True And IsLate = False'
After supplying parameter, it puts it within single quotes and query returns 0 rows.
I have worked around by concatenating the query statement and variable together and execute the string as query, that worked,
sql = sql + where
cur.execute(sql)
But I know that is not the professional way, as I have searched and found the professional way is to use parameterized query and use variable to store the condition(s) and supplying it at the execution of query.
Looking for advice, am I thinking the right way or otherwise?
The whole point of using parameter substitution in cursor.execute() is that it protects you from SQL injection. Each parameter is treated as a literal value, not substituted into the query and re-interpreted.
If you really want it to be interprted, you need to use string formatting or concatenation, as you discovered. But then you will have to be very careful in validating the input, because the user can supply extra SQL code that you may not have expected, and cause the query to malfunction.
What you should do is build the where string and parameter list dynamically.
where = []
params = []
if gender_supplied:
where.append('gender = %s')
params.append(gender)
if islate_supplied:
where.append*('islate = %s')
params.append(islate)
sql = 'select * from persons'
if where:
query = sql + ' where ' + ' and '.join(where)
else:
query = sql
cur.execute(query, params)

weird escape behaviour when writing string from node to mysql db

I'm on node and want to write this in my mysql db:
var x = JSON.stringify(['aa"a']);
console.log(x);
mysqlConnection.query("UPDATE `table` SET field = '" + x + "' WHERE id = 1");
The console.log() produces: ["aa\"a"]
When I read the string from the db later, I get: ["aa"a"]
The backslash is missing, making the string useless, as calling JSON.parse() would produce an error.
You're mashing your SQL together as a string. \ is an escape character (in SQL as well as JSON), so it escapes the " when passed to the SQL engine.
Use placeholders (whichever MySQL API library you are using should have a way of using them) instead of manually shoving variables into the string of SQL.

Quotes around dynamic expression Groovy SQL

I am doing a query to my database using Groovy, the query is working perfectly and bringing back the correct data however I get this error in my terminal.
In Groovy SQL please do not use quotes around dynamic expressions
(which start with $) as this means we cannot use a JDBC
PreparedStatement and so is a security hole. Groovy has worked around
your mistake but the security hole is still there.
Here is my query
sql.firstRow("""select elem
from site_content,
lateral jsonb_array_elements(content->'playersContainer'->'series') elem
where elem #> '{"id": "${id}"}'
""")
If I change it to just $id or
sql.firstRow("""select elem
from site_content,
lateral jsonb_array_elements(content->'playersContainer'->'series') elem
where elem #> '{"id": ?}'
""", id)
I get the following error
org.postgresql.util.PSQLException: The column index is out of range:
1, number of columns: 0.
Positional or named parameters are handled by groovy sql properly and should be used instead of "'$id'".
As #Opal mentioned and as described here, you should be passing your params either as a list or map:
sql.execute "select * from tbl where a=? and b=?", [ 'aa', 'bb' ]
sql.execute "select * from tbl where a=:first and b=:last", first: 'aa', last: 'bb'

How to delete multiple rows (using named parameters) in Adobe AIR

I am trying to delete multiple rows in the sqlite table in my Adobe AIR app (runtime 2.5).
Here is the statement, using the "IN" operator:
"DELETE FROM mylist WHERE tdId IN (tdId1, tdId2, tdId3, ...)";
Where tdId1, tdId2, etc. will be determined at runtime based on which row(s) the user chooses to delete. The user can delete arbitrary number of rows.
I've tried something like:
//delete statement text
"DELETE FROM mylist WHERE tdId IN :tdId";
//delete statement parameters: take 1.
//Got "argument error: near ':tdId': syntax error"
deleteStmt.parameters[":tdId"] = "(26, 32)";
//delete statement parameters: take 2.
//Also got "argument error: near ':tdId': syntax error"
var arr:Array = [26, 32];
deleteStmt.parameters[":tdId"] = arr;
How I go about deleting multiple rows?
[Edit] So it looks like the aforementioned cached statement with parameter [":tdId"] doesn't work when deleting multiple rows. When attempting to execute the delete statement multiple times in asynchronous mode, after the very first row in the queue is deleted, Flash throws the following error:
"Error #3110: Operation cannot be
performed while SQLStatement.executing
is true."
It would seem too much of trouble to chain these deletes with a callback. So I guess I am using my last resort: building the sql at runtime. Conclusion: Cached statements can't be used in these kind of situations...
The problem occurs when you insert the parameter "(26,32)". As the parameter is not purely a substitution of value, it represents a variable to SQL, NOT A STRING. Hence you statement effectively became (or roughly) in your first take...
"DELETE FROM mylist WHERE tdId IN '(26,32)'"
Hence your error, due to the syntax... In your second take it gets worse...
"DELETE FROM mylist WHERE tdId IN *Array(26,32)*"
As the variable does not convert to a string value, this does not actually happen. But what happens is that when the interprater (SQL) tries to understand the code after the 'IN' text, it gets an ARRAY object, which it has completely no idea on what to do... Its not even a valid SQL type....
Solution? [I yet to fully test it, so please do]
var toDel:Array = [26,32]
//delete statement text
var baseStr:String = "DELETE FROM mylist WHERE tdId IN (";
var midStr:String = '';
//delete statement parameters: Processing parameter
for( var i = 0; i < toDel.length; i++ ) {
deleteStmt.parameters[i] = toDel[i];
if(midStr.length > 0) { midStr += ' , '; }
midStr += '?';
}
deleteStmt.text = baseStr + midStr + ' )';
//Then execute
So what happens in this case is that u effectively execute...
"DELETE FROM mylist WHERE tdId IN ( :val1 , :val2 )"
In this way you still maintain the safe (good practice) use of parameters, without converting everything to a string.
EDIT: if you dun understand the use of parameter / '?' refer to :
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/data/SQLStatement.html#parameters
If the IN clause does not allow parameters, you can try old-school SQL style: append multiple
" OR (tdId = :param" + paramCounter.toString() + ")"
to the SQL string