628 lines
20 KiB
PL/PgSQL
628 lines
20 KiB
PL/PgSQL
create or replace function to_localtime(ts timestamp with time zone)
|
|
returns time with time zone
|
|
as $$
|
|
select ts::time at time zone 'US/Eastern';
|
|
$$ language SQL;
|
|
|
|
create or replace function get_bin_num(x numeric, width int)
|
|
returns int
|
|
as $$
|
|
-- select (floor(x/width)*width)::int;
|
|
select floor(x/width)::int;
|
|
$$ language SQL;
|
|
|
|
CREATE OR REPLACE FUNCTION bin_ranges(tname text, cname text, width int)
|
|
RETURNS TABLE(bin_number int, bin_interval text)
|
|
AS $func$
|
|
BEGIN
|
|
RETURN QUERY EXECUTE format('
|
|
with recursive
|
|
no_bins_t as (
|
|
select min(get_bin_num(%1$s,%2$s)) as bin_number from %3$s a
|
|
|
|
union all
|
|
|
|
select bin_number + 1 as bin_number from no_bins_t
|
|
where
|
|
bin_number < (select max(get_bin_num(%1$s,%2$s)) + 1 from %3$s a)
|
|
)
|
|
|
|
select
|
|
bin_number,
|
|
concat(bin_number*%2$s, ''-'', bin_number*%2$s + %2$s - 1) as bin_interval
|
|
from no_bins_t
|
|
order by no_bins_t.bin_number;',
|
|
cname,
|
|
width,
|
|
tname
|
|
);
|
|
END
|
|
$func$ LANGUAGE plpgsql;
|
|
|
|
CREATE OR REPLACE FUNCTION histogram(tname text, cname text, width int)
|
|
RETURNS TABLE(bin_number int, bin_interval text, count bigint)
|
|
AS $func$
|
|
BEGIN
|
|
RETURN QUERY EXECUTE format('
|
|
with binned as (
|
|
select
|
|
get_bin_num(%1$s,%2$s) as ab_floor,
|
|
count(*) as count
|
|
from %3$s a
|
|
group by ab_floor
|
|
order by ab_floor
|
|
)
|
|
|
|
select
|
|
bin_number,
|
|
bin_interval,
|
|
case when b.count is null then 0 else b.count end as count
|
|
from bin_ranges(''%3$s'',''%1$s'',%2$s) r
|
|
left join binned b on r.bin_number=b.ab_floor
|
|
order by r.bin_number;',
|
|
cname,
|
|
width,
|
|
tname
|
|
);
|
|
END
|
|
$func$ LANGUAGE plpgsql;
|
|
|
|
create or replace procedure make_vis_tables()
|
|
language plpgsql
|
|
as $$
|
|
|
|
declare
|
|
begin
|
|
|
|
create schema if not exists viz;
|
|
|
|
create temporary table _scheduled_timestamps as
|
|
select ts.* from planning_entries pe
|
|
join timestamps ts using (timestamp_id)
|
|
where
|
|
pe.planning_type = 'scheduled';
|
|
|
|
create temporary table _deadline_timestamps as
|
|
select ts.* from planning_entries pe
|
|
join timestamps ts using (timestamp_id)
|
|
where
|
|
pe.planning_type = 'deadline';
|
|
|
|
create temporary table _closed_timestamps as
|
|
select ts.* from planning_entries pe
|
|
join timestamps ts using (timestamp_id)
|
|
where
|
|
pe.planning_type = 'closed';
|
|
|
|
create temporary table _tags as
|
|
select hc.headline_id, t.tag from headline_tags t
|
|
join headline_closures hc on hc.parent_id = t.headline_id
|
|
union
|
|
select h.headline_id, t.tag from file_tags t
|
|
join headlines h on h.outline_hash = t.outline_hash;
|
|
|
|
create temporary table _category_tags as
|
|
select distinct * from _tags t where t.tag like '\__%';
|
|
|
|
-- TODO there is a small chance that headlines might have two context tags
|
|
-- if different contexts are explicitly specified on two headlines at different
|
|
-- levels
|
|
create temporary table _context_tags as
|
|
select distinct * from _tags t where t.tag like '@_%';
|
|
|
|
create temporary table _resource_tags as
|
|
select distinct t.headline_id, array_agg(t.tag) as tags
|
|
from _tags t
|
|
where
|
|
t.tag like '#_%'
|
|
group by t.headline_id;
|
|
|
|
create temporary table _incubated_headlines as
|
|
select distinct * from _tags t
|
|
where
|
|
t.tag = '%inc';
|
|
|
|
create temporary table _other_tags as
|
|
select distinct t.headline_id, array_agg(t.tag) as tags
|
|
from _tags t
|
|
-- TODO this is not robust code; change will require edits in two places :(
|
|
where
|
|
not t.tag like '#_%'
|
|
and not t.tag like '@_%'
|
|
and not t.tag like '\__%'
|
|
and t.tag <> '%inc'
|
|
group by t.headline_id;
|
|
|
|
create temporary table _created_timestamps as
|
|
select
|
|
hp.headline_id,
|
|
to_timestamp(p.val_text, '[YYYY-MM-DD Dy HH24:MI]') as created_timestamp
|
|
from headline_properties hp
|
|
join properties p using (property_id)
|
|
where
|
|
p.key_text = 'CREATED'
|
|
and not p.val_text is NULL;
|
|
|
|
-- temp table to hold all headlines with scheduled repeaters
|
|
create temporary table _repeaters as
|
|
select * from headlines h
|
|
join _scheduled_timestamps ts using (headline_id)
|
|
join timestamp_repeaters tr using (timestamp_id);
|
|
|
|
create temporary table _todo_closures as
|
|
select
|
|
parents.headline_id as parent_id,
|
|
children.headline_id as child_id,
|
|
hc.depth as depth
|
|
from headline_closures hc
|
|
join headlines parents on parents.headline_id = hc.parent_id
|
|
join headlines children on children.headline_id = hc.headline_id
|
|
where
|
|
-- this ensures we only get headlines that are children of other headlines
|
|
hc.depth > 0
|
|
-- the parents must have a keyword
|
|
and not parents.keyword is NULL
|
|
-- the children must have a keyword
|
|
and not children.keyword is NULL;
|
|
|
|
create temporary table _iterator_headlines as
|
|
select distinct
|
|
children.headline_id,
|
|
children.headline_text,
|
|
children.keyword
|
|
from headline_closures hc
|
|
join headlines parents on parents.headline_id = hc.parent_id
|
|
join headlines children on children.headline_id = hc.headline_id
|
|
join headline_properties parent_hp on parents.headline_id = parent_hp.headline_id
|
|
join properties parent_props on parent_hp.property_id = parent_props.property_id
|
|
where
|
|
hc.depth > 0
|
|
and parent_props.key_text = 'PARENT_TYPE'
|
|
and parent_props.val_text = 'iterator';
|
|
|
|
create temporary table _clock_sums as
|
|
select
|
|
c.headline_id,
|
|
to_timestamp(c.time_start) as clock_start,
|
|
to_timestamp(c.time_end) as clock_end,
|
|
sum(c.time_end - c.time_start) / 60.0 as clock_sum
|
|
from clocks c
|
|
group by c.headline_id, c.time_start, c.time_end;
|
|
|
|
-- clock sums partitioned by DONE -> TODO state changes (this mostly useful for
|
|
-- repeaters although all headlines are included in this calculation)
|
|
create temporary table _partitioned_clock_sums as
|
|
-- with
|
|
-- -- this table will have all the clock times with the done->todo state changes
|
|
-- -- inserted in between (sorted by clock start time/state change time)
|
|
-- tmp as (
|
|
-- select
|
|
-- c.file_path,
|
|
-- c.headline_offset,
|
|
-- c.time_start,
|
|
-- c.time_end,
|
|
-- NULL as state_change_offset
|
|
-- from clocks c
|
|
-- union
|
|
-- select
|
|
-- le.file_path,
|
|
-- le.headline_offset,
|
|
-- le.time_logged as time_start,
|
|
-- NULL as time_end,
|
|
-- le.entry_offset as state_change_offset
|
|
-- from logbook_entries le
|
|
-- join state_changes sc using (file_path, entry_offset)
|
|
-- where
|
|
-- sc.state_old = 'TODO'
|
|
-- and (sc.state_new = 'DONE' or sc.state_new = 'CANC')
|
|
-- ),
|
|
-- -- this table will number each "group" of timestamps, where a "group" is
|
|
-- -- defined by timestamps under the same headline (eg matching file_path and
|
|
-- -- headline_offset) that are partitioned by the todo->done state change
|
|
-- -- entries (if any, if only one or none, there will only be one group under
|
|
-- -- one headline)
|
|
-- --
|
|
-- -- NOTE 1: the WHERE clause is in the next outer query since state-change rows
|
|
-- -- themselves (which are removed by the WHERE) are necessary to define the
|
|
-- -- groups)
|
|
-- --
|
|
-- -- NOTE 2: if a headline does not have any state changes, it may get the same
|
|
-- -- group index as the last group of the previous headline. This shouldn't
|
|
-- -- matter, since the GROUP BY in the outer query also takes the file_path and
|
|
-- -- headline_offset into account
|
|
-- grouped as (
|
|
-- select
|
|
-- t.file_path,
|
|
-- t.headline_offset,
|
|
-- t.time_start,
|
|
-- t.time_end,
|
|
-- t.state_change_offset,
|
|
-- sum(case when t.state_change_offset is not null then 1 end)
|
|
-- over (order by t.file_path,
|
|
-- t.headline_offset,
|
|
-- t.time_start desc,
|
|
-- t.state_change_offset desc)
|
|
-- as grp
|
|
-- from tmp t
|
|
-- ),
|
|
-- offsets as (
|
|
-- select
|
|
-- g.file_path,
|
|
-- g.headline_offset,
|
|
-- g.grp,
|
|
-- g.state_change_offset
|
|
-- from grouped g
|
|
-- where
|
|
-- not g.state_change_offset is NULL
|
|
-- )
|
|
-- select
|
|
-- g.file_path,
|
|
-- g.headline_offset,
|
|
-- min(g.time_start) as partitioned_time_start,
|
|
-- max(g.time_end) as partitioned_time_end,
|
|
-- sum(g.time_end - g.time_start) / 60.0 as partitioned_clock_sum,
|
|
-- o.state_change_offset
|
|
-- from grouped g
|
|
-- right join offsets o using (file_path, headline_offset, grp)
|
|
-- where
|
|
-- g.state_change_offset is NULL
|
|
-- group by g.file_path, g.headline_offset, g.grp, o.state_change_offset;
|
|
|
|
with
|
|
-- this table will have all the clock times with the done->todo state changes
|
|
-- inserted in between (sorted by clock start time/state change time)
|
|
tmp as (
|
|
select
|
|
c.headline_id,
|
|
c.time_start,
|
|
c.time_end,
|
|
NULL as state_change_id
|
|
from clocks c
|
|
union
|
|
select
|
|
le.headline_id,
|
|
le.time_logged as time_start,
|
|
NULL as time_end,
|
|
le.entry_id as state_change_id
|
|
from logbook_entries le
|
|
join state_changes sc using (entry_id)
|
|
where
|
|
sc.state_old = 'TODO'
|
|
and (sc.state_new = 'DONE' or sc.state_new = 'CANC')
|
|
),
|
|
-- this table will number each "group" of timestamps, where a "group" is
|
|
-- defined by timestamps under the same headline (eg matching file_path and
|
|
-- headline_offset) that are partitioned by the todo->done state change
|
|
-- entries (if any, if only one or none, there will only be one group under
|
|
-- one headline)
|
|
--
|
|
-- NOTE 1: the WHERE clause is in the next outer query since state-change rows
|
|
-- themselves (which are removed by the WHERE) are necessary to define the
|
|
-- groups)
|
|
--
|
|
-- NOTE 2: if a headline does not have any state changes, it may get the same
|
|
-- group index as the last group of the previous headline. This shouldn't
|
|
-- matter, since the GROUP BY in the outer query also takes the file_path and
|
|
-- headline_offset into account
|
|
grouped as (
|
|
select
|
|
t.headline_id,
|
|
t.time_start,
|
|
t.time_end,
|
|
t.state_change_id,
|
|
sum(case when t.state_change_id is not null then 1 end)
|
|
over (order by t.headline_id, t.time_start desc, t.state_change_id desc)
|
|
as grp
|
|
from tmp t
|
|
),
|
|
ids as (
|
|
select g.headline_id, g.grp, g.state_change_id from grouped g
|
|
where
|
|
not g.state_change_id is NULL
|
|
),
|
|
sums as (
|
|
select
|
|
g.headline_id,
|
|
g.grp,
|
|
to_timestamp(min(g.time_start)) as partitioned_clock_start,
|
|
to_timestamp(max(g.time_end)) as partitioned_clock_end,
|
|
sum(g.time_end - g.time_start) / 60.0 as partitioned_clock_sum
|
|
from grouped g
|
|
where
|
|
g.state_change_id is NULL
|
|
group by g.headline_id, g.grp
|
|
)
|
|
select
|
|
o.headline_id,
|
|
o.state_change_id,
|
|
s.partitioned_clock_start,
|
|
s.partitioned_clock_end,
|
|
s.partitioned_clock_sum
|
|
from ids o
|
|
left join sums s using (headline_id, grp);
|
|
|
|
create temporary table _habit_headlines as
|
|
select
|
|
r.*,
|
|
cs.partitioned_clock_start,
|
|
cs.partitioned_clock_end,
|
|
cs.partitioned_clock_sum,
|
|
s.state_old,
|
|
s.state_new,
|
|
to_timestamp(le.time_logged) as closed_timestamp
|
|
from _repeaters r
|
|
join headline_properties hp using (headline_id)
|
|
join properties p using (property_id)
|
|
join _partitioned_clock_sums cs using (headline_id)
|
|
left join state_changes s
|
|
on s.entry_id = cs.state_change_id
|
|
left join logbook_entries le
|
|
on le.headline_id = r.headline_id and le.entry_id = s.entry_id
|
|
where
|
|
p.key_text = 'STYLE'
|
|
and p.val_text = 'habit';
|
|
|
|
create temporary table _repeater_headlines as
|
|
select
|
|
r.*,
|
|
cs.partitioned_clock_start,
|
|
cs.partitioned_clock_end,
|
|
cs.partitioned_clock_sum,
|
|
s.state_old,
|
|
s.state_new,
|
|
to_timestamp(le.time_logged) as closed_timestamp
|
|
from _repeaters r
|
|
join _partitioned_clock_sums cs using (headline_id)
|
|
left join state_changes s
|
|
on s.entry_id = cs.state_change_id
|
|
left join logbook_entries le
|
|
on le.headline_id = r.headline_id and le.entry_id = s.entry_id
|
|
where
|
|
not exists (select * from _habit_headlines habits
|
|
where r.headline_id = habits.headline_id);
|
|
|
|
create temporary table _project_task_headlines as
|
|
select distinct h.* from _todo_closures tc
|
|
join headlines h on tc.child_id = h.headline_id
|
|
where
|
|
not exists (select * from _iterator_headlines i
|
|
where i.headline_id = h.headline_id);
|
|
|
|
-- drop table if exists viz.project_toplevel_headlines;
|
|
-- create table viz.project_toplevel_headlines as
|
|
-- select distinct
|
|
-- h.file_path,
|
|
-- h.headline_offset,
|
|
-- h.keyword,
|
|
-- h.headline_text
|
|
-- from _todo_closures tc0
|
|
-- join headlines h
|
|
-- on tc0.file_path = h.file_path and tc0.parent_offset = h.headline_offset
|
|
-- where
|
|
-- tc0.depth = 1
|
|
-- and not exists (select * from _todo_closures tc1
|
|
-- where tc1.child_offset = tc0.parent_offset);
|
|
|
|
create temporary table _task_parent_mappings as
|
|
with
|
|
maxdepth as (
|
|
select t.child_id, max(t.depth) as depth
|
|
from _todo_closures t
|
|
group by t.child_id
|
|
)
|
|
select tc.parent_id, tc.child_id from maxdepth m
|
|
join _todo_closures tc using (child_id, depth);
|
|
|
|
-- -- TODO this will be more useful if I can also link it easily with the
|
|
-- -- toplevel headline
|
|
-- drop table if exists viz.project_parent_headlines;
|
|
-- create table viz.project_parent_headlines as
|
|
-- select distinct
|
|
-- h.file_path,
|
|
-- h.headline_offset,
|
|
-- h.keyword,
|
|
-- h.headline_text
|
|
-- from _todo_closures tc
|
|
-- join headlines h
|
|
-- on tc.file_path = h.file_path and tc.parent_offset = h.headline_offset
|
|
-- where
|
|
-- not exists
|
|
-- (select * from _iterator_headlines i
|
|
-- where
|
|
-- i.file_path = h.file_path
|
|
-- and i.headline_offset = h.headline_offset)
|
|
-- and not exists
|
|
-- (select * from viz.project_toplevel_headlines t
|
|
-- where
|
|
-- t.file_path = h.file_path
|
|
-- and t.headline_offset = h.headline_offset);
|
|
|
|
|
|
create temporary table _atomic_tasks as
|
|
select * from headlines h
|
|
where
|
|
not h.keyword is NULL
|
|
and not exists (select * from _project_task_headlines pt
|
|
where pt.headline_id = h.headline_id)
|
|
-- and not exists (select * from viz.project_parent_headlines pp
|
|
-- where pp.headline_id = h.headline_id)
|
|
-- and not exists (select * from viz.project_toplevel_headlines pl
|
|
-- where pl.headline_id = h.headline_id)
|
|
and not exists (select * from _task_parent_mappings m
|
|
where m.parent_id = h.headline_id)
|
|
and not exists (select * from _repeaters r
|
|
where r.headline_id = h.headline_id)
|
|
and not exists (select * from _iterator_headlines i
|
|
where i.headline_id = h.headline_id);
|
|
|
|
create temporary table _iterator_tasks as
|
|
select * from headlines h
|
|
where
|
|
not h.keyword is NULL
|
|
and exists (select * from _iterator_headlines i
|
|
where i.headline_id = h.headline_id)
|
|
and not exists (select * from _task_parent_mappings m
|
|
where m.parent_id = h.headline_id);
|
|
|
|
-- drop table if exists viz.atomic_tasks;
|
|
-- create table viz.atomic_tasks as
|
|
-- select
|
|
-- a.*,
|
|
-- cs.clock_sum,
|
|
-- to_timestamp(s.time_start) as scheduled_time,
|
|
-- to_timestamp(d.time_start) as deadline_time,
|
|
-- to_timestamp(c.time_start) as closed_time,
|
|
-- ct.tag as category,
|
|
-- xt.tag as context,
|
|
-- rt.tags as resources,
|
|
-- t.tags as tags,
|
|
-- cr.created_timestamp,
|
|
-- (ih.tag is not NULL) as incubated
|
|
-- from _atomic_tasks a
|
|
-- left join _clock_sums cs using (headline_offset, file_path)
|
|
-- left join _scheduled_timestamps s using (file_path, headline_offset)
|
|
-- left join _deadline_timestamps d using (file_path, headline_offset)
|
|
-- left join _closed_timestamps c using (file_path, headline_offset)
|
|
-- left join _category_tags ct using (file_path, headline_offset)
|
|
-- left join _context_tags xt using (file_path, headline_offset)
|
|
-- left join _resource_tags rt using (file_path, headline_offset)
|
|
-- left join _other_tags t using (file_path, headline_offset)
|
|
-- left join _created_timestamps cr using (file_path, headline_offset)
|
|
-- left join _incubated_headlines ih using (file_path, headline_offset);
|
|
|
|
-- TODO this doesn't have iterators (yet)
|
|
drop table if exists viz.all_tasks;
|
|
create table viz.all_tasks as
|
|
with
|
|
all_tasks as (
|
|
select
|
|
r.headline_id,
|
|
r.state_new as keyword,
|
|
r.partitioned_clock_start as clock_start,
|
|
r.partitioned_clock_end as clock_end,
|
|
r.partitioned_clock_sum as clock_sum,
|
|
r.closed_timestamp,
|
|
'repeater' as task_type
|
|
from _repeater_headlines r
|
|
union all
|
|
select
|
|
h.headline_id,
|
|
h.state_new as keyword,
|
|
h.partitioned_clock_start as clock_start,
|
|
h.partitioned_clock_end as clock_end,
|
|
h.partitioned_clock_sum as clock_sum,
|
|
h.closed_timestamp,
|
|
'habit' as task_type
|
|
from _habit_headlines h
|
|
union all
|
|
-- TODO this is redundant to have 'tasks' made twice from different sources
|
|
select
|
|
a.headline_id,
|
|
a.keyword,
|
|
cs.clock_start,
|
|
cs.clock_end,
|
|
cs.clock_sum,
|
|
to_timestamp(c.time_start) as closed_timestamp,
|
|
'atomic' as task_type
|
|
from _atomic_tasks a
|
|
left join _clock_sums cs using (headline_id)
|
|
left join _closed_timestamps c using (headline_id)
|
|
union all
|
|
select
|
|
p.headline_id,
|
|
p.keyword,
|
|
cs.clock_start,
|
|
cs.clock_end,
|
|
cs.clock_sum,
|
|
to_timestamp(c.time_start) as closed_timestamp,
|
|
'project' as task_type
|
|
from _project_task_headlines p
|
|
left join _clock_sums cs using (headline_id)
|
|
left join _closed_timestamps c using (headline_id)
|
|
union all
|
|
select
|
|
i.headline_id,
|
|
i.keyword,
|
|
cs.clock_start,
|
|
cs.clock_end,
|
|
cs.clock_sum,
|
|
to_timestamp(c.time_start) as closed_timestamp,
|
|
'iterator' as task_type
|
|
from _iterator_tasks i
|
|
left join _clock_sums cs using (headline_id)
|
|
left join _closed_timestamps c using (headline_id)
|
|
)
|
|
select
|
|
f.file_path,
|
|
a.*,
|
|
tm.parent_id as project_parent_id,
|
|
to_timestamp(s.time_start) as scheduled_timestamp,
|
|
to_timestamp(d.time_start) as deadline_timestamp,
|
|
h.headline_text,
|
|
h.effort,
|
|
h.priority,
|
|
h.is_archived,
|
|
h.is_commented,
|
|
h.content,
|
|
ct.tag as category,
|
|
xt.tag as context,
|
|
rt.tags as resources,
|
|
t.tags as tags,
|
|
cr.created_timestamp,
|
|
(ih.tag is not NULL) as incubated
|
|
from all_tasks a
|
|
join headlines h using (headline_id)
|
|
join file_metadata f using (outline_hash)
|
|
left join _scheduled_timestamps s using (headline_id)
|
|
left join _deadline_timestamps d using (headline_id)
|
|
left join _category_tags ct using (headline_id)
|
|
left join _context_tags xt using (headline_id)
|
|
left join _resource_tags rt using (headline_id)
|
|
left join _other_tags t using (headline_id)
|
|
left join _created_timestamps cr using (headline_id)
|
|
left join _incubated_headlines ih using (headline_id)
|
|
left join _task_parent_mappings tm on tm.child_id = h.headline_id
|
|
order by a.headline_id, a.closed_timestamp desc;
|
|
|
|
|
|
drop table if exists viz.sleep_length;
|
|
create table viz.sleep_length as
|
|
with
|
|
tmp as (
|
|
select distinct
|
|
to_localtime(clock_start) as time_start_clock,
|
|
clock_sum,
|
|
clock_start,
|
|
clock_end
|
|
from viz.all_tasks
|
|
where
|
|
headline_text = 'sleep')
|
|
select distinct
|
|
clock_start as sleep_timestamp,
|
|
clock_sum / 60.0 as sleep_hours,
|
|
time_start_clock as sleep_start_clock,
|
|
-- day of week that sleep starts; subtract 12 hours off timestamp to count
|
|
-- bedtime after midnight as starting on the previous day
|
|
extract(dow from (clock_start - (12||' hours')::interval) at time zone 'US/Eastern')
|
|
as sleep_start_day,
|
|
-- offset from target bedtime start (assume target bedtime is 23:45)
|
|
mod((extract(hour from time_start_clock) * 60
|
|
+ extract(minute from time_start_clock) + 15 + 720)::bigint,
|
|
1440) / 1440.0 * 24 - 12 as sleep_start_offset from tmp;
|
|
|
|
drop table if exists viz.check_email_2;
|
|
create table viz.check_email_2 as
|
|
-- ASSUME that the repeater has the correct time of day, but may be offset
|
|
-- by a multiple of 7 days
|
|
select
|
|
clock_start,
|
|
extract(epoch from clock_start::time - scheduled_timestamp::time)/60 as offset_minutes
|
|
from viz.all_tasks
|
|
where
|
|
headline_text = 'check email 2'
|
|
order by clock_start desc;
|
|
|
|
end
|
|
|
|
$$;
|