Forum OpenACS Development: Response to CONNECT BY solution
As with the nested set model, the tree hierarchy is encoded in a separate field, and triggers are used to maintain the hierarchy.
create table acs_object_types (
object_type varchar(100) primary key,
supertype varchar(100) references acs_object_types,
sortkey varchar(4000)
);
The character Encodings are kept in a separate table which is used for doing the conversion to and from base-159 encoding.
CREATE TABLE tree_encodings (
deci int primary key,
code char(1)
);
create index tree_encode_idx on tree_encodings(code);
In discussing the base-159 encoding scheme, Sofer mentions in his paper that if the database is not built with internationalization support, then it might be necessary to limit the key encodings to ASCII characters. In such a case, it would be necessary to use a base-64 encoding scheme. In my testing, I'm using a plain vanilla build of postgresql, and the base-159 encoding loads correctly, and it seems to operate well, so I'm not sure about Sofer's comment in this regard. It seems that postgresql supports the latin-1 character set by default. I would be interested in hearing comments from people with regards to potential problems that might be encountered if base-159 encoding is chosen as the default.
For the m-vgID model, there are two triggers, one for inserts and one for updates. The insert trigger function shown below, is quite simple. To calculate the newly inserted nodes sortkey value, it finds the max sortkey value among its sister nodes, increments it by one and appends it to it to the sortkey of its parent. If the newly inserted node doesn't have any sister nodes, then the sortkey value is set to '00' (see Sofer's paper for an explanation of the encoding of sortkeys in the m-vgID model).
create function acs_object_type_insert_tr () returns opaque as '
declare
v_parent_sk varchar;
max_key varchar;
begin
select max(sortkey) into max_key
from acs_object_types
where supertype = new.supertype;
select coalesce(max(sortkey),'''') into v_parent_sk
from acs_object_types
where object_type = new.supertype;
new.sortkey := v_parent_sk || ''/'' || next_sortkey(max_key);
return new;
end;' language 'plpgsql';
create trigger acs_object_type_insert_tr before insert
on acs_object_types for each row
execute procedure acs_object_type_insert_tr ();
The update trigger, is slightly more complicated, as it must update the whole sub-tree for the newly updated node. The first time through the loop, the clr_keys_p flag is true, and this forces all of the sub-trees sortkeys to be set to null, as a result, all of the sortkeys in the sub-tree are recomputed.
create function acs_object_type_update_tr () returns opaque as '
declare
v_parent_sk varchar;
max_key varchar;
v_rec record;
clr_keys_p boolean default ''t'';
begin
if new.object_type = old.object_type and
new.supertype = old.supertype then
return new;
end if;
for v_rec in select object_type
from acs_object_types
where sortkey like new.sortkey || ''%''
order by sortkey
LOOP
if clr_keys_p then
update acs_object_types set sortkey = null
where sortkey like new.sortkey || ''%'';
clr_keys_p := ''f'';
end if;
select max(sortkey) into max_key
from acs_object_types
where supertype = (select supertype
from acs_object_types
where object_type = v_rec.object_type);
select coalesce(max(sortkey),'''') into v_parent_sk
from acs_object_types
where object_type = (select supertype
from acs_object_types
where object_type = v_rec.object_type);
update acs_object_types
set sortkey = v_parent_sk || ''/'' || next_sortkey(max_key)
where object_type = v_rec.object_type;
end LOOP;
return new;
end;' language 'plpgsql';
create trigger acs_object_type_update_tr after update on acs_object_types
for each row execute procedure acs_object_type_update_tr ();
Each of the triggers references the "next_sortkey" routine shown below. This routine takes as its input the max sortkey value of the current nodes sister nodes and increments it by one using base 159 math. If the current node doesn't have any sister nodes at the same level, then a null is passed in and a value of "00" is returned. "00" is the encoded value of the first key for any level.
create function next_sortkey(varchar) returns varchar as '
declare
skey alias for $1;
pos integer;
dec_val integer;
stop boolean default ''f'';
carry boolean default ''t'';
nkey varchar default '''';
base integer;
ch char(1);
begin
base := default_tree_encoding_base();
if skey is null then
return ''00'';
else
pos := length(skey);
LOOP
ch := substr(skey,pos,1);
if carry = ''t'' then
carry = ''f'';
select (deci + 1) % base into dec_val
from tree_encodings
where code = ch;
select code::varchar || nkey into nkey
from tree_encodings
where deci = dec_val;
if dec_val = 0 then carry = ''t''; end if;
else
nkey := ch::varchar || nkey;
end if;
pos := pos - 1;
select case when substr(skey,pos - 1,1) = ''/''
then ''t''
else ''f''
end into stop;
exit when stop = ''t'';
END LOOP;
if carry = ''t'' then
nkey := ''0'' || nkey;
end if;
end if;
select code::varchar || nkey into nkey
from tree_encodings
where deci = length(nkey) - 1;
return nkey;
end;' language 'plpgsql';
In the m-vgID model, the level corresponds to the number of '/' characters, so the pseudo level column can be calculated using the following simple pl/pgsql function:
create function tree_level(varchar) returns integer as '
declare
inkey alias for $1;
cnt integer default 0;
begin
for i in 1..length(inkey) LOOP
if substr(inkey,i,1) = ''/'' then
cnt := cnt + 1;
end if;
end LOOP;
return cnt;
end;' language 'plpgsql';
As with the nested set model, emulation of oracle connect-by queries can be achieved through simple queries based on the new sortkey field. Using the same data as the nested set example shown in the preceeding posts, I will show a couple of common queries with the m-vgID model.
A simple connect by statement can be emulated by selecting from acs_object_types with an "order by" on the sortkey:
select repeat(' ',(tree_level(sortkey) - 1)*2) || object_type as object_type,
supertype,
tree_level(sortkey) as level,
sortkey
from acs_object_types
order by sortkey;
object_type | supertype | level | sortkey
-------------------+------------+-------+--------------
acs_object | | 1 | /00
relationship | acs_object | 2 | /00/00
party | acs_object | 2 | /00/01
person | party | 3 | /00/01/00
user | person | 4 | /00/01/00/00
group | party | 3 | /00/01/01
membership_rel | acs_object | 2 | /00/02
composition_rel | acs_object | 2 | /00/03
journal_entry | acs_object | 2 | /00/04
site_node | acs_object | 2 | /00/05
(10 rows)
The sub-tree rooted at the 'party' object_type can be selected as follows:
select repeat(' ',(tree_level(sortkey) - 1)*2) || object_type as object_type,
supertype,
tree_level(sortkey) as level,
sortkey
from acs_object_types
where sortkey like (select sortkey || '%'
from acs_object_types
where object_type = 'party')
order by sortkey;
object_type | supertype | level | sortkey
-------------+------------+-------+--------------
party | acs_object | 2 | /00/01
person | party | 3 | /00/01/00
user | person | 4 | /00/01/00/00
group | party | 3 | /00/01/01
(4 rows)
And all of the ancestors of the 'user' object_type can be selected using the following join:
select repeat(' ',(tree_level(o2.sortkey) - 1)*2) || o2.object_type as object_type,
o2.supertype,
tree_level(o2.sortkey) as level,
o2.sortkey
from acs_object_types o1, acs_object_types o2
where o1.object_type = 'user'
and o2.sortkey <= o1.sortkey
and o1.sortkey like (o2.sortkey || '%')
order by sortkey;
object_type | supertype | level | sortkey
-------------+------------+-------+--------------
acs_object | | 1 | /00
party | acs_object | 2 | /00/01
person | party | 3 | /00/01/00
user | person | 4 | /00/01/00/00
(4 rows)
Leaf node indication can be obtained by using a sub-select in the target list:
select repeat(' ',(tree_level(sortkey) - 1)*2) || object_type as object_type,
supertype,
tree_level(sortkey) as level,
sortkey,
(select case when count(*) = 0 then 't' else 'f' end
from acs_object_types
where supertype = ot.object_type) as is_leaf_p
from acs_object_types ot
order by sortkey;
object_type | supertype | level | sortkey | is_leaf_p
-------------------+------------+-------+--------------+-----------
acs_object | | 1 | /00 | f
relationship | acs_object | 2 | /00/00 | t
party | acs_object | 2 | /00/01 | f
person | party | 3 | /00/01/00 | f
user | person | 4 | /00/01/00/00 | t
group | party | 3 | /00/01/01 | t
membership_rel | acs_object | 2 | /00/02 | t
composition_rel | acs_object | 2 | /00/03 | t
journal_entry | acs_object | 2 | /00/04 | t
site_node | acs_object | 2 | /00/05 | t
(10 rows)