Skip to content

Commit 39134b7

Browse files
committed
Added/Updated tests\bugs\gh_7687_test.py: Checked on 5.0.0.1169. See notes.
1 parent 9bbc395 commit 39134b7

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed

tests/bugs/gh_7687_test.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#coding:utf-8
2+
3+
"""
4+
ID: issue-7687
5+
ISSUE: https://github.com/FirebirdSQL/firebird/issues/7687
6+
TITLE: Add LEVEL column to PLG$PROF_RECORD_SOURCES and PLG$PROF_RECORD_SOURCE_STATS_VIEW
7+
DESCRIPTION:
8+
Test creates master-detail tables and view 'v_test' for them.
9+
Then we create auxiliary view 'vp_rec_stats' to see profiler data.
10+
Running query to 'v_test' will add data to profiler snapshot tables so that we must have ability
11+
to reconstruct the whole EXPLAINED PLAN of that query, i.e. with top-level and all subsequent access paths.
12+
This reconstruction is performed by recursive query which first gets records with 'parent_record_source_id is null'
13+
(and all these records always have LEVEL = 0), and then goes down according to the principle:
14+
"give be lines with level that be bigger but CLOSEST to current one". This is achieved by applying dense_rank()
15+
function, see 'ranked_level' column usage and notes below.
16+
NOTES:
17+
[25.08.2023] pzotov
18+
1. We can NOT use "next level must be current one plus 1" in recursive part of query that reconstructs explained plan!
19+
This is because some access paths can be 'splitted' onto one or several 'sub-paths' by LF character, thus they are looked
20+
to be somewhat as 'multi-line' parts. Every LF in such case causes LEVEL to be "sinlently" increased by 1, so lines
21+
AFTER this "complex" access path will have LEVEL that differs from source for MORE than 1.
22+
23+
Here is example of such data (there is NO record with LEVEL = 3):
24+
STTM_ID LEVEL REC_ID PAR_ID ACC_PATH SQL_TEXT
25+
======= ======= ======= ======= ================================================== ==================================================
26+
5760 0 1 <null> Select Expression select first 1 r.* from rdb$database r order by 1
27+
5760 1 2 1 -> First N Records select first 1 r.* from rdb$database r order by 1
28+
5760 2 3 2 -> Refetch
29+
-> Sort (record length: 36, key select first 1 r.* from rdb$database r order by 1
30+
5760 4 4 3 -> Table "RDB$DATABASE" as "R" Full Scan select first 1 r.* from rdb$database r order by 1
31+
32+
This was implemented intentionally with purpose to make reconstruction of indents easier.
33+
Because of that, view that queries PLG$PROF_ tables must use "dense_rank()over(order by t.level)".
34+
2. We have to make additional (preliminary) call to rdb$profiler.start_session() / .finish_session() BEFORE test in order to create view
35+
that is based on profiler snapshot tables. Otherwise error raises:
36+
SQLSTATE = 42S02 / ... / -Table unknown / -PLG$PROF_RECORD_SOURCE_STATS_VIEW
37+
See: doc/sql.extensions/README.profiler.md
38+
"Snapshot tables (as well views and sequence) are automatically created in the first usage of the profiler"
39+
3. Currently there is problem with distinguish 'blocks' from profiler data that corresponds to sub-queries: there is no column that applies
40+
to the whole block and lines that belong (by hierarchy) to subquery can be displayed either before or after lines that relate to 'main'
41+
part of query. Although it seems that subquery is mostly displayed *after* main part.
42+
Because of that, it was decided for now to leave expected output as it is produced by current FB versions.
43+
If future runs will show that subquery part "jumps" randomly (from "top" to "bottom") than only presense of top-level access paths
44+
will be checked (without requirement to their concrete order).
45+
4. In order to see indents properly (including case if they will change in size) we have to prefix each line ('#' character is used).
46+
47+
Checked on 5.0.0.1169.
48+
"""
49+
50+
import os
51+
import pytest
52+
from firebird.qa import *
53+
54+
db = db_factory()
55+
act = python_act('db') #, substitutions=[('[ \t]+', ' ')])
56+
57+
@pytest.mark.version('>=5.0')
58+
def test_1(act: Action, capsys):
59+
60+
test_sql = f"""
61+
create table tmain(id int primary key using index tmain_pk, x int);
62+
create table tdetl(id int primary key using index tdetl_pk, pid int references tmain using index tdetl_fk, y int, z int);
63+
insert into tmain(id,x) select row_number()over(), -100 + rand()*200 from rdb$types rows 100;
64+
insert into tdetl(id, pid, y,z) select row_number()over(), 1+rand()*99, rand()*1000, rand()*1000 from rdb$types;
65+
commit;
66+
create index tmain_x on tmain(x);
67+
create index tdetl_y on tdetl(y);
68+
create index tdetl_z on tdetl(z);
69+
70+
set statistics index tdetl_fk;
71+
commit;
72+
73+
out nul;
74+
-- This is needed in order to create view based on snapshot 'plg$prof_*' tables:
75+
select rdb$profiler.start_session('profile session 0') from rdb$database;
76+
out;
77+
78+
create view vp_rec_stats as
79+
select
80+
cast(t.statement_id as smallint) as sttm_id
81+
,cast(t.level as smallint) as level
82+
,cast(t.record_source_id as smallint) as rec_id
83+
,cast(t.parent_record_source_id as smallint) as par_id
84+
,substring('#' || lpad('', 4*t.level,' ') || replace( replace(t.access_path, ascii_char(13), ''), ascii_char(10), ascii_char(10) || '#' || lpad('', 4*t.level,' ') ) from 1 for 320) as acc_path
85+
,substring( cast(t.sql_text as varchar(255)) from 1 for 50 ) as sql_text
86+
,dense_rank()over(order by t.level) as ranked_level
87+
from plg$prof_record_source_stats_view t
88+
join plg$prof_sessions s
89+
on s.profile_id = t.profile_id and
90+
s.description = 'profile session 1'
91+
;
92+
execute procedure rdb$profiler.finish_session(true);
93+
commit;
94+
95+
96+
create view v_test as
97+
select m4.id, d4.y, d4.z
98+
from tmain m4
99+
cross join lateral (
100+
select y, z
101+
from tdetl dx
102+
where
103+
dx.pid = m4.id
104+
and m4.x between dx.y and dx.z
105+
) d4
106+
where exists(select count(*) from tdetl dy group by dy.pid having count(*) > 2);
107+
108+
out nul;
109+
select rdb$profiler.start_session('profile session 1') from rdb$database;
110+
-- Test query for which we want to see data in the profiler tables:
111+
-- ##########
112+
select * from v_test;
113+
out;
114+
execute procedure rdb$profiler.finish_session(true);
115+
commit;
116+
117+
set transaction read committed;
118+
set count on;
119+
set list off;
120+
set heading off;
121+
122+
-- ##############################################
123+
-- Output data from profiler.
124+
-- NB-1: each line is prefixed with '#' character in order to see indents.
125+
-- NB-2: 'level' column can not be used for joining in recursive part because of 'gaps'!
126+
-- Instead, 'ranked_level' is used fo this:
127+
-- ##############################################
128+
with recursive
129+
r as (
130+
select sttm_id, level, t.ranked_level, rec_id, par_id, acc_path --, sql_text
131+
from vp_rec_stats t
132+
where
133+
t.par_id is null and t.sql_text not containing 'rdb$profiler.start_session'
134+
135+
UNION ALL
136+
137+
select t.sttm_id, t.level, t.ranked_level, t.rec_id, t.par_id, t.acc_path --, t.sql_text
138+
from vp_rec_stats t
139+
join r on t.sttm_id = r.sttm_id and t.ranked_level = r.ranked_level + 1 and r.rec_id = t.par_id
140+
)
141+
select acc_path from r;
142+
"""
143+
144+
act.expected_stdout = f"""
145+
#Select Expression
146+
# -> Filter (preliminary)
147+
# -> Nested Loop Join (inner)
148+
# -> Table "TMAIN" as "V_TEST M4" Full Scan
149+
# -> Filter
150+
# -> Table "TDETL" as "V_TEST D4 DX" Access By ID
151+
# -> Bitmap And
152+
# -> Bitmap
153+
# -> Index "TDETL_FK" Range Scan (full match)
154+
# -> Bitmap
155+
# -> Index "TDETL_Y" Range Scan (upper bound: 1/1)
156+
#Sub-query (invariant)
157+
# -> Filter
158+
# -> Aggregate
159+
# -> Table "TDETL" as "V_TEST DY" Access By ID
160+
# -> Index "TDETL_FK" Full Scan
161+
162+
Records affected: 10
163+
"""
164+
act.isql(input = test_sql, combine_output = True)
165+
assert act.clean_stdout == act.clean_expected_stdout
166+
act.reset()

0 commit comments

Comments
 (0)