Search code examples
sqloracle-databaseplsqlexecute-immediate

Using UDT variable within an EXECUTE IMMEDIATE in a PL/SQL function


I am building a function on PL/SQL using Oracle 11g.

I am trying to use a table variable within an EXECUTE IMMEDIATE statement, but it is not working, as you can see:

ERROR at line 1:
ORA-00904: "CENTER_OBJECTS": invalid identifier
ORA-06512: at "HIGIIA.KNN_JOIN", line 18

The code I am using is...

First, the type definitions

CREATE TYPE join_t IS OBJECT (
   inn                          char(40),
   out                        char(40)
);
/


CREATE TYPE join_jt IS TABLE OF join_t;
/

CREATE TYPE blob_t IS OBJECT (
   id           CHAR(40),
   fv           BLOB
);
/

CREATE TYPE blob_tt IS TABLE OF blob_t;
/

The function is:

create or replace FUNCTION knn_join (tab_inn IN varchar2, tab_out IN varchar2, blob_col1 IN varchar2, blob_col2 IN varchar2, dist_alg in VARCHAR2, kv in NUMBER ) RETURN join_jt
IS
var_fv BLOB;
var_id CHAR(40);
center_objects blob_tt := blob_tt();
retval join_jt := join_jt ();
join_table join_jt := join_jt();
sql_stmt1 varchar2(400);
sql_stmt2 varchar2(400);
BEGIN
    sql_stmt1 := 'SELECT blob_t(ROWIDTOCHAR(rowid),' || blob_col1 || ') FROM ' || tab_out;
    sql_stmt2 := 'SELECT join_t(ROWIDTOCHAR(r.rowid), center_objects(idx).id) FROM ' || tab_inn || ' r  WHERE ' || dist_alg || '_knn(r.' || blob_col2 || ',  center_objects(idx).' ||   blob_col1 || ')<=' || kv;
    dbms_output.put_line(sql_stmt2);    
    EXECUTE IMMEDIATE sql_stmt1 BULK COLLECT INTO center_objects;
    for idx in center_objects.first()..center_objects.last()
                loop
                --SELECT join_t(ROWIDTOCHAR(r.rowid), center_objects(idx).id) BULK COLLECT INTO join_table FROM londonfv r WHERE manhattan_knn(r.fv, center_objects(idx).fv) <=5;
                EXECUTE IMMEDIATE sql_stmt2 BULK COLLECT INTO join_table;   
            for idx2 in join_table.first()..join_table.last()
                   loop
                            retval.extend();
                        retval(retval.count()) := join_table(idx2);
                       end loop;
            end loop;
RETURN retval;
END;
/

To run the function:

select * from TABLE(knn_join('london','cophirfv','fv','fv','manhattan',5)); 

I am trying to use run the statement 'SELECT join_t(ROWIDTOCHAR(r.rowid), center_objects(idx).id) BULK COLLECT INTO join_table FROM london r WHERE manhattan_knn(r.fv, center_objects(idx).fv) <=5' using the EXECUTE IMMEDIATE, but it does not work because I am using a variable in it.

Can someone give me a hand on it?

Thanks in advance!


Solution

  • You can't refer to a local PL/SQL variable inside a dynamic SQL statement, because it is out of scope within the SQL context used by the dynamic call. You could replace your first call:

    SELECT join_t(ROWIDTOCHAR(r.rowid), center_objects(idx).id) FROM ' ...
    

    with a bind variable:

    SELECT join_t(ROWIDTOCHAR(r.rowid), :id FROM ' ...
    EXECUTE IMMEDIATE ... USING center_objects(idx).id ...
    

    but you can't do what when the object attribute is variable too:

    ... ',  center_objects(idx).' ||   blob_col1 || ')<='...
    

    although - at least in the example you've shown - the only object attribute name available is fv, regardless of the table column names passed in to the function - so that could be hard-coded; and thus a bind variable could be used:

    ... ',  :fv)<='...
    EXECUTE IMMEDIATE ... USING center_objects(idx).id, center_objects(idx).fv ...
    

    and the kv value should also be a bind variable, so you'd end up with:

    create or replace FUNCTION knn_join (tab_inn IN varchar2, tab_out IN varchar2,
      blob_col1 IN varchar2, blob_col2 IN varchar2, dist_alg in VARCHAR2, kv in NUMBER )
    RETURN join_jt
    IS
      center_objects blob_tt := blob_tt();
      retval join_jt := join_jt ();
      join_table join_jt := join_jt();
      sql_stmt1 varchar2(400);
      sql_stmt2 varchar2(400);
    BEGIN
      sql_stmt1 := 'SELECT blob_t(ROWIDTOCHAR(rowid),' || blob_col1 || ') FROM ' || tab_out;
      sql_stmt2 := 'SELECT join_t(ROWIDTOCHAR(r.rowid), :id) FROM ' || tab_inn || ' r  WHERE '
        || dist_alg || '_knn(r.' || blob_col2 || ',  :fv)<= :kv';
      dbms_output.put_line(sql_stmt1);    
      dbms_output.put_line(sql_stmt2);    
      EXECUTE IMMEDIATE sql_stmt1 BULK COLLECT INTO center_objects;
      for idx in center_objects.first()..center_objects.last()
      loop
        EXECUTE IMMEDIATE sql_stmt2 BULK COLLECT INTO join_table
        USING center_objects(idx).id, center_objects(idx).fv, kv;   
        for idx2 in join_table.first()..join_table.last()
        loop
          retval.extend();
          retval(retval.count()) := join_table(idx2);
        end loop;
      end loop;
      RETURN retval;
    END;
    /
    

    As far as I can tell you could still do the join within the dynamic SQL statement, and eliminate the loops and the need for the intermediate center_objects and join_table collections:

    create or replace FUNCTION knn_join (tab_inn IN varchar2, tab_out IN varchar2,
      blob_col1 IN varchar2, blob_col2 IN varchar2, dist_alg in VARCHAR2, kv in NUMBER )
    RETURN join_jt
    IS
      retval join_jt;
      sql_stmt varchar2(400);
    BEGIN
      sql_stmt := 'SELECT join_t(ROWIDTOCHAR(tinn.rowid), ROWIDTOCHAR(tout.rowid))'
        || ' FROM ' || tab_inn || ' tinn JOIN ' || tab_out || ' tout'
        || ' ON ' || dist_alg || '_knn(tinn.fv, tout.fv) <= :kv';
    
      dbms_output.put_line(sql_stmt);
      EXECUTE IMMEDIATE sql_stmt BULK COLLECT INTO retval USING kv;
      RETURN retval;
    END;
    /
    

    When you call it as you've shown:

    select * from TABLE(knn_join('london','cophirfv','fv','fv','manhattan',5)); 
    

    that's the equivalent of the hard-coded:

    SELECT join_t(ROWIDTOCHAR(tinn.rowid), ROWIDTOCHAR(tout.rowid))
    FROM london tinn
    JOIN cophirfv tout
    ON manhattan_knn(tinn.fv, tout.fv) <= 5
    

    ... so I guess you can verify whether that hard-coded version gives you the results you expect first. (Adding sample data and expected results to the question would have helped, of course).

    That join condition may be expensive, depending on what the function is doing, how may rows are in each table (as every row in each table has to be compared with every row in the other), whether you actually have other filters, etc. The loop version would be even worse though. Without more information there isn't much to be done about that anyway.

    As an aside, using varchar2 instead of char for the object attributes would be more normal; that's also the data type returned by the rowidtochar() function.