Comme vous ne connaissez pas les colonnes qui seront renvoyées par la requête transmise au moment de la compilation, vous ne pouvez pas y faire référence de manière statique dans la boucle.
Vous pouvez utiliser le dbms_sql
package pour le faire dynamiquement :
CREATE OR REPLACE PROCEDURE p_create_text_file (
loc IN VARCHAR2
, file IN VARCHAR2
, select_statement in varchar2
, line_statement in varchar2 -- not used?
)
IS
fid UTL_FILE.FILE_TYPE := UTL_FILE.FOPEN (loc, file, 'W');
-- for dbms_sql
l_c pls_integer;
l_col_cnt pls_integer;
l_desc_t dbms_sql.desc_tab3;
l_rc pls_integer;
l_varchar varchar2(4000);
BEGIN
-- create cursor and prepare from passed-in statement
l_c := dbms_sql.open_cursor;
dbms_sql.parse(c=>l_c, statement=>select_statement,
language_flag=>dbms_sql.native);
dbms_sql.describe_columns3(c => l_c, col_cnt => l_col_cnt,
desc_t => l_desc_t);
-- define all columns as strings; this will end up with implicit conversion
-- of dates etc. using NLS settings, so shoudl be finsessed based on data
-- actual data type really...
for i in 1..l_col_cnt loop
dbms_sql.define_column(c=>l_c, position=>i,
column=>l_varchar, column_size=>4000);
end loop;
-- execute the query
l_rc := dbms_sql.execute(c=>l_c);
-- fetch each row in turn
while dbms_sql.fetch_rows(c=>l_c) > 0 loop
-- for each column from describe
for i in 1..l_col_cnt loop
-- get the column value for this row (again, as string...)
dbms_sql.column_value(l_c, i, l_varchar);
-- write out to file, with delimiter after first column
if i > 1 then
UTL_FILE.PUT (fid, ';');
end if;
UTL_FILE.PUT (fid, l_varchar);
end loop;
UTL_FILE.NEW_LINE (fid);
end loop;
dbms_sql.close_cursor(l_c);
UTL_FILE.FCLOSE (fid);
EXCEPTION
WHEN OTHERS THEN UTL_FILE.FCLOSE (fid);
END;
/
Cela, en gros, analyse l'instruction transmise, l'exécute, récupère chaque ligne, obtient chaque valeur de colonne à tour de rôle (sous forme de chaîne, qui pourrait/devrait être développée pour éviter les conversions implicites) et écrit chacune de celles-ci dans le fichier à son tour - en ajoutant un délimiteur entre eux et une nouvelle ligne finale après chaque ligne.
Lorsqu'il est appelé depuis votre bloc anonyme qui crée un fichier contenant :
NLS_CHARACTERSET;AL32UTF8
NLS_RDBMS_VERSION;11.2.0.4.0
Soyez conscient, cependant, que cela exécutera tout ce qui lui est donné, y compris DDL (qui est exécuté lorsqu'il est analysé). Si vous ne contrôlez pas la façon dont cela est appelé, et même si vous le faites, vous devez ajouter la validation de l'instruction transmise pour vérifier qu'il ne s'agit en fait que d'une requête.
Vous trouverez peut-être plus simple d'explorer d'autres méthodes, telles que les tables externes (comme suggéré par @Kaushik) ou la fonctionnalité client.
Comme @kfinity l'a suggéré dans un commentaire, vous pouvez utiliser un curseur de référence pour analyser et exécuter la requête, ce qui devrait empêcher l'exécution de tout élément malveillant. Le dbms_sql
package a une fonction pour convertir un curseur de référence en curseur natif
, donc en utilisant cet insetad des étapes explicites d'ouverture, d'analyse et d'exécution :
CREATE OR REPLACE PROCEDURE p_create_text_file (
loc IN VARCHAR2
, file IN VARCHAR2
, select_statement in varchar2
, line_statement in varchar2 -- not used?
)
IS
fid UTL_FILE.FILE_TYPE := UTL_FILE.FOPEN (loc, file, 'W');
-- for initial parse and execute
l_refcursor sys_refcursor;
-- for dbms_sql
l_c pls_integer;
l_col_cnt pls_integer;
l_desc_t dbms_sql.desc_tab3;
l_rc pls_integer;
l_varchar varchar2(4000);
BEGIN
-- open ref cursor for the statement
open l_refcursor for select_statement;
-- convert ref cursor to dbms_sql cursor
l_c := dbms_sql.to_cursor_number(l_refcursor);
dbms_sql.describe_columns3(c => l_c, col_cnt => l_col_cnt,
desc_t => l_desc_t);
-- define all columns as strings; this will end up with implicit conversion
-- of dates etc. using NLS settings, so shoudl be finsessed based on data
-- actual data type really...
for i in 1..l_col_cnt loop
dbms_sql.define_column(c=>l_c, position=>i,
column=>l_varchar, column_size=>4000);
end loop;
-- fetch each row in turn
while dbms_sql.fetch_rows(c=>l_c) > 0 loop
-- for each column from describe
for i in 1..l_col_cnt loop
-- get the column value for this row (again, as string...)
dbms_sql.column_value(l_c, i, l_varchar);
-- write out to file, with delimiter after first column
if i > 1 then
UTL_FILE.PUT (fid, ';');
end if;
UTL_FILE.PUT (fid, l_varchar);
end loop;
UTL_FILE.NEW_LINE (fid);
end loop;
dbms_sql.close_cursor(l_c);
UTL_FILE.FCLOSE (fid);
EXCEPTION
WHEN OTHERS THEN UTL_FILE.FCLOSE (fid);
END;
/
... qui produit le même fichier de sortie.
Incidemment, si vous le souhaitez, vous pouvez également écrire les noms de colonne sous forme de ligne d'en-tête, avant la boucle d'extraction de lignes :
-- write column names as header row
for i in 1..l_col_cnt loop
if i > 1 then
UTL_FILE.PUT (fid, ';');
end if;
UTL_FILE.PUT (fid, l_desc_t(i).col_name);
end loop;
UTL_FILE.NEW_LINE (fid);