Skip to content

Commit c7f1a9c

Browse files
committed
new dragnet selection for DETERMINISTIC
1 parent a7a2283 commit c7f1a9c

File tree

7 files changed

+331
-129
lines changed

7 files changed

+331
-129
lines changed

app/controllers/dragnet_controller.rb

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -119,20 +119,26 @@ def exec_dragnet_sql
119119

120120
# Headerzeile des Report erstellen, Parameter ermitteln
121121
@caption = "#{dragnet_sql[:name]}"
122-
command_array = [dragnet_sql[:sql]]
122+
123+
binds = []
123124
if dragnet_sql[:parameter]
124125
@caption << ": "
125126
dragnet_sql[:parameter].each do |p| # Binden evtl. Parameter
126127
bind_value = params[p[:name]] # Parameter aus Form mit Name erwartet
127128
bind_value = bind_value.to_i if bind_value.to_i.to_s == bind_value # if full value can be interpreted as integer then use this
128-
command_array << bind_value
129+
binds << bind_value
129130
@caption << " '#{p[:name]}' = #{params[p[:name]]}," if params[p[:name]] && params[p[:name]] != '' # Ausgabe im Header
130131
end
131132
@caption[@caption.length-1] = ' ' # replace trailing comma
132133
end
133134

134135
# Ausführen des SQL
135-
@res = sql_select_all command_array
136+
@res = if dragnet_sql[:plsql]
137+
select_from_dbms_output(dragnet_sql[:sql], binds)
138+
else
139+
sql_select_all [dragnet_sql[:sql]].concat(binds)
140+
end
141+
136142
# Optionales Filtern des Results
137143
if dragnet_sql[:filter_proc]
138144
raise "filter_proc muss Klasse proc besitzen für #{dragnet_sql[:name]}" if dragnet_sql[:filter_proc].class.name != 'Proc'
@@ -164,7 +170,20 @@ def show_personal_selection_form
164170
render_partial
165171
end
166172

167-
private
173+
# Drop selection from personal list
174+
def drop_personal_selection
175+
drop_selection = extract_entry_by_entry_id(params[:dragnet_hidden_entry_id])
176+
177+
178+
dragnet_personal_selection_list = ClientInfoStore.read_for_client_key(get_decrypted_client_key,:dragnet_personal_selection_list, default: [])
179+
180+
drop_external_selection(dragnet_personal_selection_list, drop_selection[:name])
181+
182+
ClientInfoStore.write_for_client_key(get_decrypted_client_key,:dragnet_personal_selection_list, dragnet_personal_selection_list)
183+
184+
show_selection # Show edited selection list again
185+
end
186+
168187
# Kompletten Menu-Baum durchsuchen nach Name und raise bei Dopplung
169188
def look_for_double_names(list, double_names)
170189
list.each do |l|
@@ -174,7 +193,6 @@ def look_for_double_names(list, double_names)
174193
end
175194
end
176195

177-
public
178196
def add_personal_selection
179197
dragnet_personal_selection_list = ClientInfoStore.read_for_client_key(get_decrypted_client_key,:dragnet_personal_selection_list, default: [])
180198

@@ -212,22 +230,18 @@ def drop_external_selection(list, name)
212230
end
213231
end
214232

215-
216-
public
217-
# Drop selection from personal list
218-
def drop_personal_selection
219-
drop_selection = extract_entry_by_entry_id(params[:dragnet_hidden_entry_id])
220-
221-
222-
dragnet_personal_selection_list = ClientInfoStore.read_for_client_key(get_decrypted_client_key,:dragnet_personal_selection_list, default: [])
223-
224-
drop_external_selection(dragnet_personal_selection_list, drop_selection[:name])
225-
226-
ClientInfoStore.write_for_client_key(get_decrypted_client_key,:dragnet_personal_selection_list, dragnet_personal_selection_list)
227-
228-
show_selection # Show edited selection list again
233+
# Execute PL/SQL with DBMS_OUTPUT to get result
234+
# Each Line of DBMS_OUTPUT should be JSON formatted
235+
# @param sql [String] the SQL statement
236+
# @param binds [Array] the values to bind
237+
# @return [Array]
238+
def select_from_dbms_output(sql, binds)
239+
string_array = PanoramaConnection.exec_plsql_with_dbms_output_result(sql, binds)
240+
result = []
241+
string_array.each do |line|
242+
result << JSON.parse(line)
243+
end
244+
result
229245
end
230246

231-
232-
233247
end

app/helpers/dragnet/pl_sql_usage_helper.rb

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ def pl_sql_usage
179179
For functions without dependencies there is a high likelihood that they are deterministic.
180180
This selection shows PL/SQL functions not declared as DETERMINISTIC and without any dependency other than sys.STANDARD.
181181
In addition SQLs from SGA are shown which uses this function name in their SQL syntax.
182+
183+
This check does not evaluate package functions, only standalone functions are considered.
182184
"),
183185
:sql=> "\
184186
WITH Procs AS (SELECT /*+ NO_MERGE MATERIALIZE */ p.Owner, p.Object_Name
@@ -208,6 +210,172 @@ def pl_sql_usage
208210
ORDER BY 4 DESC NULLS LAST
209211
",
210212
},
213+
{
214+
:name => t(:dragnet_helper_178_name, :default=>'Candidates for DETERMINISTIC in package and standalone PL/SQL functions'),
215+
:desc => t(:dragnet_helper_178_desc, :default=>"User-defined PL/SQL functions may be cached in session for subsequent calls with same parameters if they are declared as deterministic.
216+
This selection shows PL/SQL functions not declared as DETERMINISTIC but used in long running SQL statements.
217+
218+
The result should be checked if these functions could be declared DETERMINISTIC.
219+
This could be useful even if there are SQL selections within the function that regularly prevent the deterministic state.
220+
E.g. selections on rarely changed master data can be considered deterministic for the duration of a SQL execution.
221+
222+
This check evaluates also functions in packages.
223+
"),
224+
plsql: true,
225+
:sql=> "\
226+
DECLARE
227+
TYPE proc_RT IS RECORD(
228+
Owner VARCHAR2(128),
229+
Package_Name VARCHAR2(128),
230+
Function_Name VARCHAR2(128)
231+
);
232+
TYPE Proc_TT IS TABLE OF Proc_RT INDEX BY VARCHAR2(2000);
233+
proc_table Proc_TT;
234+
235+
TYPE Char_Table_Type IS TABLE OF CHAR(1);
236+
Char_Table Char_Table_Type := Char_Table_Type(' ', '(', '!', '=', '<', '>', '+', '-', '*', '/'); -- delimiters chars to search for
237+
char_table_count INTEGER := Char_Table.COUNT;
238+
239+
Start_Pos INTEGER;
240+
End_pos INTEGER;
241+
Test_Pos INTEGER;
242+
l_text CLOB;
243+
l_match VARCHAR2(4000);
244+
full_match VARCHAR2(4000);
245+
l_json VARCHAR2(4000);
246+
l_count INTEGER;
247+
l_stmts INTEGER;
248+
249+
FUNCTION JSON_Esc(p_In VARCHAR2) RETURN VARCHAR2 IS
250+
BEGIN
251+
RETURN REPLACE(p_In, '\"', '\\\\\"');
252+
END JSON_Esc;
253+
254+
PROCEDURE Log(p_In VARCHAR2) IS
255+
BEGIN
256+
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SYSTIMESTAMP, 'HH24:MI:SS.FF3')||' - ' ||p_In);
257+
END;
258+
259+
BEGIN
260+
DBMS_OUTPUT.ENABLE(1000000);
261+
FOR p_Rec IN (SELECT Owner, Package_Name, Function_Name, Compare_Name
262+
FROM (
263+
SELECT Owner, NULL Package_Name, Object_Name Function_Name, Deterministic, Object_Name Compare_Name
264+
FROM DBA_Procedures
265+
WHERE Object_Type = 'FUNCTION'
266+
UNION ALL
267+
SELECT p.Owner, p.Object_Name Package_Name, p.Procedure_Name Function_Name, p.Deterministic, p.Object_Name||'.'||p.Procedure_Name Compare_Name
268+
FROM DBA_Procedures p
269+
JOIN DBA_Arguments a ON a.Owner = p.Owner AND a.Package_Name = p.Object_Name AND a.Object_Name = p.Procedure_Name
270+
WHERE p.Object_Type = 'PACKAGE'
271+
AND a.Argument_Name IS NULL /* Program is a function with a return value */
272+
)
273+
WHERE Owner NOT IN (SELECT UserName FROM All_Users WHERE Oracle_Maintained = 'Y')
274+
AND Deterministic = 'NO'
275+
) LOOP
276+
-- Lookup with function name only
277+
proc_table(p_Rec.Compare_Name) := Proc_RT(p_Rec.Owner, p_Rec.Package_Name, p_Rec.Function_Name);
278+
-- Lookup with function name qualified with owner
279+
proc_table(p_Rec.Owner||'.'||p_Rec.Compare_Name) := Proc_RT(p_Rec.Owner, p_Rec.Package_Name, p_Rec.Function_Name);
280+
END LOOP;
281+
282+
l_stmts := 0;
283+
FOR t_Rec IN (SELECT Instance_Number, SQL_ID, Parsing_Schema_Name, SQL_Text, Elapsed_Secs
284+
FROM (
285+
SELECT Instance_Number, SQL_ID, Parsing_Schema_Name,
286+
LTRIM(SQL_Text, CHR(10) || CHR(13) || ' ' || CHR(9)) SQL_Text, /* remove leading whitespaces from SQL */
287+
SUM(Elapsed_Secs) OVER (PARTITION BY Instance_Number, SQL_ID, Parsing_Schema_Name) Elapsed_Secs,
288+
ROW_NUMBER() OVER (PARTITION BY Instance_Number, SQL_ID, Parsing_Schema_Name ORDER BY 1) AS rn /* Because of no aggregate functions on CLOB */
289+
FROM (
290+
SELECT Inst_ID Instance_Number, SQL_ID, UPPER(SQL_FullText) SQL_Text, Parsing_Schema_Name, Elapsed_Time/1000000 Elapsed_Secs
291+
FROM gv$SQLArea
292+
WHERE Command_Type != 47 /* No PL/SQL */
293+
UNION ALL
294+
SELECT h.Instance_Number, h.SQL_ID, UPPER(t.SQL_Text) SQL_Text, h.Parsing_Schema_Name, h.Elapsed_Secs
295+
FROM (SELECT st.Instance_Number, st.SQL_ID, st.Parsing_Schema_Name, SUM(st.Elapsed_Time_Delta)/1000000 Elapsed_Secs
296+
FROM DBA_Hist_SQLStat st
297+
JOIN DBA_Hist_Snapshot ss ON ss.DBID = st.DBID AND ss.Snap_ID = st.Snap_ID AND ss.Instance_Number = st.Instance_Number
298+
WHERE ss.Begin_Interval_Time > SYSDATE - ?
299+
GROUP BY st.Instance_Number, st.SQL_ID, st.Parsing_Schema_Name
300+
) h
301+
JOIN DBA_Hist_SQLText t ON t.SQL_ID = h.SQL_ID
302+
)
303+
WHERE Elapsed_Secs > ?
304+
)
305+
WHERE RN = 1 /* Select one occurrence of SQL text per SQL-ID only */
306+
AND SQL_Text NOT LIKE 'BEGIN%' /* No command_type in DBA_Hist_SQLStat to filter PL/SQL */
307+
AND SQL_Text NOT LIKE 'DECLARE%'
308+
) LOOP
309+
l_stmts := l_stmts + 1;
310+
Start_Pos := 1;
311+
l_text := t_Rec.SQL_Text;
312+
l_Count := 0;
313+
-- Log('Start '||t_Rec.SQL_ID);
314+
LOOP
315+
-- Look for the next delimiter
316+
End_pos := 0; -- start value
317+
FOR i IN 1..char_table_count LOOP
318+
Test_Pos := INSTR(l_text, Char_Table(i), Start_Pos);
319+
IF Test_Pos > 0 AND ( End_Pos = 0 OR Test_Pos < End_Pos) THEN
320+
End_Pos := Test_Pos;
321+
END IF;
322+
END LOOP;
323+
324+
BEGIN
325+
IF End_Pos > 0 THEN
326+
l_match := SUBSTR(l_text, Start_Pos, End_pos-Start_Pos);
327+
ELSE
328+
l_match := SUBSTR(l_text, Start_Pos); -- The rest of the string
329+
END IF;
330+
l_Count := l_Count + 1;
331+
-- DBMS_OUTPUT.PUT_LINE(l_match);
332+
333+
IF proc_Table.EXISTS(l_match) -- Either owner and function name or only function name match
334+
THEN
335+
-- Get the following parameter for matching function call
336+
-- Check if parameters follow immediately (after end_pos are only spaces up to an opening parenthesis)
337+
full_match := l_match; -- Default if no parameter list follows
338+
IF End_Pos > 0 AND REGEXP_LIKE(SUBSTR(l_text, End_Pos), '^\\s*\\(') THEN -- opening parenthesis directly after or after spaces
339+
End_Pos := INSTR(l_text, ')', Start_Pos);
340+
IF End_Pos > 0 THEN
341+
full_match := SUBSTR(l_text, Start_Pos, End_pos-Start_Pos + 1);
342+
ELSE
343+
full_match := SUBSTR(l_text, Start_Pos); -- The rest of the string
344+
END IF;
345+
END IF;
346+
347+
348+
l_json := '{' ||
349+
'\"Instance\": ' || t_Rec.Instance_Number || ', ' ||
350+
'\"SQL-ID\": \"' || JSON_Esc(t_Rec.SQL_ID) || '\", ' ||
351+
'\"Parsing Schema Name\": \"' || t_Rec.Parsing_Schema_Name || '\", ' ||
352+
'\"Elapsed Secs\": ' || ROUND(t_Rec.Elapsed_Secs) || ', ' ||
353+
'\"Owner\": \"' || proc_Table(l_match).Owner || '\", ' ||
354+
'\"Package Name\": \"' || proc_Table(l_match).Package_Name || '\", ' ||
355+
'\"Function Name\": \"' || proc_Table(l_match).Function_Name || '\", ' ||
356+
'\"Match in SQL\": \"' || JSON_Esc(full_match) || '\" ' ||
357+
'}';
358+
DBMS_OUTPUT.PUT_LINE(l_json);
359+
END IF;
360+
EXCEPTION
361+
WHEN OTHERS THEN
362+
IF SQLCODE != -6502 THEN -- End_Pos > 4000 raises ORA-06502: PL/SQL: numeric or value error: character string buffer too small
363+
RAISE;
364+
END IF;
365+
END;
366+
EXIT WHEN End_Pos = 0;
367+
Start_Pos := End_Pos + 1;
368+
END LOOP;
369+
-- Log('End '||t_Rec.SQL_ID||' '||l_Count||' words');
370+
END LOOP;
371+
-- Log(l_stmts||' Statements');
372+
END;
373+
",
374+
:parameter=>[
375+
{:name=>t(:dragnet_helper_param_history_backward_name, :default=>'Consideration of history backward in days'), :size=>8, :default=>2, :title=>t(:dragnet_helper_param_history_backward_hint, :default=>'Number of days in history backward from now for consideration') },
376+
{:name=>t(:dragnet_helper_178_param_1_name, :default=>'Min. elapsed seconds for SQL execution'), :size=>8, :default=>1000, :title=>t(:dragnet_helper_178_param_1_hint, :default=>'Minimum number of total elapsed seconds for a SQL in SGA and AWR reports to be considered in selection')},
377+
]
378+
},
211379
]
212380
end
213381

app/helpers/dragnet_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def tag_external_selections(list, flag)
5454
# :sql SQL-Statement zur Ausführung
5555
# :min_db_version Optional minimum DB version
5656
# :not_executable Optional mark entry as not executable SQL
57+
# :plsql Execute as anonymous PL/SQL with JSON result per DBMS_OUTPUT
5758
# :suppress_error_for_code Ignore errors if this code is contained in error message
5859
# :parameter Array von Hashes mit folgender Struktur
5960
# :name Name des Parameters

app/models/panorama_connection.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -766,19 +766,18 @@ def self.exec_clob_plsql_function(function_call, binds)
766766
thread_connection&.unregister_sql_execution
767767
end
768768

769-
# Execute anonymous PL/SQL code that returns its result by use of DBMS_OUTPUT, mind the missing semicolon at the end of the command
770-
# @param code [String] the PL/SQL code to execute, e.g. "BEGIN DBMS_OUTPUT.PUT_LINE(?); END"
769+
# Execute anonymous PL/SQL code that returns its result by use of DBMS_OUTPUT
770+
# @param code [String] the PL/SQL code to execute, e.g. "BEGIN DBMS_OUTPUT.PUT_LINE(?); END;"
771771
# @param binds [Array] the parameters to bind to the function call
772772
# one bind as a Hash "{ java_type: "STRING", value: "some_value" }"
773773
# @return [Array] the DBMS_OUTPUT result as array of strings
774774
def self.exec_plsql_with_dbms_output_result(code, binds)
775-
sql = "{ call #{code} }"; # CallableStatement syntax for PL/SQL
776-
thread_connection.register_sql_execution(sql)
775+
thread_connection.register_sql_execution(code)
777776

778777
self.sql_execute("BEGIN DBMS_OUTPUT.ENABLE(NULL); END;")
779778

780779

781-
cs = PanoramaConnection.get_jdbc_raw_connection.prepare_call(sql)
780+
cs = PanoramaConnection.get_jdbc_raw_connection.prepare_call(code)
782781

783782
binds.each_with_index do |bind, index|
784783
self.bind_java_input_parameter(cs, index + 1, bind)
@@ -787,7 +786,7 @@ def self.exec_plsql_with_dbms_output_result(code, binds)
787786
cs.execute();
788787

789788
# read the DBMS_OUTPUT buffer now
790-
ob_cs = PanoramaConnection.get_jdbc_raw_connection.prepare_call("{ call DBMS_OUTPUT.GET_LINES(?, ?) }")
789+
ob_cs = PanoramaConnection.get_jdbc_raw_connection.prepare_call("BEGIN DBMS_OUTPUT.GET_LINES(?, ?); END;")
791790
ob_cs.register_out_parameter(1, OracleTypes::ARRAY, "DBMS_OUTPUT.CHARARR"); # oracle.jdbc.OracleTypes::ARRAY needs to be imorted by java_import (no leading keyword "java" in class)
792791
ob_cs.register_out_parameter(2, java.sql.Types::INTEGER);
793792

@@ -805,7 +804,7 @@ def self.exec_plsql_with_dbms_output_result(code, binds)
805804
self.sql_execute("BEGIN DBMS_OUTPUT.DISABLE; END;")
806805
result
807806
rescue Exception => e
808-
Rails.logger.error('PanoramaConnection.exec_plsql_with_dbms_output_result') { "Error '#{e.class} : #{e.message}' occurred" }
807+
Rails.logger.error('PanoramaConnection.exec_plsql_with_dbms_output_result') { "Error '#{e.class} : #{e.message}' occurred at execution of:\n#{code}" }
809808
raise e
810809
ensure
811810
cs&.close if defined? cs

0 commit comments

Comments
 (0)