I want to create a PL/SQL function which is passed a table name and a condition, and which returns the number of rows than the condition meets on that table.
I creat
You cannot use bind variables for table or column names, or a complete condition. Those have to be known when the statement is parsed, which is before the bind variables are assigned - otherwise you'd lose one of he benefits of the bind variables. You can only bind the values of variables.
When your string is parsed the table name is interpreted literally as :TABLE_NAME
, and the colon makes that an invalid value for a table name. It is not using the value you passed in to the function.
So you need to concatenate the name and condition instead; your INTO
clause is also in the wrong place:
CREATE OR REPLACE FUNCTION CHECK_EXISTS
(TABLE_NAME VARCHAR2, CONDITION VARCHAR2) RETURN NUMBER AS
VAL NUMBER;
SQL_CODE VARCHAR2(200):='SELECT COUNT (*) FROM '
|| TABLE_NAME || ' WHERE ' || CONDITION;
BEGIN
EXECUTE IMMEDIATE SQL_CODE INTO VAL;
RETURN VAL;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 0;
END;
This might seem odd given all the advice you will see about using bind variables to avoid SQL injection. That still applies, you just cannot do it for the table name here.
But that does mean you are potentially open to SQL injection, so you should sanitise the inputs you get, which is reasonably straightforward for the table name - you can see if it exists in all_tables
, for example - but the variable condition will be harder to check. Luckily you can only execute a single statement with dynamic SQL so it would be hard to do anything too nasty, except as side-effects from something you can put in a valid condition.
If for some reason you really want to use bind variables for your execute immediate
call you could do something convoluted like:
CREATE OR REPLACE FUNCTION CHECK_EXISTS
(TABLE_NAME VARCHAR2, CONDITION VARCHAR2) RETURN NUMBER AS
VAL NUMBER;
SQL_CODE VARCHAR2(200):=q'[BEGIN EXECUTE IMMEDIATE 'SELECT COUNT (*) FROM ' || :TABLE_NAME || ' WHERE ' || :CONDITION INTO :VAL; END;]';
BEGIN
EXECUTE IMMEDIATE SQL_CODE USING TABLE_NAME, CONDITION, OUT VAL;
RETURN VAL;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 0;
END;
/
... which pushes the concatenation into an anonymous block; the execution then uses an OUT bind variable for the count result instead of an INTO
. I'm not sure there's much benefit in doing that though.
As @AvrajitRoy mentioned, since you're doing an aggregate count()
the query will always return a result (unless it errors from a non-existent table or malformed condition of course, which you'd want to know about), so the exception handler you have can never be reached. While it isn't really doing any harm, it can be removed:
CREATE OR REPLACE FUNCTION CHECK_EXISTS (TABLE_NAME VARCHAR2, CONDITION VARCHAR2)
RETURN NUMBER AS
VAL NUMBER;
SQL_CODE VARCHAR2(200):='SELECT COUNT (*) FROM '
|| TABLE_NAME || ' WHERE ' || CONDITION;
BEGIN
EXECUTE IMMEDIATE SQL_CODE INTO VAL;
RETURN VAL;
END;
As illustrated above perfectly by Alex just to add make the exception handling generic as NO_DATA_FOUND will not be raised when we are doing COUNT(*).
CREATE OR REPLACE FUNCTION CHECK_EXISTS(
TABLE_NAME VARCHAR2,
CONDITION VARCHAR2)
RETURN NUMBER
AS
VAL NUMBER;
SQL_CODE VARCHAR2(2000 CHAR):= 'SELECT COUNT (*) FROM '||TABLE_NAME||' WHERE '|| CONDITION;
BEGIN
EXECUTE IMMEDIATE SQL_CODE INTO val;
RETURN VAL;
EXCEPTION
WHEN OTHERS THEN --no data found may not get raised as we are doing count(*) here
RETURN 0;
END;