Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
38e02ab
Testing something
lwasylow Mar 9, 2026
461ad5d
Another test
lwasylow Mar 9, 2026
44f5495
Test
lwasylow Mar 9, 2026
b5f2dc7
Thats funny:)
lwasylow Mar 9, 2026
0dd7dba
that is rubbish
lwasylow Mar 9, 2026
4ea0fcb
revert test
lwasylow Mar 9, 2026
17604d8
Small tweaks
lwasylow Mar 9, 2026
5869f32
one at the time
lwasylow Mar 9, 2026
3f8d2cf
Move out outside
lwasylow Mar 9, 2026
37fae24
fix invalid locator
lwasylow Mar 9, 2026
5189955
revert
lwasylow Mar 9, 2026
5a21774
Changing a code to validate by lines instead of clobs for performance.
lwasylow Mar 14, 2026
39f5d64
Fixing loop syntax
lwasylow Mar 14, 2026
93ffb72
Merge branch 'develop' of https://github.com/utPLSQL/utPLSQL into fea…
lwasylow Mar 14, 2026
393cfec
Update code
lwasylow Mar 15, 2026
8d794f0
Update block
lwasylow Mar 15, 2026
a32e09d
Small fixes
lwasylow Mar 15, 2026
b2e914f
Update comment
lwasylow Mar 15, 2026
66de806
Introduce global variable
lwasylow Mar 15, 2026
f82a3b5
Cleanup
lwasylow Mar 15, 2026
934df67
Optimization fiurther
lwasylow Mar 15, 2026
65145c2
Trim spaces
lwasylow Mar 15, 2026
8105179
Added extra tests.
lwasylow Mar 15, 2026
cc491cf
Enhance annotation parser with new tests and source line handling fun…
lwasylow Mar 15, 2026
6717ad3
Add tests for Windows-style newlines and long procedure names in anno…
lwasylow Mar 15, 2026
6c513be
Fix regex extraction for procedure/function names and ensure ordered …
lwasylow Mar 15, 2026
46f3d12
Update tests
lwasylow Mar 15, 2026
81e1752
Refactor annotation parser tests and utility functions
lwasylow Mar 16, 2026
fecbcc1
Remove redundant parse_object_annotations function overloads and upda…
lwasylow Mar 16, 2026
2547b40
Update code to avoid
lwasylow Mar 17, 2026
40eb166
Enhance annotation processing by improving SQL text handling and addi…
lwasylow Mar 17, 2026
1cdeb9c
Refactor annotation processing and enhance line scanning functionalit…
lwasylow Mar 18, 2026
c4d0ab0
Remove redundant exception handling in build_annot_cache_for_sources …
lwasylow Mar 18, 2026
aa3bfe7
Refactor annotation patterns in ut_annotation_parser and remove unuse…
lwasylow Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Changing a code to validate by lines instead of clobs for performance.
  • Loading branch information
lwasylow committed Mar 14, 2026
commit 5a217746ef9109cd96185310f92ee8ca6b01e32a
153 changes: 149 additions & 4 deletions source/core/annotations/ut_annotation_parser.pkb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,78 @@ create or replace package body ut_annotation_parser as
end loop;
end add_procedure_annotations;

procedure add_procedure_annotations(
a_annotations in out nocopy ut_annotations,
a_source in dbms_preprocessor.source_lines_t,
a_comments in out nocopy tt_comment_list
) is
l_proc_comments varchar2(32767);
l_proc_name varchar2(250);
l_line varchar2(32767);
l_in_comment_block boolean := false;
l_i binary_integer;
begin
l_i := 1;
while l_i <= a_source.count loop
l_line := a_source(l_i);

-- -----------------------------------------------------------------
-- Comment placeholder line: start/continue accumulating a block
-- -----------------------------------------------------------------
if instr(l_line, '{COMMENT#') > 0 then
if l_in_comment_block then
l_proc_comments := l_proc_comments || l_line || chr(10);
else
l_in_comment_block := true;
l_proc_comments := l_line || chr(10);
end if;
l_i := l_i + 1;

-- -----------------------------------------------------------------
-- Whitespace-only line: allowed between comment block and proc decl
-- -----------------------------------------------------------------
elsif l_in_comment_block and trim(replace(l_line, chr(9))) is null then
l_i := l_i + 1;

-- -----------------------------------------------------------------
-- procedure/function declaration following a comment block
-- -----------------------------------------------------------------
elsif l_in_comment_block
and regexp_like(l_line, '^\s*(procedure|function)\s+', 'i')
then
-- extract just the identifier name (subexpression 2)
l_proc_name := trim(regexp_substr(l_line
,pattern => '^\s*(procedure|function)\s+('||gc_regexp_identifier||')'
,modifier => 'i'
,subexpression => 2));

-- pass accumulated comment placeholders + proc name to add_annotations
add_annotations(a_annotations, l_proc_comments, a_comments, l_proc_name);

-- reset comment block state
l_in_comment_block := false;
l_proc_comments := null;

-- advance past proc header to the terminating ';'
-- the header may span multiple lines e.g. with multi-line parameter lists
while l_i <= a_source.count loop
exit when instr(a_source(l_i), ';') > 0;
Comment thread
lwasylow marked this conversation as resolved.
l_i := l_i + 1;
end loop;
l_i := l_i + 1; -- step past the ';' line itself

-- -----------------------------------------------------------------
-- Any other line: reset comment block accumulator
-- -----------------------------------------------------------------
else
l_in_comment_block := false;
l_proc_comments := null;
l_i := l_i + 1;
end if;

end loop;
end add_procedure_annotations;

function extract_and_replace_comments(a_source in out nocopy clob) return tt_comment_list is
Comment thread
jgebal marked this conversation as resolved.
Outdated
l_comments tt_comment_list;
l_comment_pos binary_integer;
Expand Down Expand Up @@ -175,13 +247,87 @@ create or replace package body ut_annotation_parser as

end loop;

ut_utils.debug_log(a_source);
return l_comments;
end extract_and_replace_comments;

function extract_and_replace_comments(
a_source in out nocopy dbms_preprocessor.source_lines_t
) return tt_comment_list is
l_comments tt_comment_list;
l_line varchar2(32767);
l_comment_pos binary_integer;
l_comment_replacer varchar2(50);
begin
for i in 1 .. a_source.count loop
l_line := a_source(i);

-- fast path: skip lines that can't possibly match
-- must contain '--' and '%' to be an annotation comment
if instr(l_line, '--') = 0 or instr(l_line, gc_annotation_qualifier) = 0 then
continue;
end if;

-- find '--' on the line
l_comment_pos := instr(l_line, '--');

-- verify everything before '--' is only spaces/tabs (matches ^ *( |\t)*--)
if trim(replace(substr(l_line, 1, l_comment_pos - 1), chr(9))) is not null then
continue;
end if;

-- skip '--' and any spaces after it, then check for annotation qualifier '%'
l_comment_pos := l_comment_pos + 2;
-- skip optional spaces between -- and %
while l_comment_pos <= length(l_line)
and substr(l_line, l_comment_pos, 1) = ' '
loop
l_comment_pos := l_comment_pos + 1;
end loop;

-- must start with annotation qualifier at this position
if substr(l_line, l_comment_pos, 1) != gc_annotation_qualifier then
continue;
end if;

-- extract annotation text (from % to end of line, trimmed)
l_comments(i) := trim(substr(l_line, l_comment_pos));

-- replace line with placeholder, preserving line number in token
l_comment_replacer := replace(gc_comment_replacer_patter, '%N%', i);
a_source(i) := l_comment_replacer;

end loop;

return l_comments;
end extract_and_replace_comments;

------------------------------------------------------------
--public definitions
------------------------------------------------------------
function parse_object_annotations(a_source dbms_preprocessor.source_lines_t) return ut_annotations is
l_source dbms_preprocessor.source_lines_t := a_source;
l_comments tt_comment_list;
l_annotations ut_annotations := ut_annotations();
l_result ut_annotations;
l_comment_index positive;
begin
l_source := ut_utils.replace_multiline_comments(l_source);
l_comments := extract_and_replace_comments(l_source);
add_procedure_annotations(l_annotations, l_source, l_comments);
delete_processed_comments(l_comments, l_annotations);

--at this point, only the comments not related to procedures are left, so we process them all as top-level
l_comment_index := l_comments.first;
while l_comment_index is not null loop
add_annotation( l_annotations, l_comment_index, l_comments( l_comment_index ) );
l_comment_index := l_comments.next(l_comment_index);
end loop;

select /*+ no_parallel */ value(x) bulk collect into l_result from table(l_annotations) x order by x.position;

return l_result;

end parse_object_annotations;

function parse_object_annotations(a_source clob) return ut_annotations is
Comment thread
lwasylow marked this conversation as resolved.
Outdated
l_source clob := a_source;
Expand Down Expand Up @@ -217,7 +363,7 @@ create or replace package body ut_annotation_parser as

function parse_object_annotations(a_source_lines dbms_preprocessor.source_lines_t, a_object_type varchar2) return ut_annotations is
l_processed_lines dbms_preprocessor.source_lines_t;
l_source clob;
l_source dbms_preprocessor.source_lines_t;
l_annotations ut_annotations := ut_annotations();
ex_package_is_wrapped exception;
pragma exception_init(ex_package_is_wrapped, -24241);
Expand All @@ -235,11 +381,10 @@ create or replace package body ut_annotation_parser as
end if;
--convert to clob
for i in 1..l_processed_lines.count loop
ut_utils.append_to_clob(l_source, replace(l_processed_lines(i), chr(13)||chr(10), chr(10)));
l_source(i) := replace(l_processed_lines(i), chr(13)||chr(10), chr(10));
end loop;
--parse annotations
l_annotations := parse_object_annotations(l_source);
dbms_lob.freetemporary(l_source);
exception
when ex_package_is_wrapped or source_text_is_empty then
null;
Expand Down
2 changes: 2 additions & 0 deletions source/core/annotations/ut_annotation_parser.pks
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,7 @@ create or replace package ut_annotation_parser authid current_user as
*/
function parse_object_annotations(a_source clob) return ut_annotations;
Comment thread
lwasylow marked this conversation as resolved.
Outdated

function parse_object_annotations(a_source dbms_preprocessor.source_lines_t) return ut_annotations;
Comment thread
lwasylow marked this conversation as resolved.
Outdated

end;
/
176 changes: 176 additions & 0 deletions source/core/ut_utils.pkb
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,182 @@ create or replace package body ut_utils is
return l_result;
end;

function replace_multiline_comments(a_source dbms_preprocessor.source_lines_t)
return dbms_preprocessor.source_lines_t
is
l_result dbms_preprocessor.source_lines_t;
l_line varchar2(32767);
l_remaining varchar2(32767);
l_in_ml_comment boolean := false;
l_ml_end binary_integer;
l_ml_start binary_integer;
l_comment_start binary_integer;
l_text_start binary_integer;
l_eq_text_start binary_integer;
l_eq_end_char varchar2(1 char);
l_pos binary_integer;
l_end binary_integer;
l_token_count binary_integer;
l_has_ml_comment boolean := false;
begin

-- Guard: empty source
if a_source.count = 0 then
return a_source;
end if;

-- Fast pre-scan: check if any /* exists at all if not, nothing to do — return source as-is
for i in 1 .. a_source.count loop
if instr(a_source(i), '/*') > 0 then
l_has_ml_comment := true;
exit prescan;
end if;
end loop prescan;

if not l_has_ml_comment then
return a_source;
end if;

<<process_lines>>
for i in 1 .. a_source.count loop
l_line := a_source(i);
-- Fast path: currently inside a multi-line comment
if l_in_ml_comment then
l_ml_end := instr(l_line, '*/');
if l_ml_end > 0 then
l_in_ml_comment := false;
l_line := substr(l_line, l_ml_end + 2);
-- fall through to normal scan of remainder
else
l_result(i) := '';
continue process_lines;
end if;
end if;

-- Fast path: no special tokens on this line at all
if instr(l_line, '/') = 0
and instr(l_line, '-') = 0
and instr(l_line, '''') = 0
then
l_result(i) := l_line;
continue process_lines;
end if;

-- Normal scan: consume one token at a time, advance l_remaining
l_remaining := l_line;
l_line := null;

<<scan_line>>
Comment thread
lwasylow marked this conversation as resolved.
Outdated
loop
exit when l_remaining is null or l_remaining = '';
Comment thread
lwasylow marked this conversation as resolved.
Outdated

l_ml_start := instr(l_remaining, '/*');
l_comment_start := instr(l_remaining, '--');
l_text_start := instr(l_remaining, '''');
-- only search for q' if ' was found — q' always contains '
l_eq_text_start := case when l_text_start > 0
then instr(l_remaining, 'q''')
else 0
end;

-- count how many tokens are present
l_token_count := sign(l_ml_start) + sign(l_comment_start)
+ sign(l_text_start) + sign(l_eq_text_start);

-- no special tokens left — consume remainder and stop
if l_token_count = 0 then
l_line := l_line || l_remaining;
exit scan_line;
end if;

-- only one token present — skip LEAST, use GREATEST to find it
if l_token_count = 1 then
l_pos := greatest(l_ml_start, l_comment_start, l_text_start, l_eq_text_start);
else
l_pos := least(
case when l_ml_start > 0 then l_ml_start else 32767 end,
case when l_comment_start > 0 then l_comment_start else 32767 end,
case when l_text_start > 0 then l_text_start else 32767 end,
case when l_eq_text_start > 0 then l_eq_text_start else 32767 end
);
end if;

-- q-quoted string: checked before plain quote because q' contains ' and would be misidentified
if l_pos = l_eq_text_start
and (l_ml_start = 0 or l_eq_text_start < l_ml_start)
and (l_comment_start = 0 or l_eq_text_start < l_comment_start)
and (l_text_start = 0 or l_eq_text_start < l_text_start)
then
l_eq_end_char := translate(
substr(l_remaining, l_eq_text_start + 2, 1),
'[{(<', ']})>'
);
l_end := instr(l_remaining, l_eq_end_char || '''', l_eq_text_start + 3);
if l_end > 0 then
l_line := l_line || substr(l_remaining, 1, l_end + 1);
l_remaining := substr(l_remaining, l_end + 2);
else
l_line := l_line || l_remaining;
exit scan_line;
end if;

-- Multi-line comment: skip it, continue scanning remainder of line after comment end
elsif l_pos = l_ml_start
and (l_comment_start = 0 or l_ml_start < l_comment_start)
and (l_text_start = 0 or l_ml_start < l_text_start)
and (l_eq_text_start = 0 or l_ml_start < l_eq_text_start)
then
l_line := l_line || substr(l_remaining, 1, l_ml_start - 1);
l_ml_end := instr(l_remaining, '*/', l_ml_start + 2);
if l_ml_end > 0 then
l_remaining := substr(l_remaining, l_ml_end + 2);
else
l_in_ml_comment := true;
exit scan_line;
end if;

-- Single-line comment: keep it, stop scanning this line
elsif l_pos = l_comment_start
and (l_ml_start = 0 or l_comment_start < l_ml_start)
and (l_text_start = 0 or l_comment_start < l_text_start)
and (l_eq_text_start = 0 or l_comment_start < l_eq_text_start)
then
l_line := l_line || l_remaining;
exit scan_line;

-- Regular string literal: keep it, continue scanning remainder of line after closing quote
else
-- scan forward continuously to handle '' escaped quotes
l_end := l_text_start + 1;
loop
l_end := instr(l_remaining, '''', l_end);
exit when l_end = 0;
if substr(l_remaining, l_end, 2) = '''''' then
l_end := l_end + 2; -- skip escaped quote, keep scanning
else
exit; -- found real closing quote
end if;
end loop;

if l_end > 0 then
l_line := l_line || substr(l_remaining, 1, l_end);
l_remaining := substr(l_remaining, l_end + 1);
else
l_line := l_line || l_remaining;
exit scan_line;
end if;
end if;

end loop scan_line;

l_result(i) := l_line;

end loop process_lines;

return l_result;
end replace_multiline_comments;


function replace_multiline_comments(a_source clob) return clob is
Comment thread
lwasylow marked this conversation as resolved.
Outdated
l_result clob;
l_ml_comment_start binary_integer := 1;
Expand Down
3 changes: 3 additions & 0 deletions source/core/ut_utils.pks
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ create or replace package ut_utils authid definer is
/**
* Replaces multi-line comments in given source-code with empty lines
*/
function replace_multiline_comments(a_source dbms_preprocessor.source_lines_t)
return dbms_preprocessor.source_lines_t;

function replace_multiline_comments(a_source clob) return clob;
Comment thread
lwasylow marked this conversation as resolved.
Outdated

/**
Expand Down
Loading