// Code generated by "make migrations"; DO NOT EDIT.
package migrations

var postgresMigrations = map[string]*fakeFile{
	"migrations": {
		name: "migrations",
	},
	"migrations/01_domain_types.down.sql": {
		name: "01_domain_types.down.sql",
		bytes: []byte(`
begin;

drop domain wt_timestamp;
drop domain wt_public_id;
drop domain wt_private_id;
drop domain wt_scope_id;
drop domain wt_user_id;
drop domain wt_version;

drop function default_create_time;
drop function update_time_column;
drop function update_version_column;
drop function immutable_columns;

commit;

`),
	},
	"migrations/01_domain_types.up.sql": {
		name: "01_domain_types.up.sql",
		bytes: []byte(`
begin;

create domain wt_public_id as text
check(
  length(trim(value)) > 10
);
comment on domain wt_public_id is
'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62';

create domain wt_private_id as text
not null
check(
  length(trim(value)) > 10
);
comment on domain wt_private_id is
'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62';

create domain wt_scope_id as text
check(
  length(trim(value)) > 10 or value = 'global'
);
comment on domain wt_scope_id is
'"global" or random ID generated with github.com/hashicorp/vault/sdk/helper/base62';

create domain wt_user_id as text
not null
check(
  length(trim(value)) > 10 or value = 'u_anon' or value = 'u_auth' or value = 'u_recovery'
);
comment on domain wt_scope_id is
'"u_anon", "u_auth", or random ID generated with github.com/hashicorp/vault/sdk/helper/base62';

create domain wt_role_id as text
not null
check(
  length(trim(value)) > 10 or value = 'r_default'
);
comment on domain wt_scope_id is
'"r_default", or random ID generated with github.com/hashicorp/vault/sdk/helper/base62';

create domain wt_timestamp as
  timestamp with time zone
  default current_timestamp;
comment on domain wt_timestamp is
'Standard timestamp for all create_time and update_time columns';

create or replace function
  update_time_column()
  returns trigger
as $$
begin
  if row(new.*) is distinct from row(old.*) then
    new.update_time = now();
    return new;
  else
    return old;
  end if;
end;
$$ language plpgsql;

comment on function
  update_time_column()
is
  'function used in before update triggers to properly set update_time columns';
  
create or replace function
  default_create_time()
  returns trigger
as $$
begin
  if new.create_time is distinct from now() then
    raise warning 'create_time cannot be set to %', new.create_time;
    new.create_time = now();
  end if;
  return new;
end;
$$ language plpgsql;

comment on function
  default_create_time()
is
  'function used in before insert triggers to set create_time column to now';


create domain wt_version as bigint
  default 1
  not null
  check(
   value > 0
  );
comment on domain wt_version is
'standard column for row version';

-- update_version_column() will increment the version column whenever row data
-- is updated and should only be used in an update after trigger.  This function
-- will overwrite any explicit updates to the version column. The function
-- accepts an optional parameter of 'private_id' for the tables primary key.
create or replace function
  update_version_column()
  returns trigger
as $$
begin
  if pg_trigger_depth() = 1 then
    if row(new.*) is distinct from row(old.*) then
      if tg_nargs = 0 then
        execute format('update %I set version = $1 where public_id = $2', tg_relid::regclass) using old.version+1, new.public_id;
        new.version = old.version + 1;
        return new;
      end if;
      if tg_argv[0] = 'private_id' then
        execute format('update %I set version = $1 where private_id = $2', tg_relid::regclass) using old.version+1, new.private_id;
        new.version = old.version + 1;
        return new;
      end if;
    end if;
  end if;
  return new;
end;
$$ language plpgsql;

comment on function
  update_version_column()
is
  'function used in after update triggers to properly set version columns';

-- immutable_columns() will make the column names immutable which are passed as
-- parameters when the trigger is created. It raises error code 23601 which is a
-- class 23 integrity constraint violation: immutable column  
create or replace function
  immutable_columns()
  returns trigger
as $$
declare 
	col_name text; 
	new_value text;
	old_value text;
begin
  foreach col_name in array tg_argv loop
    execute format('SELECT $1.%I', col_name) into new_value using new;
    execute format('SELECT $1.%I', col_name) into old_value using old;
  	if new_value is distinct from old_value then
      raise exception 'immutable column: %.%', tg_table_name, col_name using
        errcode = '23601', 
        schema = tg_table_schema,
        table = tg_table_name,
        column = col_name;
  	end if;
  end loop;
  return new;
end;
$$ language plpgsql;

comment on function
  immutable_columns()
is
  'function used in before update triggers to make columns immutable';

commit;

`),
	},
	"migrations/02_oplog.down.sql": {
		name: "02_oplog.down.sql",
		bytes: []byte(`
begin;

drop table oplog_metadata cascade;
drop table oplog_ticket cascade;
drop table oplog_entry cascade;

commit;

`),
	},
	"migrations/02_oplog.up.sql": {
		name: "02_oplog.up.sql",
		bytes: []byte(`
begin;

-- TODO (jimlambrt 7/2020) remove update_time
create table if not exists oplog_entry (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  version text not null,
  aggregate_name text not null,
  "data" bytea not null
);

create trigger 
  update_time_column 
before 
update on oplog_entry 
  for each row execute procedure update_time_column();

create trigger 
  default_create_time_column
before
insert on oplog_entry
  for each row execute procedure default_create_time();

-- oplog_entry is immutable.
create trigger 
  immutable_columns
before
update on oplog_entry
  for each row execute procedure immutable_columns('id','update_time','create_time','version','aggregate_name', 'data');

create table if not exists oplog_ticket (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  "name" text not null unique,
  "version" bigint not null
);

create trigger 
  update_time_column 
before 
update on oplog_ticket 
  for each row execute procedure update_time_column();

create trigger 
  default_create_time_column
before
insert on oplog_ticket
  for each row execute procedure default_create_time();

-- oplog_ticket: only allow updates to: version and update_time
create trigger 
  immutable_columns
before
update on oplog_ticket
  for each row execute procedure immutable_columns('id','create_time','name');
  
-- TODO (jimlambrt 7/2020) remove update_time
create table if not exists oplog_metadata (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  entry_id bigint not null references oplog_entry(id) on delete cascade on update cascade,
  "key" text not null,
  value text null
);

create trigger 
  update_time_column 
before 
update on oplog_metadata 
  for each row execute procedure update_time_column();

create trigger 
  default_create_time_column
before
insert on oplog_metadata 
  for each row execute procedure default_create_time();

 -- oplog_metadata is immutable
create trigger 
  immutable_columns
before
update on oplog_metadata
  for each row execute procedure immutable_columns('id','create_time','update_time','entry_id','key','value');

create index if not exists idx_oplog_metatadata_key on oplog_metadata(key);

create index if not exists idx_oplog_metatadata_value on oplog_metadata(value);

insert into oplog_ticket (name, version)
values
  ('auth_token', 1),
  ('default', 1),
  ('iam_scope', 1),
  ('iam_user', 1),
  ('iam_group', 1),
  ('iam_group_member', 1),
  ('iam_role', 1),
  ('iam_role_grant', 1),
  ('iam_group_role', 1),
  ('iam_user_role', 1),
  ('db_test_user', 1),
  ('db_test_car', 1),
  ('db_test_rental', 1),
  ('db_test_scooter', 1),
  ('auth_account', 1),
  ('iam_principal_role', 1);
  

commit;


`),
	},
	"migrations/03_db.down.sql": {
		name: "03_db.down.sql",
		bytes: []byte(`
begin;

drop table db_test_rental cascade;
drop table db_test_car cascade;
drop table db_test_user cascade;
drop table db_test_scooter cascade;

commit;

`),
	},
	"migrations/03_db.up.sql": {
		name: "03_db.up.sql",
		bytes: []byte(`
begin;

-- create test tables used in the unit tests for the internal/db package 
-- these tables (db_test_user, db_test_car, db_test_rental, db_test_scooter) are
-- not part of the boundary domain model... they are simply used for testing
-- the internal/db package 
create table if not exists db_test_user (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  public_id text not null unique,
  name text unique,
  phone_number text,
  email text,
  version wt_version
);

create trigger 
  update_time_column 
before 
update on db_test_user 
  for each row execute procedure update_time_column();

-- define the immutable fields for db_test_user
create trigger 
  immutable_columns
before
update on db_test_user
  for each row execute procedure immutable_columns('create_time');

create trigger 
  default_create_time_column
before
insert on db_test_user 
  for each row execute procedure default_create_time();

create trigger 
  update_version_column
after update on db_test_user
  for each row execute procedure update_version_column();
  
create table if not exists db_test_car (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  public_id text not null unique,
  name text unique,
  model text,
  mpg smallint
);

create trigger 
  update_time_column 
before 
update on db_test_car 
  for each row execute procedure update_time_column();

-- define the immutable fields for db_test_car
create trigger 
  immutable_columns
before
update on db_test_car
  for each row execute procedure immutable_columns('create_time');

create trigger 
  default_create_time_column
before
insert on db_test_car
  for each row execute procedure default_create_time();

create table if not exists db_test_rental (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  public_id text not null unique,
  name text unique,
  user_id bigint not null references db_test_user(id),
  car_id bigint not null references db_test_car(id)
);

create trigger 
  update_time_column 
before 
update on db_test_rental 
  for each row execute procedure update_time_column();

-- define the immutable fields for db_test_rental
create trigger 
  immutable_columns
before
update on db_test_rental
  for each row execute procedure immutable_columns('create_time');

create trigger 
  default_create_time_column
before
insert on db_test_rental
  for each row execute procedure default_create_time();


create table if not exists db_test_scooter (
  id bigint generated always as identity primary key,
  create_time wt_timestamp,
  update_time wt_timestamp,
  private_id text not null unique,
  name text unique,
  model text,
  mpg smallint
);

create trigger 
  update_time_column 
before 
update on db_test_scooter 
  for each row execute procedure update_time_column();


-- define the immutable fields for db_test_scooter
create trigger 
  immutable_columns
before
update on db_test_scooter
  for each row execute procedure immutable_columns('create_time');

create trigger 
  default_create_time_column
before
insert on db_test_scooter
  for each row execute procedure default_create_time();

commit;

`),
	},
	"migrations/06_iam.down.sql": {
		name: "06_iam.down.sql",
		bytes: []byte(`
BEGIN;

drop table iam_group cascade;
drop table iam_user cascade;
drop table iam_scope_project cascade;
drop table iam_scope_org cascade;
drop table iam_scope_global cascade;
drop table iam_scope cascade;
drop table iam_scope_type_enm cascade;
drop table iam_role cascade;
drop view iam_principal_role cascade;
drop table iam_group_role cascade;
drop table iam_user_role cascade;
drop table iam_group_member_user cascade;
drop view iam_group_member cascade;
drop table iam_role_grant cascade;

drop function iam_sub_names cascade;
drop function iam_immutable_scope_type_func cascade;
drop function iam_sub_scopes_func cascade;
drop function iam_immutable_role_principal cascade;
drop function iam_user_role_scope_check cascade;
drop function iam_group_role_scope_check cascade;
drop function iam_group_member_scope_check cascade;
drop function iam_immutable_group_member cascade;
drop function get_scoped_member_id cascade;
drop function grant_scope_id_valid cascade;
drop function disallow_global_scope_deletion cascade;
drop function user_scope_id_valid cascade;
drop function iam_immutable_role_grant cascade;
drop function disallow_iam_predefined_user_deletion cascade;
drop function recovery_user_not_allowed cascade;

COMMIT;

`),
	},
	"migrations/06_iam.up.sql": {
		name: "06_iam.up.sql",
		bytes: []byte(`
begin;

create table iam_scope_type_enm (
  string text not null primary key
    constraint only_predefined_scope_types_allowed
    check(string in ('unknown', 'global', 'org', 'project'))
);

insert into iam_scope_type_enm (string)
values
  ('unknown'),
  ('global'),
  ('org'),
  ('project');

 -- define the immutable fields of iam_scope_type_enm
create trigger 
  immutable_columns
before
update on iam_scope_type_enm
  for each row execute procedure immutable_columns('string');

create table iam_scope (
    public_id wt_scope_id primary key,
    create_time wt_timestamp,
    update_time wt_timestamp,
    name text,
    type text not null
      references iam_scope_type_enm(string)
      constraint only_known_scope_types_allowed
      check(
        (
          type = 'global'
          and parent_id is null
        )
        or (
          type = 'org'
          and parent_id = 'global'
        )
        or (
          type = 'project'
          and parent_id is not null
          and parent_id != 'global'
        )
      ),
    description text,
    parent_id text references iam_scope(public_id) on delete cascade on update cascade,

    -- version allows optimistic locking of the role when modifying the role
    -- itself and when modifying dependent items like principal roles.
    version wt_version
  );

create table iam_scope_global (
    scope_id wt_scope_id primary key
      references iam_scope(public_id)
      on delete cascade
      on update cascade
      constraint only_one_global_scope_allowed
      check(
        scope_id = 'global'
      ),
    name text unique
);

create table iam_scope_org (
  scope_id wt_scope_id primary key
    references iam_scope(public_id)
    on delete cascade
    on update cascade,
  parent_id wt_scope_id
    not null
    references iam_scope_global(scope_id)
    on delete cascade
    on update cascade,
  name text,
  unique (parent_id, name)
);

create table iam_scope_project (
    scope_id wt_scope_id
      not null
      references iam_scope(public_id)
      on delete cascade
      on update cascade,
    parent_id wt_public_id not null references iam_scope_org(scope_id) on delete cascade on update cascade,
    name text,
    unique(parent_id, name),
    unique(scope_id),
    primary key(scope_id, parent_id)
  );

create or replace function 
  iam_sub_scopes_func() 
  returns trigger
as $$ 
declare parent_type int;
begin
  if new.type = 'global' then
    insert into iam_scope_global (scope_id, name)
    values
      (new.public_id, new.name);
    return new;
  end if;
  if new.type = 'org' then
    insert into iam_scope_org (scope_id, parent_id, name)
    values
      (new.public_id, new.parent_id, new.name);
    return new;
  end if;
  if new.type = 'project' then
    insert into iam_scope_project (scope_id, parent_id, name)
    values
      (new.public_id, new.parent_id, new.name);
    return new;
  end if;
  raise exception 'unknown scope type';
end;
$$ language plpgsql;

create trigger 
  iam_scope_insert
after
insert on iam_scope 
  for each row execute procedure iam_sub_scopes_func();

create or replace function
  disallow_global_scope_deletion()
  returns trigger
as $$
begin
  if old.type = 'global' then
    raise exception 'deletion of global scope not allowed';
  end if;
  return old;
end;
$$ language plpgsql;

create trigger
  iam_scope_disallow_global_deletion
before
delete on iam_scope
  for each row execute procedure disallow_global_scope_deletion();

create trigger 
  update_time_column 
before update on iam_scope 
  for each row execute procedure update_time_column();
  
create trigger 
  default_create_time_column
before
insert on iam_scope
  for each row execute procedure default_create_time();

create trigger
  update_version_column
after update on iam_scope
  for each row execute procedure update_version_column();

 -- define the immutable fields for iam_scope
create trigger 
  immutable_columns
before
update on iam_scope
  for each row execute procedure immutable_columns('public_id', 'create_time', 'type', 'parent_id');

 -- define the immutable fields of iam_scope_global
create trigger 
  immutable_columns
before
update on iam_scope_global
  for each row execute procedure immutable_columns('scope_id');

 -- define the immutable fields of iam_scope_org
create trigger 
  immutable_columns
before
update on iam_scope_org
  for each row execute procedure immutable_columns('scope_id');

 -- define the immutable fields of iam_scope_project
create trigger 
  immutable_columns
before
update on iam_scope_project
  for each row execute procedure immutable_columns('scope_id');


-- iam_sub_names will allow us to enforce the different name constraints for
-- orgs and projects via a before update trigger on the iam_scope
-- table. 
create or replace function 
  iam_sub_names() 
  returns trigger
as $$ 
begin
  if new.name != old.name then
    if new.type = 'global' then
      update iam_scope_global set name = new.name where scope_id = old.public_id;
      return new;
    end if;
    if new.type = 'org' then
      update iam_scope_org set name = new.name where scope_id = old.public_id;
      return new;
    end if;
    if new.type = 'project' then
      update iam_scope_project set name = new.name where scope_id = old.public_id;
      return new;
    end if;
    raise exception 'unknown scope type';
  end if;
  return new;
end;
$$ language plpgsql;

create trigger 
  iam_sub_names 
before 
update on iam_scope
  for each row execute procedure iam_sub_names();

insert into iam_scope (public_id, name, type, description)
  values ('global', 'global', 'global', 'Global Scope');


create table iam_user (
    public_id wt_user_id
      primary key,
    create_time wt_timestamp,
    update_time wt_timestamp,
    name text,
    description text,
    scope_id wt_scope_id
      not null
      references iam_scope(public_id)
      on delete cascade
      on update cascade,
    unique(name, scope_id),
    version wt_version,

    -- The order of columns is important for performance. See:
    -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
    -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field
    unique(scope_id, public_id)
  );

create or replace function
  user_scope_id_valid()
  returns trigger
as $$
begin
  perform from iam_scope where public_id = new.scope_id and type in ('global', 'org');
  if not found then
    raise exception 'invalid scope type for user creation';
  end if;
  return new;
end;
$$ language plpgsql;

create or replace function
  grant_scope_id_valid()
  returns trigger
as $$
declare parent_scope_id text;
declare role_scope_type text;
begin
  -- There is a not-null constraint so ensure that if the value passed in is
  -- empty we simply set to the scope ID
  if new.grant_scope_id = '' or new.grant_scope_id is null then
    new.grant_scope_id = new.scope_id;
  end if;
  -- If the scopes match, it's allowed
  if new.grant_scope_id = new.scope_id then
    return new;
  end if;
  -- Fetch the type of scope
  select isc.type from iam_scope isc where isc.public_id = new.scope_id into role_scope_type;
  -- Always allowed
  if role_scope_type = 'global' then
    return new;
  end if;
  -- Never allowed; the case where it's set to the same scope ID as the project
  -- itself is covered above
  if role_scope_type = 'project' then
    raise exception 'invalid to set grant_scope_id to non-same scope_id when role scope type is project';
  end if;
  if role_scope_type = 'org' then
    -- Look up the parent scope ID for the scope ID given
    select isc.parent_id from iam_scope isc where isc.public_id = new.grant_scope_id into parent_scope_id;
    -- Allow iff the grant scope ID's parent matches the role's scope ID; that
    -- is, match if the role belongs to a direct child scope of this
    -- org
    if parent_scope_id = new.scope_id then
      return new;
    end if;
    raise exception 'grant_scope_id is not a child project of the role scope';
  end if;
  raise exception 'unknown scope type';
end;
$$ language plpgsql;

create or replace function
  disallow_iam_predefined_user_deletion()
  returns trigger
as $$
begin
  if old.public_id = 'u_anon' then
    raise exception 'deletion of anonymous user not allowed';
  end if;
  if old.public_id = 'u_auth' then
    raise exception 'deletion of authenticated user not allowed';
  end if;
    if old.public_id = 'u_recovery' then
    raise exception 'deletion of recovery user not allowed';
  end if;
  return old;
end;
$$ language plpgsql;

create trigger
  update_version_column
after update on iam_user
  for each row execute procedure update_version_column();

create trigger
  ensure_user_scope_id_valid
before
insert or update on iam_user
  for each row execute procedure user_scope_id_valid();

create trigger 
  update_time_column 
before update on iam_user 
  for each row execute procedure update_time_column();
  
create trigger 
  default_create_time_column
before
insert on iam_user
  for each row execute procedure default_create_time();

create trigger
  iam_user_disallow_predefined_user_deletion
before
delete on iam_user
  for each row execute procedure disallow_iam_predefined_user_deletion();

-- TODO: Do we want to disallow changing the name or description?
insert into iam_user (public_id, name, description, scope_id)
  values ('u_anon', 'anonymous', 'The anonymous user matches any request, whether authenticated or not', 'global');

insert into iam_user (public_id, name, description, scope_id)
  values ('u_auth', 'authenticated', 'The authenticated user matches any user that has a valid token', 'global');

insert into iam_user (public_id, name, description, scope_id)
  values ('u_recovery', 'recovery', 'The recovery user is used for any request that was performed with the recovery KMS workflow', 'global');

 -- define the immutable fields for iam_user
create trigger
  immutable_columns
before
update on iam_user
  for each row execute procedure immutable_columns('public_id', 'create_time', 'scope_id');
  
create table iam_role (
    public_id wt_role_id primary key,
    create_time wt_timestamp,
    update_time wt_timestamp,
    name text,
    description text,
    scope_id wt_scope_id
      not null
      references iam_scope(public_id)
      on delete cascade
      on update cascade,
    grant_scope_id wt_scope_id
      not null
      references iam_scope(public_id)
      on delete cascade
      on update cascade,
    unique(name, scope_id),
    version wt_version,

    -- add unique index so a composite fk can be declared.
    unique(scope_id, public_id)
  );

  -- Grants are immutable, which is enforced via the trigger below
  create table iam_role_grant (
    create_time wt_timestamp,
    role_id wt_role_id -- pk
      references iam_role(public_id)
      on delete cascade
      on update cascade,
    canonical_grant text -- pk
      constraint canonical_grant_must_not_be_empty
      check(
        length(trim(canonical_grant)) > 0
      ),
    raw_grant text not null
      constraint raw_grant_must_not_be_empty
      check(
        length(trim(raw_grant)) > 0
      ),
    primary key(role_id, canonical_grant)
  );

-- iam_immutable_role_grant() ensures that grants assigned to roles are immutable. 
create or replace function
  iam_immutable_role_grant()
  returns trigger
as $$
begin
  raise exception 'role grants are immutable';
end;
$$ language plpgsql;

create trigger immutable_role_grant
before
update on iam_role_grant
  for each row execute procedure iam_immutable_role_grant();
  
create trigger
  default_create_time_column
before
insert on iam_role_grant
  for each row execute procedure default_create_time();

create or replace function
  disallow_r_default_deletion()
  returns trigger
as $$
begin
  if old.public_id = 'r_default' then
    raise exception 'deletion of r_default not allowed';
  end if;
  return old;
end;
$$ language plpgsql;

create trigger
  iam_role_disallow_global_deletion
before
delete on iam_role
  for each row execute procedure disallow_r_default_deletion();

create trigger 
  update_version_column
after update on iam_role
  for each row execute procedure update_version_column();

create trigger
  update_time_column 
before update on iam_role
  for each row execute procedure update_time_column();
  
create trigger 
  default_create_time_column
before
insert on iam_role
  for each row execute procedure default_create_time();

create trigger
  ensure_grant_scope_id_valid
before
insert or update on iam_role
  for each row execute procedure grant_scope_id_valid();

-- define the immutable fields for iam_role (started trigger name with "a_" so
-- it will run first)
create trigger 
  a_immutable_columns
before
update on iam_role
  for each row execute procedure immutable_columns('public_id', 'create_time', 'scope_id');

create or replace function
  recovery_user_not_allowed()
  returns trigger
as $$
declare
  new_value text;
begin
    execute format('SELECT $1.%I', tg_argv[0]) into new_value using new;
    if new_value = 'u_recovery' then
      raise exception '"u_recovery" not allowed here"';
    end if;
    return new;
end;
$$ language plpgsql;

create table iam_group (
    public_id wt_public_id
      primary key,
    create_time wt_timestamp,
    update_time wt_timestamp,
    name text,
    description text,
    scope_id wt_scope_id
      not null
      references iam_scope(public_id)
      on delete cascade
      on update cascade,
    unique(name, scope_id),
    -- version allows optimistic locking of the group when modifying the group
    -- itself and when modifying dependent items like group members. 
    version wt_version,

    -- add unique index so a composite fk can be declared.
    unique(scope_id, public_id)
  );
  
create trigger 
  update_version_column
after update on iam_group
  for each row execute procedure update_version_column();

create trigger 
  update_time_column 
before update on iam_group
  for each row execute procedure update_time_column();
  
create trigger 
  default_create_time_column
before
insert on iam_group
  for each row execute procedure default_create_time();

-- define the immutable fields for iam_group
create trigger 
  immutable_columns
before
update on iam_group
  for each row execute procedure immutable_columns('public_id', 'create_time', 'scope_id');

-- iam_user_role contains roles that have been assigned to users. Users can be
-- from any scope. The rows in this table must be immutable after insert, which
-- will be ensured with a before update trigger using
-- iam_immutable_role_principal(). 
create table iam_user_role (
  create_time wt_timestamp,
  role_id wt_role_id
    references iam_role(public_id)
    on delete cascade
    on update cascade,
  principal_id wt_user_id 
    references iam_user(public_id)
    on delete cascade
    on update cascade,
  primary key (role_id, principal_id)
  );

-- iam_group_role contains roles that have been assigned to groups. 
-- Groups can be from any scope. The rows in this table must be immutable after
-- insert, which will be ensured with a before update trigger using
-- iam_immutable_role_principal(). 
create table iam_group_role (
  create_time wt_timestamp,
  role_id wt_role_id
    references iam_role(public_id)
    on delete cascade
    on update cascade,
  principal_id wt_public_id 
    references iam_group(public_id)
    on delete cascade
    on update cascade,
  primary key (role_id, principal_id)
  );

-- get_scoped_principal_id is used by the iam_principle_role view as a convient
-- way to create <scope_id>:<principal_id> to reference principals from
-- other scopes than the role's scope. 
create or replace function get_scoped_principal_id(role_scope text, principal_scope text, principal_id text) returns text 
as $$
begin
	if role_scope = principal_scope then
		return principal_id;
	end if;
	return principal_scope || ':' || principal_id;
end;
$$ language plpgsql;

insert into iam_role (public_id, name, description, scope_id)
  values('r_default', 'Default Grants', 'Default role created on first instantiation of Boundary. It is meant to provide enough permissions for users to successfully authenticate via various client types.', 'global');
insert into iam_role_grant (role_id, canonical_grant, raw_grant)
  values
    ('r_default', 'type=scope;actions=list', 'type=scope;actions=list'),
    ('r_default', 'id=*;type=auth-method;actions=authenticate,list', 'id=*;type=auth-method;actions=authenticate,list');
insert into iam_user_role (role_id, principal_id)
  values 
    ('r_default', 'u_anon');

-- iam_principle_role provides a consolidated view all principal roles assigned
-- (user and group roles).
create view iam_principal_role as
select 
	ur.create_time, 
	ur.principal_id,
	ur.role_id,
	u.scope_id as principal_scope_id, 
	r.scope_id as role_scope_id,
	get_scoped_principal_id(r.scope_id, u.scope_id, ur.principal_id) as scoped_principal_id,
	'user' as type
from 	
	iam_user_role ur, 
	iam_role r,
	iam_user u
where
	ur.role_id = r.public_id and 
	u.public_id = ur.principal_id
union 
select 
	gr.create_time, 
	gr.principal_id,
	gr.role_id,
	g.scope_id as principal_scope_id, 
	r.scope_id as role_scope_id,
	get_scoped_principal_id(r.scope_id, g.scope_id, gr.principal_id) as scoped_principal_id,
	'group' as type
from 	
	iam_group_role gr, 
	iam_role r,
	iam_group g
where
	gr.role_id = r.public_id and 
	g.public_id = gr.principal_id;

-- iam_immutable_role_principal() ensures that roles assigned to principals are immutable. 
create or replace function
  iam_immutable_role_principal()
  returns trigger
as $$
begin
    raise exception 'roles are immutable';
end;
$$ language plpgsql;

create trigger immutable_role_principal
before
update on iam_user_role
  for each row execute procedure iam_immutable_role_principal();

create trigger 
  recovery_user_not_allowed_user_role
before
insert on iam_user_role
  for each row execute procedure recovery_user_not_allowed('principal_id');

create trigger 
  default_create_time_column
before
insert on iam_user_role
  for each row execute procedure default_create_time();

create trigger immutable_role_principal
before
update on iam_group_role
  for each row execute procedure iam_immutable_role_principal();
  
create trigger 
  default_create_time_column
before
insert on iam_group_role
  for each row execute procedure default_create_time();

-- iam_group_member_user is an association table that represents groups with
-- associated users.
create table iam_group_member_user (
  create_time wt_timestamp,
  group_id wt_public_id references iam_group(public_id) on delete cascade on update cascade,
  member_id wt_user_id references iam_user(public_id) on delete cascade on update cascade,
  primary key (group_id, member_id)
);

-- iam_immutable_group_member() ensures that group members are immutable. 
create or replace function
  iam_immutable_group_member()
  returns trigger
as $$
begin
    raise exception 'group members are immutable';
end;
$$ language plpgsql;

create trigger 
  default_create_time_column
before
insert on iam_group_member_user
  for each row execute procedure default_create_time();

create trigger iam_immutable_group_member
before
update on iam_group_member_user
  for each row execute procedure iam_immutable_group_member();

create trigger 
  recovery_user_not_allowed_group_member
before
insert on iam_group_member_user
  for each row execute procedure recovery_user_not_allowed('member_id');

-- get_scoped_principal_id is used by the iam_group_member view as a convient
-- way to create <scope_id>:<member_id> to reference members from
-- other scopes than the group's scope. 
create or replace function get_scoped_member_id(group_scope text, member_scope text, member_id text) returns text 
as $$
begin
	if group_scope = member_scope then
		return member_id;
	end if;
	return member_scope || ':' || member_id;
end;
$$ language plpgsql;

-- iam_group_member provides a consolidated view of group members.
create view iam_group_member as
select
  gm.create_time,
  gm.group_id,
  gm.member_id,
  u.scope_id as member_scope_id,
  g.scope_id as group_scope_id,
  get_scoped_member_id(g.scope_id, u.scope_id, gm.member_id) as scoped_member_id,
  'user' as type
from
  iam_group_member_user gm,
  iam_user u,
  iam_group g
where
  gm.member_id = u.public_id and
  gm.group_id = g.public_id;
  

commit;

`),
	},
	"migrations/07_auth.down.sql": {
		name: "07_auth.down.sql",
		bytes: []byte(`
begin;

  drop function update_iam_user_auth_account;
  drop function insert_auth_account_subtype;
  drop function insert_auth_method_subtype;

  drop table auth_account cascade;
  drop table auth_method cascade;

commit;

`),
	},
	"migrations/07_auth.up.sql": {
		name: "07_auth.up.sql",
		bytes: []byte(`
begin;

/*

  ┌────────────────┐               ┌────────────────┐
  │   iam_scope    │               │  auth_method   │
  ├────────────────┤               ├────────────────┤
  │ public_id (pk) │              ╱│ public_id (pk) │
  │                │┼┼───────────○─│ scope_id  (fk) │
  │                │              ╲│                │
  └────────────────┘               └────────────────┘
           ┼                                ┼
           ┼                                ┼
           │                                │
           │                                │ ▲fk1
           │                                │
           ○                                ○
          ╱│╲                              ╱│╲
  ┌────────────────┐          ┌──────────────────────────┐
  │    iam_user    │          │       auth_account       │
  ├────────────────┤          ├──────────────────────────┤
  │ public_id (pk) │          │ public_id         (pk)   │
  │ scope_id  (fk) │   ◀fk2   │ scope_id          (fk1)  │
  │                │┼○──────○┼│ auth_method_id    (fk1)  │
  │                │          │ iam_user_scope_id (fk2)  │
  └────────────────┘          │ iam_user_id       (fk2)  │
                              └──────────────────────────┘

  An iam_scope can have 0 to many iam_users.
  An iam_scope can have 0 to many auth_methods.

  An iam_user belongs to 1 iam_scope.
  An auth_method belongs to 1 iam_scope.

  An iam_user can have 0 or 1 auth_account.
  An auth_account belongs to 0 or 1 iam_user.

  An auth_method can have 0 to many auth_accounts.
  An auth_account belongs to 1 auth_account.

  An auth_account can only be associated with an iam_user in the same scope of
  the auth_account's auth_method. Including scope_id in fk1 and fk2 ensures this
  restriction is not violated.

  Design influenced by:
  https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972

*/

  -- base table for auth methods
  create table auth_method (
    public_id wt_public_id
      primary key,
    scope_id wt_scope_id
      not null
      references iam_scope(public_id)
      on delete cascade
      on update cascade,

    -- The order of columns is important for performance. See:
    -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
    -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field
    unique(scope_id, public_id)
  );


  -- base table for auth accounts
  create table auth_account (
    public_id wt_public_id
      primary key,
    auth_method_id wt_public_id
      not null,
    scope_id wt_scope_id
      not null,
    iam_user_id wt_public_id,
    -- The auth_account can only be assigned to an iam_user in the same scope as
    -- the auth_method the auth_account belongs to. A separate column for
    -- iam_user's scope id is needed because using the scope_id column in the
    -- foreign key constraint causes an error when the iam_user is deleted but
    -- the auth_account still exists. This is a valid scenario since the
    -- lifetime of the auth_account is tied to the auth_method not the iam_user.
    iam_user_scope_id wt_scope_id,
      constraint user_and_auth_account_in_same_scope
      check(
        (iam_user_id is null and iam_user_scope_id is null)
        or
        (iam_user_id is not null and (iam_user_scope_id = scope_id))
      ),
    -- including scope_id in fk1 and fk2 ensures the scope_id of the owning
    -- auth_method and the scope_id of the owning iam_user are the same
    foreign key (scope_id, auth_method_id) -- fk1
      references auth_method (scope_id, public_id)
      on delete cascade
      on update cascade,
    foreign key (iam_user_scope_id, iam_user_id) -- fk2
      references iam_user (scope_id, public_id)
      on delete set null
      on update cascade,
    unique(scope_id, auth_method_id, public_id)
  );

  create or replace function
    insert_auth_method_subtype()
    returns trigger
  as $$
  begin
    insert into auth_method
      (public_id, scope_id)
    values
      (new.public_id, new.scope_id);
    return new;
  end;
  $$ language plpgsql;

  create or replace function
    insert_auth_account_subtype()
    returns trigger
  as $$
  begin

    select auth_method.scope_id
      into new.scope_id
    from auth_method
    where auth_method.public_id = new.auth_method_id;

    insert into auth_account
      (public_id, auth_method_id, scope_id)
    values
      (new.public_id, new.auth_method_id, new.scope_id);

    return new;

  end;
  $$ language plpgsql;

  -- update_iam_user_auth_account is a before update trigger on the auth_account
  -- table. If the new.iam_user_id column is different from the old.iam_user_id
  -- column, update_iam_user_auth_account retrieves the scope id of the iam user
  -- and sets new.iam_user_scope_id to that value. If the new.iam_user_id column
  -- is null and the old.iam_user_id column is not null,
  -- update_iam_user_auth_account sets the iam_user_scope_id to null.
  create or replace function
    update_iam_user_auth_account()
    returns trigger
  as $$
  begin
    if new.iam_user_id is distinct from old.iam_user_id then
      if new.iam_user_id is null then
        new.iam_user_scope_id = null;
      else
        select iam_user.scope_id into new.iam_user_scope_id
          from iam_user
         where iam_user.public_id = new.iam_user_id;
      end if;
    end if;
    return new;
  end;
  $$ language plpgsql;


  create trigger update_iam_user_auth_account
    before update of iam_user_id on auth_account
    for each row
    execute procedure update_iam_user_auth_account();

commit;

`),
	},
	"migrations/08_servers.down.sql": {
		name: "08_servers.down.sql",
		bytes: []byte(`
begin;

  drop table server;

commit;

`),
	},
	"migrations/08_servers.up.sql": {
		name: "08_servers.up.sql",
		bytes: []byte(`
begin;

-- For now at least the IDs will be the same as the name, because this allows us
-- to not have to persist some generated ID to worker and controller nodes.
-- Eventually we may want them to diverge, so we have both here for now.

create table server (
    private_id text,
    type text,
    name text not null unique
      constraint server_name_must_not_be_empty
      check(length(trim(name)) > 0),
    description text,
    address text,
    create_time wt_timestamp,
    update_time wt_timestamp,
    primary key (private_id, type)
  );
  
create trigger 
  immutable_columns
before
update on server
  for each row execute procedure immutable_columns('create_time');
  
create trigger 
  default_create_time_column
before
insert on server
  for each row execute procedure default_create_time();

create table recovery_nonces (
    nonce text
      primary key,
    create_time wt_timestamp
  );

create trigger 
  default_create_time_column
before
insert on recovery_nonces
  for each row execute procedure default_create_time();

create trigger 
  immutable_columns
before
update on recovery_nonces
  for each row execute procedure immutable_columns('nonce', 'create_time');

commit;

`),
	},
	"migrations/11_auth_token.down.sql": {
		name: "11_auth_token.down.sql",
		bytes: []byte(`
begin;

  drop view auth_token_account cascade;
  drop table auth_token cascade;

  drop function update_last_access_time cascade;
  drop function immutable_auth_token_columns cascade;
  drop function expire_time_not_older_than_token cascade;

commit;

`),
	},
	"migrations/11_auth_token.up.sql": {
		name: "11_auth_token.up.sql",
		bytes: []byte(`
begin;

  -- an auth token belongs to 1 and only 1 auth account
  -- an auth account can have 0 to many auth tokens
  create table auth_token (
    public_id wt_public_id primary key,
    token bytea not null unique,
    -- TODO: Make key_id a foreign key once we have DEKs
    key_id text not null
      constraint key_id_must_not_be_empty
      check(length(trim(key_id)) > 0),
    auth_account_id wt_public_id not null
      references auth_account(public_id)
      on delete cascade
      on update cascade,
    create_time wt_timestamp,
    update_time wt_timestamp,
    -- This column is not updated every time this auth token is accessed.
    -- It is updated after X minutes from the last time it was updated on
    -- a per row basis.
    approximate_last_access_time wt_timestamp
      constraint last_access_time_must_not_be_after_expiration_time
      check(
        approximate_last_access_time <= expiration_time
      ),
    expiration_time wt_timestamp
      constraint create_time_must_not_be_after_expiration_time
      check(
        create_time <= expiration_time
      )
  );

  create view auth_token_account as
        select at.public_id,
               at.token,
               at.auth_account_id,
               at.create_time,
               at.update_time,
               at.approximate_last_access_time,
               at.expiration_time,
               aa.scope_id,
               aa.iam_user_id,
               aa.auth_method_id
          from auth_token as at
    inner join auth_account as aa
            on at.auth_account_id = aa.public_id;

  create or replace function
    update_last_access_time()
    returns trigger
  as $$
  begin
    if new.approximate_last_access_time is distinct from old.approximate_last_access_time then
      new.approximate_last_access_time = now();
    end if;
    return new;
  end;
  $$ language plpgsql;

  comment on function
    update_last_access_time()
  is
    'function used in before update triggers to properly set last_access_time columns';

  create or replace function
    immutable_auth_token_columns()
    returns trigger
  as $$
  begin
    if new.auth_account_id is distinct from old.auth_account_id then
      raise exception 'auth_account_id is read-only';
    end if;
    if new.token is distinct from old.token then
      raise exception 'token is read-only';
    end if;
    return new;
  end;
  $$ language plpgsql;

  comment on function
    immutable_auth_token_columns()
  is
    'function used in before update triggers to make specific columns immutable';

  -- This allows the expiration to be calculated on the server side and still hold the constraint that
  -- the expiration time cant be before the creation time of the auth token.
  create or replace function
    expire_time_not_older_than_token()
    returns trigger
  as $$
  begin
    if new.expiration_time < new.create_time then
      new.expiration_time = new.create_time;
    end if;
    return new;
  end;
  $$ language plpgsql;

  comment on function
      expire_time_not_older_than_token()
  is
    'function used in before insert triggers to ensure expiration time is not older than create time';

  create trigger
    default_create_time_column
  before insert on auth_token
    for each row execute procedure default_create_time();

  create trigger
    expire_time_not_older_than_token
  before insert on auth_token
    for each row execute procedure expire_time_not_older_than_token();

  create trigger
    update_time_column
  before update on auth_token
    for each row execute procedure update_time_column();

  create trigger
    update_last_access_time
  before update on auth_token
    for each row execute procedure update_last_access_time();


  create trigger
    immutable_auth_token_columns
  before update on auth_token
    for each row execute procedure immutable_auth_token_columns();

  create trigger 
    immutable_columns
  before
  update on auth_token
    for each row execute procedure immutable_columns('public_id', 'auth_account_id', 'create_time');

commit;

`),
	},
	"migrations/12_auth_password.down.sql": {
		name: "12_auth_password.down.sql",
		bytes: []byte(`
begin;

  drop table auth_password_credential;
  drop table auth_password_conf cascade;
  drop table if exists auth_password_account;
  drop table if exists auth_password_method;

  drop function insert_auth_password_credential_subtype;
  drop function insert_auth_password_conf_subtype;

commit;

`),
	},
	"migrations/12_auth_password.up.sql": {
		name: "12_auth_password.up.sql",
		bytes: []byte(`
begin;

/*

       ┌────────────────┐                 ┌──────────────────────┐             ┌────────────────────────────┐
       │  auth_method   │                 │ auth_password_method │             │     auth_password_conf     │
       ├────────────────┤                 ├──────────────────────┤             ├────────────────────────────┤
       │ public_id (pk) │                 │ public_id (pk,fk)    │            ╱│ private_id         (pk,fk) │
       │ scope_id  (fk) │┼┼─────────────○┼│ scope_id  (fk)       │┼┼─────────○─│ password_method_id (fk)    │
       │                │                 │ ...                  │            ╲│                            │
       └────────────────┘                 └──────────────────────┘             └────────────────────────────┘
                ┼                                     ┼                                       ┼
                ┼                                     ┼                                       ┼
                │                                     │                                       │
                │ ▲fk1                                │ ▲fk1                                  │ ▲fk1
                │                                     │                                       │
                ○                                     ○                                       ○
               ╱│╲                                   ╱│╲                                     ╱│╲
  ┌──────────────────────────┐          ┌──────────────────────────┐          ┌───────────────────────────────┐
  │       auth_account       │          │  auth_password_account   │          │   auth_password_credential    │
  ├──────────────────────────┤          ├──────────────────────────┤          ├───────────────────────────────┤
  │ public_id         (pk)   │          │ public_id      (pk,fk2)  │          │ private_id          (pk)      │
  │ scope_id          (fk1)  │   ◀fk2   │ scope_id       (fk1,fk2) │   ◀fk2   │ password_method_id  (fk1,fk2) │
  │ auth_method_id    (fk1)  │┼┼──────○┼│ auth_method_id (fk1,fk2) │┼┼──────○┼│ password_conf_id    (fk1)     │
  │ iam_user_scope_id (fk2)  │          │ ...                      │          │ password_account_id (fk2)     │
  │ iam_user_id       (fk2)  │          └──────────────────────────┘          └───────────────────────────────┘
  └──────────────────────────┘

  An auth_method is a base type. An auth_password_method is an auth_method
  subtype. For every row in auth_password_method there is one row in auth_method
  with the same public_id and scope_id.

  Similarly, an auth_account is a base type. An auth_password_account is an
  auth_account subtype. For every row in auth_password_account there is one row
  in auth_account with the same public_id, scope_id, and auth_method_id.

  Both auth_password_conf and auth_password_credential are base types. Each
  password key derivation function will require a auth_password_conf and
  auth_password_credential table.

  An auth_method can have 0 or 1 auth_password_method.
  An auth_account can have 0 or 1 auth_password_account.

  An auth_password_method belongs to 1 auth_method.
  An auth_password_method can have 0 to many auth_password_accounts.
  An auth_password_method can have 0 to many auth_password_confs.

  An auth_password_account belongs to 1 auth_account.
  An auth_password_account belongs to 1 auth_password_method.
  An auth_password_account can have 0 or 1 auth_password_credential.

  An auth_password_conf belongs to 1 auth_password_method.
  An auth_password_conf can have 0 to many auth_password_credentials.

  An auth_password_credential belongs to 1 auth_password_account.
  An auth_password_credential belongs to 1 auth_password_conf.

*/

  create table auth_password_method (
    public_id wt_public_id
      primary key,
    scope_id wt_scope_id
      not null,
    password_conf_id wt_private_id, -- FK to auth_password_conf added below
    name text,
    description text,
    create_time wt_timestamp,
    update_time wt_timestamp,
    min_login_name_length int
      not null
      default 3,
    min_password_length int
      not null
      default 8,
    version wt_version,
    foreign key (scope_id, public_id)
      references auth_method (scope_id, public_id)
      on delete cascade
      on update cascade,
    unique(scope_id, name),
    unique(scope_id, public_id)
  );

  create trigger
    update_version_column
  after update on auth_password_method
    for each row execute procedure update_version_column();

  create trigger
    insert_auth_method_subtype
  before insert on auth_password_method
    for each row execute procedure insert_auth_method_subtype();

  create table auth_password_account (
    public_id wt_public_id
      primary key,
    auth_method_id wt_public_id
      not null,
    -- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain
    -- check is executed before the insert trigger which retrieves the scope_id
    -- causing an insert to fail.
    scope_id text not null,
    name text,
    description text,
    create_time wt_timestamp,
    update_time wt_timestamp,
    login_name text not null
      constraint login_name_must_be_lowercase
      check(lower(trim(login_name)) = login_name)
      constraint login_name_must_not_be_empty
      check(length(trim(login_name)) > 0),
    version wt_version,
    foreign key (scope_id, auth_method_id)
      references auth_password_method (scope_id, public_id)
      on delete cascade
      on update cascade,
    foreign key (scope_id, auth_method_id, public_id)
      references auth_account (scope_id, auth_method_id, public_id)
      on delete cascade
      on update cascade,
    unique(auth_method_id, name),
    unique(auth_method_id, login_name),
    unique(auth_method_id, public_id)
  );

  create trigger
    update_version_column
  after update on auth_password_account
    for each row execute procedure update_version_column();

  create trigger
    insert_auth_account_subtype
  before insert on auth_password_account
    for each row execute procedure insert_auth_account_subtype();

  create table auth_password_conf (
    private_id wt_private_id
      primary key,
    password_method_id wt_public_id
      not null
      references auth_password_method (public_id)
      on delete cascade
      on update cascade
      deferrable initially deferred,
    unique(password_method_id, private_id)
  );

  alter table auth_password_method
    add constraint current_conf_fkey
    foreign key (public_id, password_conf_id)
    references auth_password_conf (password_method_id, private_id)
    on delete cascade
    on update cascade
    deferrable initially deferred;

  -- insert_auth_password_conf_subtype() is a trigger function for subtypes of
  -- auth_password_conf
  create or replace function
    insert_auth_password_conf_subtype()
    returns trigger
  as $$
  begin
    insert into auth_password_conf
      (private_id, password_method_id)
    values
      (new.private_id, new.password_method_id);
    return new;
  end;
  $$ language plpgsql;

  create table auth_password_credential (
    private_id wt_private_id
      primary key,
    password_account_id wt_public_id
      not null
      unique,
    password_conf_id wt_private_id,
    password_method_id wt_public_id
      not null,
    foreign key (password_method_id, password_conf_id)
      references auth_password_conf (password_method_id, private_id)
      on delete cascade
      on update cascade,
    foreign key (password_method_id, password_account_id)
      references auth_password_account (auth_method_id, public_id)
      on delete cascade
      on update cascade,
    unique(password_method_id, password_conf_id, password_account_id)
  );

  -- insert_auth_password_credential_subtype() is a trigger function for
  -- subtypes of auth_password_credential
  create or replace function
    insert_auth_password_credential_subtype()
    returns trigger
  as $$
  begin

    select auth_password_account.auth_method_id
      into new.password_method_id
    from auth_password_account
    where auth_password_account.public_id = new.password_account_id;

    insert into auth_password_credential
      (private_id, password_account_id, password_conf_id, password_method_id)
    values
      (new.private_id, new.password_account_id, new.password_conf_id, new.password_method_id);
    return new;
  end;
  $$ language plpgsql;

  -- update_auth_password_credential_subtype() is an after update trigger
  -- function for subtypes of auth_password_credential
  create or replace function
    update_auth_password_credential_subtype()
    returns trigger
  as $$
  begin
    /*
      The configuration id of a credential is updated when a credential is
      rehashed during authentication.
    */
    if new.password_conf_id is distinct from old.password_conf_id then
      update auth_password_credential
         set password_conf_id = new.password_conf_id
       where private_id = new.private_id;
    end if;
    return null; -- result is ignored since this is an after trigger
  end;
  $$ language plpgsql;

  -- delete_auth_password_credential_subtype() is an after delete trigger
  -- function for subtypes of auth_password_credential
  create or replace function
    delete_auth_password_credential_subtype()
    returns trigger
  as $$
  begin
    delete
      from auth_password_credential
     where private_id = old.private_id;
    return null; -- result is ignored since this is an after trigger
  end;
  $$ language plpgsql;

  --
  -- triggers for time columns
  --

  create trigger
    update_time_column
  before
  update on auth_password_method
    for each row execute procedure update_time_column();

  create trigger
    immutable_columns
  before
  update on auth_password_method
    for each row execute procedure immutable_columns('create_time');

  create trigger
    default_create_time_column
  before
  insert on auth_password_method
    for each row execute procedure default_create_time();

  create trigger
    update_time_column
  before
  update on auth_password_account
    for each row execute procedure update_time_column();

  create trigger
    immutable_columns
  before
  update on auth_password_account
    for each row execute procedure immutable_columns('create_time');

  create trigger
    default_create_time_column
  before
  insert on auth_password_account
    for each row execute procedure default_create_time();

  -- The tickets for oplog are the subtypes not the base types because no updates
  -- are done to any values in the base types.
  insert into oplog_ticket
    (name, version)
  values
    ('auth_password_method', 1),
    ('auth_password_account', 1),
    ('auth_password_credential', 1);

commit;

`),
	},
	"migrations/13_auth_password_argon.down.sql": {
		name: "13_auth_password_argon.down.sql",
		bytes: []byte(`
begin;

  drop table auth_password_argon2_cred;
  drop table auth_password_argon2_conf;

commit;

`),
	},
	"migrations/13_auth_password_argon.up.sql": {
		name: "13_auth_password_argon.up.sql",
		bytes: []byte(`
begin;

  create table auth_password_argon2_conf (
    private_id wt_private_id primary key
      references auth_password_conf (private_id)
      on delete cascade
      on update cascade,
    password_method_id wt_public_id not null,
    create_time wt_timestamp,
    iterations int not null default 3
      constraint iterations_must_be_greater_than_0
      check(iterations > 0),
    memory int not null default 65536
      constraint memory_must_be_greater_than_0
      check(memory > 0),
    threads int not null default 1
      constraint threads_must_be_greater_than_0
      check(threads > 0),
    -- salt_length unit is bytes
    salt_length int not null default 32
    -- minimum of 16 bytes (128 bits)
      constraint salt_must_be_at_least_16_bytes
      check(salt_length >= 16),
    -- key_length unit is bytes
    key_length int not null default 32
    -- minimum of 16 bytes (128 bits)
      constraint key_length_must_be_at_least_16_bytes
      check(key_length >= 16),
    unique(password_method_id, iterations, memory, threads, salt_length, key_length),
    unique (password_method_id, private_id),
    foreign key (password_method_id, private_id)
      references auth_password_conf (password_method_id, private_id)
      on delete cascade
      on update cascade
      deferrable initially deferred
  );

  create or replace function
    read_only_auth_password_argon2_conf()
    returns trigger
  as $$
  begin
    raise exception 'auth_password_argon2_conf is read-only';
  end;
  $$ language plpgsql;

  create trigger
    read_only_auth_password_argon2_conf
  before
  update on auth_password_argon2_conf
    for each row execute procedure read_only_auth_password_argon2_conf();

  create trigger
    insert_auth_password_conf_subtype
  before insert on auth_password_argon2_conf
    for each row execute procedure insert_auth_password_conf_subtype();

  create table auth_password_argon2_cred (
    private_id wt_private_id primary key
      references auth_password_credential (private_id)
      on delete cascade
      on update cascade,
    password_account_id wt_public_id not null,
    password_conf_id wt_private_id,
    -- NOTE(mgaffney): The password_method_id type is not wt_public_id because
    -- the domain check is executed before the insert trigger which retrieves
    -- the password_method_id causing an insert to fail.
    password_method_id text not null,
    create_time wt_timestamp,
    update_time wt_timestamp,
    salt bytea not null -- cannot be changed unless derived_key is changed too
      constraint salt_must_not_be_empty
      check(length(salt) > 0),
    derived_key bytea not null
      constraint derived_key_must_not_be_empty
      check(length(derived_key) > 0),
    -- TODO: Make key_id a foreign key once we have DEKs
    key_id text not null
      constraint key_id_must_not_be_empty
      check(length(trim(key_id)) > 0),
    foreign key (password_method_id, password_conf_id)
      references auth_password_argon2_conf (password_method_id, private_id)
      on delete cascade
      on update cascade,
    foreign key (password_method_id, password_conf_id, password_account_id)
      references auth_password_credential (password_method_id, password_conf_id, password_account_id)
      on delete cascade
      on update cascade
      deferrable initially deferred
  );

  create trigger
    insert_auth_password_credential_subtype
  before insert on auth_password_argon2_cred
    for each row execute procedure insert_auth_password_credential_subtype();

  create trigger
    update_auth_password_credential_subtype
  after update on auth_password_argon2_cred
    for each row execute procedure update_auth_password_credential_subtype();

  create trigger
    delete_auth_password_credential_subtype
  after delete on auth_password_argon2_cred
    for each row execute procedure delete_auth_password_credential_subtype();

  --
  -- triggers for time columns
  --

  create trigger
    immutable_columns
  before
  update on auth_password_argon2_conf
    for each row execute procedure immutable_columns('create_time');

  create trigger
    default_create_time_column
  before
  insert on auth_password_argon2_conf
    for each row execute procedure default_create_time();

  create trigger
    update_time_column
  before
  update on auth_password_argon2_cred
    for each row execute procedure update_time_column();

  create trigger
    immutable_columns
  before
  update on auth_password_argon2_cred
    for each row execute procedure immutable_columns('create_time');

  create trigger
    default_create_time_column
  before
  insert on auth_password_argon2_cred
    for each row execute procedure default_create_time();

  -- The tickets for oplog are the subtypes not the base types because no updates
  -- are done to any values in the base types.
  insert into oplog_ticket
    (name, version)
  values
    ('auth_password_argon2_conf', 1),
    ('auth_password_argon2_cred', 1);

commit;

`),
	},
	"migrations/14_auth_password_views.down.sql": {
		name: "14_auth_password_views.down.sql",
		bytes: []byte(`
begin;

  drop view auth_password_current_conf;
  drop view auth_password_conf_union;

commit;

`),
	},
	"migrations/14_auth_password_views.up.sql": {
		name: "14_auth_password_views.up.sql",
		bytes: []byte(`
begin;

  -- auth_password_conf_union is a union of the configuration settings
  -- of all supported key derivation functions.
  -- It will be updated as new key derivation functions are supported.
  create or replace view auth_password_conf_union as
      -- Do not change the order of the columns when adding new configurations.
      -- Union with new tables appending new columns as needed.
      select c.password_method_id, c.private_id as password_conf_id, c.private_id,
             'argon2' as conf_type,
             c.iterations, c.memory, c.threads, c.salt_length, c.key_length
        from auth_password_argon2_conf c;

  -- auth_password_current_conf provides a view of the current password
  -- configuration for each password auth method.
  -- The view will be updated as new key derivation functions are supported
  -- but the query to create the view should not need to be updated.
  create or replace view auth_password_current_conf as
      -- Rerun this query whenever auth_password_conf_union is updated.
      select pm.min_login_name_length, pm.min_password_length, c.*
        from auth_password_method pm
  inner join auth_password_conf_union c
          on pm.password_conf_id = c.password_conf_id;

commit;

`),
	},
	"migrations/20_host.down.sql": {
		name: "20_host.down.sql",
		bytes: []byte(`
begin;

  drop table host_set;
  drop table host;
  drop table host_catalog;

  drop function insert_host_set_subtype;
  drop function insert_host_subtype;
  drop function insert_host_catalog_subtype;

  delete
    from oplog_ticket
   where name in (
          'host_catalog',
          'host',
          'host_set'
        );

commit;

`),
	},
	"migrations/20_host.up.sql": {
		name: "20_host.up.sql",
		bytes: []byte(`
begin;

/*

                               ┌─────────────────┐
                               │      host       │
                               ├─────────────────┤
                               │ public_id  (pk) │
                               │ catalog_id (fk) │
                               │                 │
                               └─────────────────┘
                                       ╲│╱
                                        ○
                                        │
                                        ┼
                                        ┼
  ┌─────────────────┐          ┌─────────────────┐
  │    iam_scope    │          │  host_catalog   │
  ├─────────────────┤          ├─────────────────┤
  │ public_id (pk)  │         ╱│ public_id (pk)  │
  │                 │┼┼──────○─│ scope_id  (fk)  │
  │                 │         ╲│                 │
  └─────────────────┘          └─────────────────┘
                                        ┼
                                        ┼
                                        │
                                        ○
                                       ╱│╲
                               ┌─────────────────┐
                               │    host_set     │
                               ├─────────────────┤
                               │ public_id  (pk) │
                               │ catalog_id (fk) │
                               │                 │
                               └─────────────────┘

*/

  -- host_catalog
  create table host_catalog (
    public_id wt_public_id
      primary key,
    scope_id wt_scope_id
      not null
      references iam_scope (public_id)
      on delete cascade
      on update cascade,

    -- The order of columns is important for performance. See:
    -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
    -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field
    unique(scope_id, public_id)
  );

  create trigger immutable_columns before update on host_catalog
    for each row execute procedure immutable_columns('public_id', 'scope_id');

  -- insert_host_catalog_subtype() is a before insert trigger
  -- function for subtypes of host_catalog
  create or replace function insert_host_catalog_subtype()
    returns trigger
  as $$
  begin
    insert into host_catalog
      (public_id, scope_id)
    values
      (new.public_id, new.scope_id);
    return new;
  end;
  $$ language plpgsql;

  -- delete_host_catalog_subtype() is an after delete trigger
  -- function for subtypes of host_catalog
  create or replace function delete_host_catalog_subtype()
    returns trigger
  as $$
  begin
    delete from host_catalog
    where public_id = old.public_id;
    return null; -- result is ignored since this is an after trigger
  end;
  $$ language plpgsql;

  -- host
  create table host (
    public_id wt_public_id primary key,
    catalog_id wt_public_id not null
      references host_catalog (public_id)
      on delete cascade
      on update cascade,
    unique(catalog_id, public_id)
  );

  create trigger immutable_columns before update on host
    for each row execute procedure immutable_columns('public_id', 'catalog_id');

  -- insert_host_subtype() is a before insert trigger
  -- function for subtypes of host
  create or replace function insert_host_subtype()
    returns trigger
  as $$
  begin
    insert into host
      (public_id, catalog_id)
    values
      (new.public_id, new.catalog_id);
    return new;
  end;
  $$ language plpgsql;

  -- delete_host_subtype() is an after delete trigger
  -- function for subtypes of host
  create or replace function delete_host_subtype()
    returns trigger
  as $$
  begin
    delete from host
    where public_id = old.public_id;
    return null; -- result is ignored since this is an after trigger
  end;
  $$ language plpgsql;

  -- host_set
  create table host_set (
    public_id wt_public_id primary key,
    catalog_id wt_public_id not null
      references host_catalog (public_id)
      on delete cascade
      on update cascade,
    unique(catalog_id, public_id)
  );

  create trigger immutable_columns before update on host_set
    for each row execute procedure immutable_columns('public_id', 'catalog_id');

  -- insert_host_set_subtype() is a before insert trigger
  -- function for subtypes of host_set
  create or replace function insert_host_set_subtype()
    returns trigger
  as $$
  begin
    insert into host_set
      (public_id, catalog_id)
    values
      (new.public_id, new.catalog_id);
    return new;
  end;
  $$ language plpgsql;

  -- delete_host_set_subtype() is an after delete trigger
  -- function for subtypes of host_set
  create or replace function delete_host_set_subtype()
    returns trigger
  as $$
  begin
    delete from host_set
    where public_id = old.public_id;
    return null; -- result is ignored since this is an after trigger
  end;
  $$ language plpgsql;

  insert into oplog_ticket (name, version)
  values
    ('host_catalog', 1),
    ('host', 1),
    ('host_set', 1);

commit;

`),
	},
	"migrations/22_static_host.down.sql": {
		name: "22_static_host.down.sql",
		bytes: []byte(`
begin;

  drop table static_host_set_member cascade;
  drop table static_host_set cascade;
  drop table static_host cascade;
  drop table static_host_catalog cascade;

  delete
    from oplog_ticket
   where name in (
          'static_host_catalog',
          'static_host',
          'static_host_set',
          'static_host_set_member'
        );

commit;

`),
	},
	"migrations/22_static_host.up.sql": {
		name: "22_static_host.up.sql",
		bytes: []byte(`
begin;

/*

  ┌─────────────────┐          ┌─────────────────────┐
  │      host       │          │     static_host     │
  ├─────────────────┤          ├─────────────────────┤
  │ public_id  (pk) │          │ public_id  (pk)     │
  │ catalog_id (fk) │┼┼──────○┼│ catalog_id (fk)     │┼┼─────────────────────┐
  │                 │          │ address             │             ◀fk1      │
  └─────────────────┘          └─────────────────────┘                       │
          ╲│╱                            ╲│╱                                 │
           ○                              ○                                  │
           │                              │                                  │
           ┼                              ┼                                  ○
           ┼                              ┼                                 ╱│╲
  ┌─────────────────┐          ┌─────────────────────┐          ┌────────────────────────┐
  │  host_catalog   │          │ static_host_catalog │          │ static_host_set_member │
  ├─────────────────┤          ├─────────────────────┤          ├────────────────────────┤
  │ public_id (pk)  │          │ public_id (pk)      │          │ host_id    (pk,fk1)    │
  │ scope_id  (fk)  │┼┼──────○┼│ scope_id  (fk)      │          │ set_id     (pk,fk2)    │
  │                 │          │                     │          │ catalog_id (fk1,fk2)   │
  └─────────────────┘          └─────────────────────┘          └────────────────────────┘
           ┼                              ┼                                 ╲│╱
           ┼                              ┼                                  ○
           │                              │                                  │
           ○                              ○                                  │
          ╱│╲                            ╱│╲                                 │
  ┌─────────────────┐          ┌─────────────────────┐                       │
  │    host_set     │          │   static_host_set   │                       │
  ├─────────────────┤          ├─────────────────────┤                       │
  │ public_id  (pk) │          │ public_id  (pk)     │             ◀fk2      │
  │ catalog_id (fk) │┼┼──────○┼│ catalog_id (fk)     │┼┼─────────────────────┘
  │                 │          │                     │
  └─────────────────┘          └─────────────────────┘

*/

  create table static_host_catalog (
    public_id wt_public_id
      primary key,
    scope_id wt_scope_id
      not null
      references iam_scope (public_id)
      on delete cascade
      on update cascade,
    name text,
    description text,
    create_time wt_timestamp,
    update_time wt_timestamp,
    version wt_version,
    foreign key (scope_id, public_id)
      references host_catalog (scope_id, public_id)
      on delete cascade
      on update cascade,
    unique(scope_id, name)
  );

  create trigger update_version_column after update on static_host_catalog
    for each row execute procedure update_version_column();

  create trigger update_time_column before update on static_host_catalog
    for each row execute procedure update_time_column();

  create trigger default_create_time_column before insert on static_host_catalog
    for each row execute procedure default_create_time();

  create trigger immutable_columns before update on static_host_catalog
    for each row execute procedure immutable_columns('public_id', 'scope_id','create_time');

  create trigger insert_host_catalog_subtype before insert on static_host_catalog
    for each row execute procedure insert_host_catalog_subtype();

  create trigger delete_host_catalog_subtype after delete on static_host_catalog
    for each row execute procedure delete_host_catalog_subtype();

  create table static_host (
    public_id wt_public_id primary key,
    catalog_id wt_public_id not null
      references static_host_catalog (public_id)
      on delete cascade
      on update cascade,
    name text,
    description text,
    address text not null
      constraint address_must_be_more_than_2_characters
      check(length(trim(address)) > 2)
      constraint address_must_be_less_than_256_characters
      check(length(trim(address)) < 256),
    create_time wt_timestamp,
    update_time wt_timestamp,
    version wt_version,
    unique(catalog_id, name),

    foreign key (catalog_id, public_id)
      references host (catalog_id, public_id)
      on delete cascade
      on update cascade,

    -- The order of columns is important for performance. See:
    -- https://dba.stackexchange.com/questions/58970/enforcing-constraints-two-tables-away/58972#58972
    -- https://dba.stackexchange.com/questions/27481/is-a-composite-index-also-good-for-queries-on-the-first-field
    unique(catalog_id, public_id)
  );

  create trigger update_version_column after update on static_host
    for each row execute procedure update_version_column();

  create trigger update_time_column before update on static_host
    for each row execute procedure update_time_column();

  create trigger default_create_time_column before insert on static_host
    for each row execute procedure default_create_time();

  create trigger immutable_columns before update on static_host
    for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time');

  create trigger insert_host_subtype before insert on static_host
    for each row execute procedure insert_host_subtype();

  create trigger delete_host_subtype after delete on static_host
    for each row execute procedure delete_host_subtype();

  create table static_host_set (
    public_id wt_public_id primary key,
    catalog_id wt_public_id not null
      references static_host_catalog (public_id)
      on delete cascade
      on update cascade,
    name text,
    description text,
    create_time wt_timestamp,
    update_time wt_timestamp,
    version wt_version,
    unique(catalog_id, name),
    foreign key (catalog_id, public_id)
      references host_set (catalog_id, public_id)
      on delete cascade
      on update cascade,
    unique(catalog_id, public_id)
  );

  create trigger update_version_column after update on static_host_set
    for each row execute procedure update_version_column();

  create trigger update_time_column before update on static_host_set
    for each row execute procedure update_time_column();

  create trigger default_create_time_column before insert on static_host_set
    for each row execute procedure default_create_time();

  create trigger immutable_columns before update on static_host_set
    for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time');

  create trigger insert_host_set_subtype before insert on static_host_set
    for each row execute procedure insert_host_set_subtype();

  create trigger delete_host_set_subtype after delete on static_host_set
    for each row execute procedure delete_host_set_subtype();

  create table static_host_set_member (
    host_id wt_public_id not null,
    set_id wt_public_id not null,
    catalog_id wt_public_id not null,
    primary key(host_id, set_id),
    foreign key (catalog_id, host_id) -- fk1
      references static_host (catalog_id, public_id)
      on delete cascade
      on update cascade,
    foreign key (catalog_id, set_id) -- fk2
      references static_host_set (catalog_id, public_id)
      on delete cascade
      on update cascade
  );

  create trigger immutable_columns before update on static_host_set_member
    for each row execute procedure immutable_columns('host_id', 'set_id', 'catalog_id');

  create or replace function insert_static_host_set_member()
    returns trigger
  as $$
  begin
    select static_host_set.catalog_id
      into new.catalog_id
    from static_host_set
    where static_host_set.public_id = new.set_id;
    return new;
  end;
  $$ language plpgsql;

  create trigger insert_static_host_set_member before insert on static_host_set_member
    for each row execute procedure insert_static_host_set_member();

  insert into oplog_ticket (name, version)
  values
    ('static_host_catalog', 1),
    ('static_host', 1),
    ('static_host_set', 1),
    ('static_host_set_member', 1);

commit;

`),
	},
	"migrations/30_keys.down.sql": {
		name: "30_keys.down.sql",
		bytes: []byte(`
begin;

drop function kms_version_column cascade;

commit;

`),
	},
	"migrations/30_keys.up.sql": {
		name: "30_keys.up.sql",
		bytes: []byte(`
begin;

-- kms_version_column() will increment the version column whenever row data
-- is inserted and should only be used in an before insert trigger.  This
-- function will overwrite any explicit values to the version column.
create or replace function
  kms_version_column()
  returns trigger
as $$
declare 
  _key_id text;
  _max bigint; 
begin
  execute format('SELECT $1.%I', tg_argv[0]) into _key_id using new;
  execute format('select max(version) + 1 from %I where %I = $1', tg_relid::regclass, tg_argv[0]) using _key_id into _max;
  if _max is null then
  	_max = 1;
  end if;
  new.version = _max;
  return new;
end;
$$ language plpgsql;

comment on function
  kms_version_column()
is
  'function used in before insert triggers to properly set version columns for kms_* tables with a version column';
  
  commit;
`),
	},
	"migrations/31_keys.down.sql": {
		name: "31_keys.down.sql",
		bytes: []byte(`
begin;

drop table kms_root_key cascade;
drop table kms_root_key_version cascade;
drop table kms_database_key cascade;
drop table kms_database_key_version cascade;
drop table kms_oplog_key cascade;
drop table kms_oplog_key_version cascade;
drop table kms_session_key cascade;
drop table kms_session_key_version cascade;

commit;

`),
	},
	"migrations/31_keys.up.sql": {
		name: "31_keys.up.sql",
		bytes: []byte(`
begin;

/*
             ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐            
             ├────────────────────────────────────────────────────────────────────────────────────────────┐                           ○            
             ├────────────────────────────────────────────────────────────────┐                           ○                           ┼            
             ├────────────────────────────────────┐                           ○                           ┼              ┌────────────────────────┐
             │                                    ○                           ┼              ┌────────────────────────┐  │     kms_token_key      │
             ┼                                    ┼              ┌────────────────────────┐  │    kms_session_key     │  ├────────────────────────┤
┌────────────────────────┐           ┌────────────────────────┐  │     kms_oplog_key      │  ├────────────────────────┤  │private_id              │
│      kms_root_key      │           │    kms_database_key    │  ├────────────────────────┤  │private_id              │  │root_key_id             │
├────────────────────────┤           ├────────────────────────┤  │private_id              │  │root_key_id             │  │                        │
│private_id              │           │private_id              │  │root_key_id             │  │                        │  │                        │
│scope_id                │           │root_key_id             │  │                        │  │                        │  │                        │
│                        │           │                        │  │                        │  │                        │  └────────────────────────┘
└────────────────────────┘           └────────────────────────┘  └────────────────────────┘  └────────────────────────┘               ┼            
             ┼                                    ┼                           ┼                           ┼                           │            
             │                                    │                           │                           │                           │            
             │                                    │                           │                           │                           │            
             │                                    │                           │                           │                           │            
             │                                    │                           │                           │                           │            
             ┼                                    ┼                           ┼                           ┼                           │            
            ╱│╲                                  ╱│╲                         ╱│╲                         ╱│╲                          ┼            
┌────────────────────────┐           ┌────────────────────────┐  ┌────────────────────────┐  ┌────────────────────────┐              ╱│╲           
│  kms_root_key_version  │           │kms_database_key_version│  │ kms_oplog_key_version  │  │kms_session_key_version │  ┌────────────────────────┐
├────────────────────────┤           ├────────────────────────┤  ├────────────────────────┤  ├────────────────────────┤  │ kms_token_key_version  │
│private_id              │           │private_id              │  │private_id              │  │private_id              │  ├────────────────────────┤
│root_key_id             │           │database_key_id         │  │oplog_key_id            │  │session_key_id          │  │private_id              │
│key                     │           │root_key_id             │  │root_key_id             │  │root_key_id             │  │token_key_id            │
│version                 │           │key                     │  │key                     │  │key                     │  │root_key_id             │
│                        │           │version                 │  │version                 │  │version                 │  │key                     │
└────────────────────────┘           └────────────────────────┘  │                        │  │                        │  │version                 │
             ┼                                    ┼              └────────────────────────┘  │                        │  │                        │
             │                                    ○                           ┼              └────────────────────────┘  │                        │
             ├────────────────────────────────────┘                           ○                           ┼              └────────────────────────┘
             ├────────────────────────────────────────────────────────────────┘                           ○                           ┼            
             ├────────────────────────────────────────────────────────────────────────────────────────────┘                           ○            
             └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/

create table kms_root_key (
  private_id wt_private_id primary key,
  scope_id wt_scope_id not null unique -- there can only be one root key for a scope.
    references iam_scope(public_id) 
    on delete cascade 
    on update cascade,
  create_time wt_timestamp
);

 -- define the immutable fields for kms_root_key (all of them)
create trigger 
  immutable_columns
before
update on kms_root_key
  for each row execute procedure immutable_columns('private_id', 'scope_id', 'create_time');

create trigger 
  default_create_time_column
before
insert on kms_root_key
  for each row execute procedure default_create_time();

create table kms_root_key_version (
  private_id wt_private_id primary key,
  root_key_id  wt_private_id not null
    references kms_root_key(private_id) 
    on delete cascade 
    on update cascade,
  version wt_version,
  key bytea not null,
  create_time wt_timestamp,
  unique(root_key_id, version)
);

 -- define the immutable fields for kms_root_key_version (all of them)
create trigger 
  immutable_columns
before
update on kms_root_key_version
  for each row execute procedure immutable_columns('private_id', 'root_key_id', 'version', 'key', 'create_time');

create trigger 
  default_create_time_column
before
insert on kms_root_key_version
  for each row execute procedure default_create_time();


create trigger
	kms_version_column
before insert on kms_root_key_version
	for each row execute procedure kms_version_column('root_key_id');

create table kms_database_key (
  private_id wt_private_id primary key,
  root_key_id wt_private_id not null unique -- there can be only one database dek per root key
    references kms_root_key(private_id)
    on delete cascade
    on update cascade,
  create_time wt_timestamp
);

 -- define the immutable fields for kms_database_key (all of them)
create trigger 
  immutable_columns
before
update on kms_database_key
  for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time');

create trigger 
  default_create_time_column
before
insert on kms_database_key
  for each row execute procedure default_create_time();

create table kms_database_key_version (
  private_id wt_private_id primary key,
  database_key_id wt_private_id not null
    references kms_database_key(private_id) 
    on delete cascade 
    on update cascade, 
  root_key_version_id wt_private_id not null
    references kms_root_key_version(private_id) 
    on delete cascade 
    on update cascade,
  version wt_version,
  key bytea not null,
  create_time wt_timestamp,
  unique(database_key_id, version)
);

 -- define the immutable fields for kms_database_key_version (all of them)
create trigger 
  immutable_columns
before
update on kms_database_key_version
  for each row execute procedure immutable_columns('private_id', 'database_key_id', 'root_key_version_id', 'version', 'key', 'create_time');
  
create trigger 
  default_create_time_column
before
insert on kms_database_key_version
  for each row execute procedure default_create_time();

create trigger
	kms_version_column
before insert on kms_database_key_version
	for each row execute procedure kms_version_column('database_key_id');

create table kms_oplog_key (
  private_id wt_private_id primary key,
  root_key_id wt_private_id not null unique -- there can be only one oplog dek per root key
    references kms_root_key(private_id)
    on delete cascade
    on update cascade,
  create_time wt_timestamp
);

 -- define the immutable fields for kms_oplog_key (all of them)
create trigger 
  immutable_columns
before
update on kms_oplog_key
  for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time');

create trigger 
  default_create_time_column
before
insert on kms_oplog_key
  for each row execute procedure default_create_time();

create table kms_oplog_key_version (
  private_id wt_private_id primary key,
  oplog_key_id wt_private_id not null
    references kms_oplog_key(private_id) 
    on delete cascade 
    on update cascade, 
  root_key_version_id wt_private_id not null
    references kms_root_key_version(private_id) 
    on delete cascade 
    on update cascade,
  version wt_version,
  key bytea not null,
  create_time wt_timestamp,
  unique(oplog_key_id, version)
);

 -- define the immutable fields for kms_oplog_key_version (all of them)
create trigger 
  immutable_columns
before
update on kms_oplog_key_version
  for each row execute procedure immutable_columns('private_id', 'oplog_key_id', 'root_key_version_id', 'version', 'key', 'create_time');
  
create trigger 
  default_create_time_column
before
insert on kms_oplog_key_version
  for each row execute procedure default_create_time();

create trigger
	kms_version_column
before insert on kms_oplog_key_version
	for each row execute procedure kms_version_column('oplog_key_id');

create table kms_session_key (
  private_id wt_private_id primary key,
  root_key_id wt_private_id not null unique -- there can be only one session dek per root key
    references kms_root_key(private_id)
    on delete cascade
    on update cascade,
  create_time wt_timestamp
);

 -- define the immutable fields for kms_oplog_key (all of them)
create trigger 
  immutable_columns
before
update on kms_session_key
  for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time');

create trigger 
  default_create_time_column
before
insert on kms_session_key
  for each row execute procedure default_create_time();

create table kms_session_key_version (
  private_id wt_private_id primary key,
  session_key_id wt_private_id not null
    references kms_session_key(private_id) 
    on delete cascade 
    on update cascade, 
  root_key_version_id wt_private_id not null
    references kms_root_key_version(private_id) 
    on delete cascade 
    on update cascade,
  version wt_version,
  key bytea not null,
  create_time wt_timestamp,
  unique(session_key_id, version)
);


 -- define the immutable fields for kms_session_key_version (all of them)
create trigger 
  immutable_columns
before
update on kms_session_key_version
  for each row execute procedure immutable_columns('private_id', 'session_key_id', 'root_key_version_id', 'version', 'key', 'create_time');
  
create trigger 
  default_create_time_column
before
insert on kms_session_key_version
  for each row execute procedure default_create_time();

create trigger
	kms_version_column
before insert on kms_session_key_version
	for each row execute procedure kms_version_column('session_key_id');

create table kms_token_key (
  private_id wt_private_id primary key,
  root_key_id wt_private_id not null unique -- there can be only one token dek per root key
    references kms_root_key(private_id)
    on delete cascade
    on update cascade,
  create_time wt_timestamp
);

 -- define the immutable fields for kms_token_key (all of them)
create trigger 
  immutable_columns
before
update on kms_token_key
  for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time');

create trigger 
  default_create_time_column
before
insert on kms_token_key
  for each row execute procedure default_create_time();

create table kms_token_key_version (
  private_id wt_private_id primary key,
  token_key_id wt_private_id not null
    references kms_token_key(private_id) 
    on delete cascade 
    on update cascade, 
  root_key_version_id wt_private_id not null
    references kms_root_key_version(private_id) 
    on delete cascade 
    on update cascade,
  version wt_version,
  key bytea not null,
  create_time wt_timestamp,
  unique(token_key_id, version)
);

 -- define the immutable fields for kms_token_key_version (all of them)
create trigger 
  immutable_columns
before
update on kms_token_key_version
  for each row execute procedure immutable_columns('private_id', 'token_key_id', 'root_key_version_id', 'version', 'key', 'create_time');
  
create trigger 
  default_create_time_column
before
insert on kms_token_key_version
  for each row execute procedure default_create_time();

create trigger
	kms_version_column
before insert on kms_token_key_version
	for each row execute procedure kms_version_column('token_key_id');

  insert into oplog_ticket
    (name, version)
  values
    ('kms_root_key', 1),
    ('kms_root_key_version', 1);
    
commit;
`),
	},
	"migrations/40_targets.down.sql": {
		name: "40_targets.down.sql",
		bytes: []byte(`
begin;

drop function insert_target_subtype;
drop function delete_target_subtype;
drop function target_scope_valid;
drop function target_host_set_scope_valid

commit;

`),
	},
	"migrations/40_targets.up.sql": {
		name: "40_targets.up.sql",
		bytes: []byte(`
begin;


-- insert_target_subtype() is a before insert trigger
-- function for subtypes of target
create or replace function
  insert_target_subtype()
  returns trigger
as $$
begin
  insert into target
    (public_id, scope_id)
  values
    (new.public_id, new.scope_id);
  return new;
end;
$$ language plpgsql;

-- delete_target_subtype() is an after delete trigger
-- function for subtypes of host
create or replace function delete_target_subtype()
  returns trigger
as $$
begin
  delete from target
  where 
    public_id = old.public_id;
  return null; -- result is ignored since this is an after trigger
end;
$$ language plpgsql;

-- target_scope_valid() is a before insert trigger function for target
create or replace function
  target_scope_valid()
  returns trigger
as $$
declare scope_type text;
begin
  -- Fetch the type of scope
  select isc.type from iam_scope isc where isc.public_id = new.scope_id into scope_type;
  if scope_type = 'project' then
    return new;
  end if;
  raise exception 'invalid target scope type % (must be project)', scope_type;
end;
$$ language plpgsql;

-- target_host_set_scope_valid() is a before insert trigger function for target_host_set 
create or replace function
  target_host_set_scope_valid()
  returns trigger
as $$
begin
    perform from
      host_catalog hc,
      host_set hs,
      target t,
      iam_scope s
    where
      hc.public_id = hs.catalog_id and 
      hc.scope_id = t.scope_id and
      t.public_id = new.target_id;
if not found then
  raise exception 'target scope and host set scope are not equal';
end if;
return new;
end;
$$ language plpgsql;

commit;
`),
	},
	"migrations/41_targets.down.sql": {
		name: "41_targets.down.sql",
		bytes: []byte(`
begin;

drop table target cascade;
drop table target_host_set cascade;
drop table target_tcp;
drop view target_all_subtypes;
drop view target_host_set_catalog;


delete
from oplog_ticket
where name in (
        'target_tcp'
    );

commit;
`),
	},
	"migrations/41_targets.up.sql": {
		name: "41_targets.up.sql",
		bytes: []byte(`
/*
                                   ┌─────────────────┐
┌─────────────────┐                │   target_tcp    │
│     target      │                ├─────────────────┤
├─────────────────┤                │public_id        │
│public_id        │┼─────────────○┼│scope_id         │
│scope_id         │                │default_port     │
│                 │                │name (not null)  │
└─────────────────┘                │description      │
         ┼                         └─────────────────┘
         │                                            
         ○                                            
        ╱│╲                                           
┌─────────────────┐                                   
│ target_host_set │                                   
├─────────────────┤                                   
│target_id        │                                   
│host_set_id      │                                   
│                 │                                   
└─────────────────┘                                   
        ╲│╱                                           
         ○                                            
         │                                            
         │                                            
         ┼                                            
┌─────────────────┐                                   
│    host_set     │                                   
├─────────────────┤                                   
│public_id        │                                   
│catalog_id       │                                   
│                 │                                   
└─────────────────┘                                                                            

*/

begin;

create table target (
  public_id wt_public_id primary key,
  scope_id wt_scope_id not null 
    references iam_scope(public_id) 
    on delete cascade 
    on update cascade,
  create_time wt_timestamp
);

create trigger 
  immutable_columns
before
update on target
  for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time');

create trigger 
  default_create_time_column
before
insert on target
  for each row execute procedure default_create_time();

create trigger 
  target_scope_valid
before insert on target
  for each row execute procedure target_scope_valid();


create table target_host_set(
  target_id wt_public_id
    references target(public_id)
    on delete cascade
    on update cascade,
  host_set_id wt_public_id
    references host_set(public_id)
    on delete cascade
    on update cascade,
  primary key(target_id, host_set_id),
  create_time wt_timestamp
);

create trigger 
  immutable_columns
before
update on target_host_set
  for each row execute procedure immutable_columns('target_id', 'host_set_id', 'create_time');

create trigger 
  target_host_set_scope_valid
before
insert on target_host_set
  for each row execute procedure target_host_set_scope_valid();

create table target_tcp (
  public_id wt_public_id primary key
    references target(public_id)
    on delete cascade
    on update cascade,
  scope_id wt_scope_id not null 
    references iam_scope(public_id) 
    on delete cascade 
    on update cascade,
  name text not null, -- name is not optional for a target subtype
  description text,
  default_port int, -- default_port can be null
   -- max duration of the session in seconds.
   -- default is 8 hours
  session_max_seconds int not null default 28800
    constraint session_max_seconds_must_be_greater_than_0
    check(session_max_seconds > 0),
  -- limit on number of session connections allowed. -1 equals no limit
  session_connection_limit int not null default 1
    constraint session_connection_limit_must_be_greater_than_0_or_negative_1
    check(session_connection_limit > 0 or session_connection_limit = -1),
  create_time wt_timestamp,
  update_time wt_timestamp,
  version wt_version,
  unique(scope_id, name) -- name must be unique within a scope
);

create trigger
  insert_target_subtype
before insert on target_tcp
  for each row execute procedure insert_target_subtype();

create trigger
  delete_target_subtype
after delete on target_tcp
  for each row execute procedure delete_target_subtype();

 -- define the immutable fields for target 
create trigger 
  immutable_columns
before
update on target_tcp
  for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time');

create trigger
  update_version_column
after update on target_tcp
  for each row execute procedure update_version_column();

create trigger
  update_time_column
before update on target_tcp
  for each row execute procedure update_time_column();

create trigger 
  default_create_time_column
before
insert on target_tcp
  for each row execute procedure default_create_time();

create trigger 
  target_scope_valid
before insert on target_tcp
  for each row execute procedure target_scope_valid();


-- target_all_subtypes is a union of all target subtypes 
create view target_all_subtypes
as 
select 
  public_id, 
  scope_id, 
  name, 
  description, 
  default_port, 
  session_max_seconds,
  session_connection_limit,
  version, 
  create_time,
  update_time,
  'tcp' as type
  from target_tcp;

create view target_set
as
select 
  hs.public_id,
  hs.catalog_id,
  ths.target_id
from
  target_host_set ths,
  host_set hs
where
  hs.public_id = ths.host_set_id;

insert into oplog_ticket
  (name, version)
values
  ('target_tcp', 1);

commit;

`),
	},
	"migrations/50_session.down.sql": {
		name: "50_session.down.sql",
		bytes: []byte(`
begin;

  drop table session_state;
  drop table session_state_enm;
  drop table session;
  drop table session_termination_reason_enm;
  drop function insert_session_state;
  drop function insert_new_session_state;
  drop function insert_session;
  drop function update_session_state_on_termination_reason;
  drop function insert_session_state;


  delete
  from oplog_ticket
  where name in (
          'session'
      );

commit;

`),
	},
	"migrations/50_session.up.sql": {
		name: "50_session.up.sql",
		bytes: []byte(`
begin;

/*

             ┌─────────────────┐               ┌─────────────────┐   ┌─────────────────┐
             │  iam_scope_org  │               │    iam_user     │   │   auth_token    │
             ├─────────────────┤               ├─────────────────┤   ├─────────────────┤
             │ public_id  (pk) │               │ public_id (pk)  │   │ public_id  (pk) │
             │                 │               │                 │   │                 │
             └─────────────────┘               └─────────────────┘   └─────────────────┘
                 ▲fk7 ┼                            ▲fk1 ┼                ▲fk6 ┼
                      ┼                                 ┼                     ┼
                      ├─────────────────────────────────┴─────────────────────┘
                      │
                      ○
                     ╱│╲
        ┌──────────────────────────┐          ┌─────────────────┐   ┌─────────────────┐
        │         session          │╲  fk3▶   │     server      │   │     target      │
        ├──────────────────────────┤─○──────○┼├─────────────────┤   ├─────────────────┤
        │ public_id          (pk)  │╱         │ private_id (pk) │   │ public_id  (pk) │
        │ user_id            (fk1) │          │ type       (pk) │   │                 │
        │ host_id            (fk2) │          └─────────────────┘   └─────────────────┘
        │ server_id          (fk3) │                                    ▲fk4 ┼
        │ server_type        (fk3) │╲                                        ┼
        │ target_id          (fk4) │─○─────────────────┬─────────────────────┤
        │ host_set_id        (fk5) │╱                  ┼                     ┼
        │ auth_token_id      (fk6) │              ▼fk5 ┼                ▼fk2 ┼
        │ scope_id           (fk7) │          ┌─────────────────┐   ┌─────────────────┐
        │ termination_reason (fk8) │          │    host_set     │   │      host       │
        └──────────────────────────┘          ├─────────────────┤   ├─────────────────┤
                 ▲fk1 ┼           ╲│╱         │ public_id  (pk) │   │ public_id  (pk) │
                      ┼            ○          │                 │   │                 │
                      │            │          └─────────────────┘   └─────────────────┘
                      │            │
                      └─┐          │
                        │          │            ┌───────────────────────────────┐
                        │          │            │session_termination_reason_enm │
                        │          │     fk8▶   ├───────────────────────────────┤
                        ┼          └──────────○┼│ name                          │
                       ╱│╲                      └───────────────────────────────┘
  ┌──────────────────────────────────────────┐
  │              session_state               │
  ├──────────────────────────────────────────┤┼○┐
  │ session_id        (pk,fk1,fk2,unq1,unq2) │  │◀fk2
  │ state             (fk3)                  │  │
  │ previous_end_time (fk2,unq1)             │┼○┘
  │ start_time        (pk)                   │
  │ end_time          (unq2)                 │
  └──────────────────────────────────────────┘
                       ╲│╱
                        ○
                        │
                        │
                        ┼
                  ▼fk3  ┼
        ┌───────────────────────────────┐
        │       session_state_enm       │
        ├───────────────────────────────┤
        │ name                          │
        └───────────────────────────────┘

*/

  create table session_termination_reason_enm (
    name text primary key
      constraint only_predefined_session_termination_reasons_allowed
      check (
        name in (
          'unknown',
          'timed out',
          'closed by end-user',
          'terminated',
          'network error',
          'system error'
        )
      )
  );

  insert into session_termination_reason_enm (name)
  values
    ('unknown'),
    ('timed out'),
    ('closed by end-user'),
    ('terminated'),
    ('network error'),
    ('system error');

  create table session (
    public_id wt_public_id primary key,
    -- the user of the session
    user_id text -- fk1
      -- not using the wt_user_id domain type because it is marked 'not null'
      references iam_user (public_id)
      on delete set null
      on update cascade,
    -- the host the user is connected to via this session
    host_id wt_public_id -- fk2
      references host (public_id)
      on delete set null
      on update cascade,
    -- the worker proxying the connection between the user and the host
    server_id text, -- fk3
    server_type text,-- fk3
    foreign key (server_id, server_type)
      references server (private_id, type)
      on delete set null
      on update cascade,
    -- the target the host was chosen from and the user was authorized to
    -- connect to
    target_id wt_public_id -- fk4
      references target (public_id)
      on delete set null
      on update cascade,
    -- the host set the host was chosen from and the user was authorized to
    -- connect to via the target
    host_set_id wt_public_id -- fk5
      references host_set (public_id)
      on delete set null
      on update cascade,
    -- the auth token of the user when this session was created
    auth_token_id wt_public_id -- fk6
      references auth_token (public_id)
      on delete set null
      on update cascade,
    -- the project which owns this session
    scope_id wt_scope_id -- fk7
      references iam_scope_project (scope_id)
      on delete set null
      on update cascade,
    -- Certificate to use when connecting (or if using custom certs, to
	  -- serve as the "login"). Raw DER bytes.  
    certificate bytea not null,
    -- after this time the connection will be expired, e.g. forcefully terminated
    expiration_time wt_timestamp, -- maybe null
    -- limit on number of session connections allowed.  default of 0 equals no limit
    connection_limit int not null default 1
      constraint connection_limit_must_be_greater_than_0_or_negative_1
      check(connection_limit > 0 or connection_limit = -1), 
    -- trust of first use token 
    tofu_token bytea, -- will be null when session is first created
    -- the reason this session ended (null until terminated)
     -- TODO: Make key_id a foreign key once we have DEKs
    key_id text, -- will be null on insert
      -- references kms_database_key_version(private_id) 
      -- on delete restrict
      -- on update cascade,
    termination_reason text -- fk8
      references session_termination_reason_enm (name)
      on delete restrict
      on update cascade,
    version wt_version,
    create_time wt_timestamp,
    update_time wt_timestamp,
    endpoint text -- not part of the warehouse, used to send info to the worker
  );

  create trigger 
    immutable_columns
  before
  update on session
    for each row execute procedure immutable_columns('public_id', 'certificate', 'expiration_time', 'connection_limit', 'create_time', 'endpoint');
  
  -- session table has some cascades of FK to null, so we need to be careful
  -- which columns trigger an update of the version column
  create trigger 
    update_version_column 
  after update of version, termination_reason, key_id, tofu_token, server_id, server_type on session
    for each row execute procedure update_version_column();
    
  create trigger 
    update_time_column 
  before update on session 
    for each row execute procedure update_time_column();
    
  create trigger 
    default_create_time_column
  before
  insert on session
    for each row execute procedure default_create_time();

  create or replace function
    insert_session()
    returns trigger
  as $$
  begin
    case 
      when new.user_id is null then
        raise exception 'user_id is null';
      when new.host_id is null then
        raise exception 'host_id is null';
      when new.target_id is null then
        raise exception 'target_id is null';
      when new.host_set_id is null then
        raise exception 'host_set_id is null';
      when new.auth_token_id is null then
        raise exception 'auth_token_id is null';
      when new.scope_id is null then
        raise exception 'scope_id is null';
      when new.endpoint is null then
        raise exception 'endpoint is null';
    else
    end case;
    return new;
  end;
  $$ language plpgsql;

  create trigger 
    insert_session
  before insert on session
    for each row execute procedure insert_session();

  create or replace function 
    insert_new_session_state()
    returns trigger
  as $$
  begin
    insert into session_state (session_id, state)
    values
      (new.public_id, 'pending');
    return new;
  end;
  $$ language plpgsql;

  create trigger 
    insert_new_session_state
  after insert on session
    for each row execute procedure insert_new_session_state();

  -- update_connection_state_on_closed_reason() is used in an update insert trigger on the
  -- session_connection table.  it will valiadate that all the session's
  -- connections are closed, and then insert a state of "closed" in
  -- session_connection_state for the closed session connection. 
  create or replace function 
    update_session_state_on_termination_reason()
    returns trigger
  as $$
  begin
    if new.termination_reason is not null then
      -- check to see if there are any open connections.
      perform from
        session_connection sc,
        session_connection_state scs
      where
        sc.public_id = scs.connection_id and 
        scs.state != 'closed' and
        sc.session_id = new.public_id;
      if found then 
        raise 'session %s has existing open connections', new.public_id;
      end if;
      
      -- check to see if there's a terminated state already, before inserting a
      -- new one.
      perform from
        session_state ss
      where
        ss.session_id = new.public_id and 
        ss.state = 'terminated';
      if found then 
        return new;
      end if;

      insert into session_state (session_id, state)
      values
        (new.public_id, 'terminated');
      end if;
      return new;
  end;
  $$ language plpgsql;


  create trigger 
    update_session_state_on_termination_reason
  after update of termination_reason on session
    for each row execute procedure update_session_state_on_termination_reason();
 

  -- cancel_session will insert a cancel state for the session, if there's isn't
  -- a canceled state already.  It's used by cancel_session_with_null_fk.
  create or replace function
    cancel_session(in sessionId text) returns void 
  as $$
  declare
    rows_affected numeric;
  begin 
    insert into session_state(session_id, state) 
    select 
	    sessionId::text, 'canceling' 
    from
      session s
    where 
      s.public_id = sessionId::text and
      s.public_id not in (
        select 
          session_id 
        from 
          session_state 
        where 
          session_id = sessionId::text and 
          state = 'canceling'
      ) limit 1;
      get diagnostics rows_affected = row_count;
      if rows_affected > 1 then
          raise exception 'cancel session: more than one row affected: %', rows_affected; 
      end if;
  end;
  $$ language plpgsql;

  -- cancel_session_with_null_fk is intended to be a before update trigger that
  -- sets the session's state to cancel if a FK is set to null.
  create or replace function 
    cancel_session_with_null_fk()
    returns trigger
  as $$
  begin
   case 
      when new.user_id is null then
        perform cancel_session(new.public_id);
      when new.host_id is null then
        perform cancel_session(new.public_id);
      when new.target_id is null then
        perform cancel_session(new.public_id);
      when new.host_set_id is null then
        perform cancel_session(new.public_id);
      when new.auth_token_id is null then
        perform cancel_session(new.public_id);
      when new.scope_id is null then
        perform cancel_session(new.public_id);
    end case;
    return new;
  end;
  $$ language plpgsql;

  create trigger 
    cancel_session_with_null_fk
  before update of user_id, host_id, target_id, host_set_id, auth_token_id, scope_id on session
    for each row execute procedure cancel_session_with_null_fk();

  create table session_state_enm (
    name text primary key
      constraint only_predefined_session_states_allowed
      check (
        name in ('pending', 'active', 'canceling', 'terminated')
      )
  );

  insert into session_state_enm (name)
  values
    ('pending'),
    ('active'),
    ('canceling'),
    ('terminated');

/*


                                              ┌────────────────┐
         start                                │                │
           .                                  │   Canceling    │
          (●)                           ┌────▶│                │─────┐
           '                            │     │                │     │
           │                            │     └────────────────┘     │
           │                            │                            │
           ▼                            │                            ▼
  ┌────────────────┐           ┌────────────────┐           ┌────────────────┐
  │                │           │                │           │                │
  │    Pending     │           │     Active     │           │   Terminated   │
  │                │──────────▶│                │──────────▶│                │
  │                │           │                │           │                │
  └────────────────┘           └────────────────┘           └────────────────┘
           │                                                         │
           │                                                         │
           │                                                         │
           │                                                         │
           └──────────────────────▶  ◉ End  ◀────────────────────────┘

*/

  -- Design influenced by:
  -- Joe Celko's SQL for Smarties: Advanced SQL Programming, 5th edition
  -- Chapter 12, p270
  create table session_state (
    session_id wt_public_id not null -- fk1, fk2
      references session (public_id)
      on delete cascade
      on update cascade,
    state text not null -- fk3
      references session_state_enm(name)
      on delete restrict
      on update cascade,
    previous_end_time timestamp with time zone, -- fk2 -- null means first state
    start_time timestamp with time zone default current_timestamp not null,
      constraint previous_end_time_and_start_time_in_sequence
        check (previous_end_time <= start_time),
    end_time timestamp with time zone, -- null means unfinished current state
      constraint start_and_end_times_in_sequence
        check (start_time <= end_time),
      constraint end_times_in_sequence
        check (previous_end_time <> end_time),
    primary key (session_id, start_time),
    unique (session_id, previous_end_time), -- null means first state
    unique (session_id, end_time), -- one null current state
    foreign key (session_id, previous_end_time) -- self-reference
      references session_state (session_id, end_time)
  );


  create trigger 
    immutable_columns
  before
  update on session_state
    for each row execute procedure immutable_columns('session_id', 'state', 'start_time', 'previous_end_time');

    
  create or replace function
    insert_session_state()
    returns trigger
  as $$
  begin

    update session_state
       set end_time = now()
     where session_id = new.session_id
       and end_time is null;

    if not found then
      new.previous_end_time = null;
      new.start_time = now();
      new.end_time = null;
      return new;
    end if;

    new.previous_end_time = now();
    new.start_time = now();
    new.end_time = null;
    return new;

  end;
  $$ language plpgsql;


  create trigger insert_session_state before insert on session_state
    for each row execute procedure insert_session_state();

  create view session_with_state as
  select
    s.public_id,
    s.user_id,
    s.host_id,
    s.server_id,
    s.server_type,
    s.target_id,
    s.host_set_id,
    s.auth_token_id,
    s.scope_id,
    s.certificate,
    s.expiration_time,
    s.connection_limit,
    s.tofu_token,
    s.key_id,
    s.termination_reason,
    s.version,
    s.create_time,
    s.update_time,
    s.endpoint,
    ss.state,
    ss.previous_end_time,
    ss.start_time,
    ss.end_time
  from  
    session s,
    session_state ss
  where 
    s.public_id = ss.session_id;

commit;

`),
	},
	"migrations/51_connection.down.sql": {
		name: "51_connection.down.sql",
		bytes: []byte(`
begin;

  drop table session_connection_state;
  drop table session_connection_state_enm;
  drop table session_connection;
  drop table session_connection_closed_reason_enm;
  drop function insert_session_connection_state;
  drop function insert_new_connection_state;
  drop function update_connection_state_on_closed_reason;
commit;

`),
	},
	"migrations/51_connection.up.sql": {
		name: "51_connection.up.sql",
		bytes: []byte(`
begin;

/*

               ┌────────────────┐
               │    session     │
               ├────────────────┤
               │ public_id (pk) │
               │                │
               │                │
               └────────────────┘
                   ▲fk1 ┼
                        ┼
                        │
                        │
                        ┼
                       ╱│╲                  ┌──────────────────────────────────────┐
            ┌───────────────────────┐       │ session_connection_closed_reason_enm │
            │  session_connection   │╲ fk2▶ ├──────────────────────────────────────┤
            ├───────────────────────┤─○───○┼│ name                                 │
            │ public_id     (pk)    │╱      └──────────────────────────────────────┘
            │ session_id    (fk1)   │
            │ closed_reason (fk2)   │
            └───────────────────────┘
                   ▲fk1 ┼
                        ┼
                        │
                        │
                        ┼
                       ╱│╲
  ┌──────────────────────────────────────────┐
  │         session_connection_state         │
  ├──────────────────────────────────────────┤┼○┐
  │ connection_id     (pk,fk1,fk2,unq1,unq2) │  │◀fk2
  │ state             (fk3)                  │  │
  │ previous_end_time (fk2,unq1)             │┼○┘
  │ start_time        (pk)                   │
  │ end_time          (unq2)                 │
  └──────────────────────────────────────────┘
                       ╲│╱
                        ○
                        │
                        │
                        ┼
                  ▼fk3  ┼
        ┌───────────────────────────────┐
        │ session_connection_state_enm  │
        ├───────────────────────────────┤
        │ name                          │
        └───────────────────────────────┘

*/

  create table session_connection_closed_reason_enm (
    name text primary key
      constraint only_predefined_session_connection_closed_reasons_allowed
      check (
        name in (
          'unknown',
          'timed out',
          'closed by end-user',
          'canceled',
          'network error',
          'system error'
        )
      )
  );

  insert into session_connection_closed_reason_enm (name)
  values
    ('unknown'),
    ('timed out'),
    ('closed by end-user'),
    ('canceled'),
    ('network error'),
    ('system error');

  -- A session connection is one connection proxied by a worker from a client to
  -- a endpoint for a session. The client initiates the connection to the worker
  -- and the worker initiates the connection to the endpoint.
  -- A session can have zero or more session connections.
  create table session_connection (
    public_id wt_public_id primary key,
    session_id wt_public_id not null
      references session (public_id)
      on delete cascade
      on update cascade,
    -- the client_tcp_address is the network address of the client which initiated
    -- the connection to a worker
    client_tcp_address inet,  -- maybe null on insert
    -- the client_tcp_port is the network port at the address of the client the
    -- worker proxied a connection for the user
    client_tcp_port integer  -- maybe null on insert
      constraint client_tcp_port_must_be_greater_than_0
      check(client_tcp_port > 0)
      constraint client_tcp_port_must_less_than_or_equal_to_65535
      check(client_tcp_port <= 65535),
    -- the endpoint_tcp_address is the network address of the endpoint which the
    -- worker initiated the connection to, for the user
    endpoint_tcp_address inet, -- maybe be null on insert
    -- the endpoint_tcp_port is the network port at the address of the endpoint the
    -- worker proxied a connection to, for the user
    endpoint_tcp_port integer -- maybe null on insert
      constraint endpoint_tcp_port_must_be_greater_than_0
      check(endpoint_tcp_port > 0)
      constraint endpoint_tcp_port_must_less_than_or_equal_to_65535
      check(endpoint_tcp_port <= 65535),
    -- the total number of bytes received by the worker from the client and sent
    -- to the endpoint for this connection
    bytes_up bigint -- can be null
      constraint bytes_up_must_be_null_or_a_non_negative_number
      check (
        bytes_up is null
        or
        bytes_up >= 0
      ),
    -- the total number of bytes received by the worker from the endpoint and sent
    -- to the client for this connection
    bytes_down bigint -- can be null
      constraint bytes_down_must_be_null_or_a_non_negative_number
      check (
        bytes_down is null
        or
        bytes_down >= 0
      ),
    closed_reason text
      references session_connection_closed_reason_enm (name)
      on delete restrict
      on update cascade,
    version wt_version,
    create_time wt_timestamp,
    update_time wt_timestamp
  );

  create trigger 
    immutable_columns
  before
  update on session_connection
    for each row execute procedure immutable_columns('public_id', 'session_id', 'create_time');

  create trigger 
    update_version_column 
  after update on session_connection
    for each row execute procedure update_version_column();
    
  create trigger 
    update_time_column 
  before update on session_connection 
    for each row execute procedure update_time_column();
    
  create trigger 
    default_create_time_column
  before
  insert on session_connection
    for each row execute procedure default_create_time();

  -- insert_new_connection_state() is used in an after insert trigger on the
  -- session_connection table.  it will insert a state of "authorized" in
  -- session_connection_state for the new session connection. 
  create or replace function 
    insert_new_connection_state()
    returns trigger
  as $$
  begin
    insert into session_connection_state (connection_id, state)
    values
      (new.public_id, 'authorized');
    return new;
  end;
  $$ language plpgsql;

  create trigger 
    insert_new_connection_state
  after insert on session_connection
    for each row execute procedure insert_new_connection_state();

  -- update_connection_state_on_closed_reason() is used in an update insert trigger on the
  -- session_connection table.  it will insert a state of "closed" in
  -- session_connection_state for the closed session connection. 
  create or replace function 
    update_connection_state_on_closed_reason()
    returns trigger
  as $$
  begin
    if new.closed_reason is not null then
      -- check to see if there's a closed state already, before inserting a
      -- new one.
      perform from
        session_connection_state cs
      where
        cs.connection_id = new.public_id and 
        cs.state = 'closed';
      if not found then 
        insert into session_connection_state (connection_id, state)
        values
          (new.public_id, 'closed');
      end if;
    end if;
    return new;
  end;
  $$ language plpgsql;

  create trigger 
    update_connection_state_on_closed_reason
  after update of closed_reason on session_connection
    for each row execute procedure update_connection_state_on_closed_reason();

  create table session_connection_state_enm (
    name text primary key
      constraint only_predefined_session_connection_states_allowed
      check (
        name in ('authorized', 'connected', 'closed')
      )
  );

  insert into session_connection_state_enm (name)
  values
    ('authorized'),
    ('connected'),
    ('closed');


  create table session_connection_state (
    connection_id wt_public_id not null
      references session_connection (public_id)
      on delete cascade
      on update cascade,
    state text not null
      references session_connection_state_enm(name)
      on delete restrict
      on update cascade,
    previous_end_time timestamp with time zone, -- fk2 -- null means first state
    start_time timestamp with time zone default current_timestamp not null,
      constraint previous_end_time_and_start_time_in_sequence
        check (previous_end_time <= start_time),
    end_time timestamp with time zone, -- null means unfinished current state
      constraint start_and_end_times_in_sequence
        check (start_time <= end_time),
      constraint end_times_in_sequence
        check (previous_end_time <> end_time),
    primary key (connection_id, start_time),
    unique (connection_id, previous_end_time), -- null means first state
    unique (connection_id, end_time), -- one null current state
    foreign key (connection_id, previous_end_time) -- self-reference
      references session_connection_state (connection_id, end_time)
  );

  create trigger 
    immutable_columns
  before
  update on session_connection_state
    for each row execute procedure immutable_columns('connection_id', 'state', 'start_time', 'previous_end_time');

  create or replace function
    insert_session_connection_state()
    returns trigger
  as $$
  begin

    update session_connection_state
       set end_time = now()
     where connection_id = new.connection_id
       and end_time is null;

    if not found then
      new.previous_end_time = null;
      new.start_time = now();
      new.end_time = null;
      return new;
    end if;

    new.previous_end_time = now();
    new.start_time = now();
    new.end_time = null;
    return new;

  end;
  $$ language plpgsql;

  create trigger insert_session_connection_state before insert on session_connection_state
    for each row execute procedure insert_session_connection_state();

commit;

`),
	},
	"migrations/60_wh_domain_types.down.sql": {
		name: "60_wh_domain_types.down.sql",
		bytes: []byte(`
begin;

  drop function wh_current_time_id;
  drop function wh_current_date_id;
  drop function wh_time_id;
  drop function wh_date_id;
  drop domain wh_dim_text;
  drop domain wh_timestamp;
  drop domain wh_public_id;
  drop domain wh_dim_id;
  drop function wh_dim_id;
  drop domain wh_bytes_transmitted;
  drop domain wh_inet_port;

commit;

`),
	},
	"migrations/60_wh_domain_types.up.sql": {
		name: "60_wh_domain_types.up.sql",
		bytes: []byte(`
begin;

  create extension if not exists "pgcrypto";

  create domain wh_inet_port as integer
  check(
    value > 0
    and
    value <= 65535
  );
  comment on domain wh_inet_port is
  'An ordinal number between 1 and 65535 representing a network port';

  create domain wh_bytes_transmitted as bigint
  check(
    value >= 0
  );
  comment on domain wh_bytes_transmitted is
  'A non-negative integer representing the number of bytes transmitted';

  -- wh_dim_id generates and returns a random ID which should be considered as
  -- universally unique.
  create or replace function wh_dim_id()
    returns text
  as $$
    select encode(digest(gen_random_bytes(16), 'sha256'), 'base64');
  $$ language sql;

  create domain wh_dim_id as text
  check(
    length(trim(value)) > 0
  );
  comment on domain wh_dim_id is
  'Random ID generated with pgcrypto';

  create domain wh_public_id as text
  check(
    value = 'None'
    or
    length(trim(value)) > 10
  );
  comment on domain wh_public_id is
  'Equivalent to wt_public_id but also allows the value to be ''None''';

  create domain wh_timestamp as timestamp with time zone not null;
  comment on domain wh_timestamp is
  'Timestamp used in warehouse tables';

  create domain wh_dim_text as text not null
  check(
    length(trim(value)) > 0
  );
  comment on domain wh_dim_text is
  'Text fields in dimension tables are always not null and always not empty strings';

  -- wh_date_id returns the wh_date_dimension id for ts.
  create or replace function wh_date_id(ts wh_timestamp)
    returns integer
  as $$
    select to_char(ts, 'YYYYMMDD')::integer;
  $$ language sql;

  -- wh_time_id returns the wh_time_of_day_dimension id for ts.
  create or replace function wh_time_id(ts wh_timestamp)
    returns integer
  as $$
    select to_char(ts, 'SSSS')::integer;
  $$ language sql;

  -- wh_date_id returns the wh_date_dimension id for current_timestamp.
  create or replace function wh_current_date_id()
    returns integer
  as $$
    select wh_date_id(current_timestamp);
  $$ language sql;

  -- wh_time_id returns the wh_time_of_day_dimension id for current_timestamp.
  create or replace function wh_current_time_id()
    returns integer
  as $$
    select wh_time_id(current_timestamp);
  $$ language sql;

commit;

`),
	},
	"migrations/62_wh_datetime.down.sql": {
		name: "62_wh_datetime.down.sql",
		bytes: []byte(`
begin;

  drop table wh_time_of_day_dimension;
  drop table wh_date_dimension;

commit;

`),
	},
	"migrations/62_wh_datetime.up.sql": {
		name: "62_wh_datetime.up.sql",
		bytes: []byte(`
begin;

  create table wh_date_dimension (
    id                            integer      primary key,
    date                          date         not null,
    calendar_quarter              wh_dim_text,
    calendar_month                wh_dim_text,
    calendar_year                 smallint     not null,
    day_of_week                   wh_dim_text,
    day_of_week_number            smallint     not null,
    day_of_week_number_iso        smallint     not null,
    day_of_week_number_zero_based smallint     not null,
    day_number_in_calendar_month  smallint     not null,
    day_number_in_calendar_year   smallint     not null,
    weekday_indicator             wh_dim_text
  );

  insert into wh_date_dimension (
    id, date,
    calendar_quarter, calendar_month, calendar_year,
    day_of_week, day_of_week_number, day_of_week_number_iso, day_of_week_number_zero_based,
    day_number_in_calendar_month, day_number_in_calendar_year,
    weekday_indicator
  ) values (
    -1, 'infinity',
    'None', 'None', -1,
    'None', -1, -1, -1,
    -1, -1,
    'None'
  );

  insert
    into wh_date_dimension
  select to_char(t.day, 'YYYYMMDD')::integer as id,
         t.day::date                         as date,
         'Q' || to_char(t.day, 'Q')          as calendar_quarter,
         to_char(t.day, 'Month')             as calendar_month,
         extract(year from t.day)            as calendar_year,
         to_char(t.day, 'Day')               as day_of_week,
         to_char(t.day, 'D')::int            as day_of_week_number,
         extract(isodow from t.day)          as day_of_week_number_iso,
         extract(dow from t.day)             as day_of_week_number_zero_based,
         extract(day from t.day)             as day_number_in_calendar_month,
         extract(doy from t.day)             as day_number_in_calendar_year,
         case extract(isodow from t.day)
           when 6 then 'Weekend'
           when 7 then 'Weekend'
           else 'Weekday'
         end                                 as weekday_indicator
    from generate_series(
           date_trunc('day', timestamp '2019-10-09'),
           date_trunc('day', timestamp '2019-10-09' + interval '50 years'),
           interval '1 day'
         ) as t(day);

  create table wh_time_of_day_dimension (
    id                 integer      primary key,
    time_no_zone       time         not null,
    time_at_utc        timetz       not null,
    hour_of_day        smallint     not null,
    minute_of_hour     smallint     not null,
    second_of_minute   smallint     not null,
    display_time_24    wh_dim_text,
    display_time_12    wh_dim_text,
    meridiem_indicator wh_dim_text
  );

  set timezone = 'UTC';

  insert into wh_time_of_day_dimension (
    id, time_no_zone, time_at_utc,
    hour_of_day, minute_of_hour, second_of_minute,
    display_time_24, display_time_12, meridiem_indicator
  ) values (
    -1, 'allballs', 'allballs',
    -1, -1, -1,
    'None', 'None', 'None'
  );

  insert
    into wh_time_of_day_dimension
  select to_char(t.second, 'SSSS')::integer as id,
         t.second::time                     as time_no_zone,
         t.second::time                     as time_at_utc,
         extract(hour from t.second)        as hour_of_day,
         extract(minute from t.second)      as minute_of_hour,
         extract(second from t.second)      as second_of_minute,
         to_char(t.second, 'HH24:MI:SS')    as display_time_24,
         to_char(t.second, 'HH12:MI:SS AM') as display_time_12,
         to_char(t.second, 'PM')            as meridiem_indicator
    from generate_series(
           date_trunc('day', current_timestamp),
           date_trunc('day', current_timestamp) + interval '24 hours' - interval '1 second',
           interval '1 second'
         ) as t(second);

commit;

`),
	},
	"migrations/65_wh_session_dimensions.down.sql": {
		name: "65_wh_session_dimensions.down.sql",
		bytes: []byte(`
begin;

  drop view whx_user_dimension_target;
  drop view whx_user_dimension_source;
  drop view whx_host_dimension_target;
  drop view whx_host_dimension_source;
  drop table wh_user_dimension;
  drop table wh_host_dimension;

commit;

`),
	},
	"migrations/65_wh_session_dimensions.up.sql": {
		name: "65_wh_session_dimensions.up.sql",
		bytes: []byte(`
begin;

  create table wh_host_dimension (
    -- random id generated using encode(digest(gen_random_bytes(16), 'sha256'), 'base64')
    -- this is done to prevent conflicts with rows in other clusters
    -- which enables warehouse data from multiple clusters to be loaded into a
    -- single database instance
    id                              wh_dim_id     primary key default wh_dim_id(),

    host_id                         wh_public_id  not null,
    host_type                       wh_dim_text,
    host_name                       wh_dim_text,
    host_description                wh_dim_text,
    host_address                    wh_dim_text,

    host_set_id                     wh_public_id  not null,
    host_set_type                   wh_dim_text,
    host_set_name                   wh_dim_text,
    host_set_description            wh_dim_text,

    host_catalog_id                 wh_public_id  not null,
    host_catalog_type               wh_dim_text,
    host_catalog_name               wh_dim_text,
    host_catalog_description        wh_dim_text,

    target_id                       wh_public_id  not null,
    target_type                     wh_dim_text,
    target_name                     wh_dim_text,
    target_description              wh_dim_text,
    target_default_port_number      integer       not null,
    target_session_max_seconds      integer       not null,
    target_session_connection_limit integer       not null,

    project_id                      wt_scope_id   not null,
    project_name                    wh_dim_text,
    project_description             wh_dim_text,

    host_organization_id            wt_scope_id   not null,
    host_organization_name          wh_dim_text,
    host_organization_description   wh_dim_text,

    current_row_indicator           wh_dim_text,
    row_effective_time              wh_timestamp,
    row_expiration_time             wh_timestamp
  );

  -- https://www.postgresql.org/docs/current/indexes-partial.html
  create unique index wh_host_dim_current_constraint
    on wh_host_dimension (target_id, host_set_id, host_id)
    where current_row_indicator = 'Current';

  -- The whx_host_dimension_source and whx_host_dimension_target views are used
  -- by an insert trigger to determine if the current row for the dimension has
  -- changed and new one needs to be inserted. The first column in the target
  -- view must be the current warehouse id and all remaining columns must match
  -- the columns in the source view.

  -- The whx_host_dimension_source view shows the current values in the
  -- operational tables of the host dimension.
  create view whx_host_dimension_source as
  select -- id is the first column in the target view
         h.public_id                     as host_id,
         'static host'                   as host_type,
         coalesce(h.name, 'None')        as host_name,
         coalesce(h.description, 'None') as host_description,
         coalesce(h.address, 'Unknown')  as host_address,
         s.public_id                     as host_set_id,
         'static host set'               as host_set_type,
         coalesce(s.name, 'None')        as host_set_name,
         coalesce(s.description, 'None') as host_set_description,
         c.public_id                     as host_catalog_id,
         'static host catalog'           as host_catalog_type,
         coalesce(c.name, 'None')        as host_catalog_name,
         coalesce(c.description, 'None') as host_catalog_description,
         t.public_id                     as target_id,
         'tcp target'                    as target_type,
         coalesce(t.name, 'None')        as target_name,
         coalesce(t.description, 'None') as target_description,
         coalesce(t.default_port, 0)     as target_default_port_number,
         t.session_max_seconds           as target_session_max_seconds,
         t.session_connection_limit      as target_session_connection_limit,
         p.public_id                     as project_id,
         coalesce(p.name, 'None')        as project_name,
         coalesce(p.description, 'None') as project_description,
         o.public_id                     as host_organization_id,
         coalesce(o.name, 'None')        as host_organization_name,
         coalesce(o.description, 'None') as host_organization_description
    from static_host as h,
         static_host_catalog as c,
         static_host_set_member as m,
         static_host_set as s,
         target_host_set as ts,
         target_tcp as t,
         iam_scope as p,
         iam_scope as o
   where h.catalog_id = c.public_id
     and h.public_id = m.host_id
     and s.public_id = m.set_id
     and t.public_id = ts.target_id
     and s.public_id = ts.host_set_id
     and p.public_id = t.scope_id
     and p.type = 'project'
     and o.public_id = p.parent_id
     and o.type = 'org'
  ;

  -- The whx_host_dimension_target view shows the rows in the wh_host_dimension
  -- table marked as 'Current'.
  create view whx_host_dimension_target as
  select id,
         host_id,
         host_type,
         host_name,
         host_description,
         host_address,
         host_set_id,
         host_set_type,
         host_set_name,
         host_set_description,
         host_catalog_id,
         host_catalog_type,
         host_catalog_name,
         host_catalog_description,
         target_id,
         target_type,
         target_name,
         target_description,
         target_default_port_number,
         target_session_max_seconds,
         target_session_connection_limit,
         project_id,
         project_name,
         project_description,
         host_organization_id,
         host_organization_name,
         host_organization_description
    from wh_host_dimension
   where current_row_indicator = 'Current'
  ;

  create table wh_user_dimension (
    -- random id generated using encode(digest(gen_random_bytes(16), 'sha256'), 'base64')
    -- this is done to prevent conflicts with rows in other clusters
    -- which enables warehouse data from multiple clusters to be loaded into a
    -- single database instance
    id                            wh_dim_id     primary key default wh_dim_id(),

    user_id                       wt_user_id    not null,
    user_name                     wh_dim_text,
    user_description              wh_dim_text,

    auth_account_id               wh_public_id  not null,
    auth_account_type             wh_dim_text,
    auth_account_name             wh_dim_text,
    auth_account_description      wh_dim_text,

    auth_method_id                wh_public_id  not null,
    auth_method_type              wh_dim_text,
    auth_method_name              wh_dim_text,
    auth_method_description       wh_dim_text,

    user_organization_id          wt_scope_id   not null,
    user_organization_name        wh_dim_text,
    user_organization_description wh_dim_text,

    current_row_indicator         wh_dim_text,
    row_effective_time            wh_timestamp,
    row_expiration_time           wh_timestamp
  );

  -- The whx_user_dimension_source and whx_user_dimension_target views are used
  -- by an insert trigger to determine if the current row for the dimension has
  -- changed and new one needs to be inserted. The first column in the target
  -- view must be the current warehouse id and all remaining columns must match
  -- the columns in the source view.

  -- The whx_user_dimension_source view shows the current values in the
  -- operational tables of the user dimension.
  create view whx_user_dimension_source as
       select -- id is the first column in the target view
              u.public_id                       as user_id,
              coalesce(u.name, 'None')          as user_name,
              coalesce(u.description, 'None')   as user_description,
              coalesce(aa.public_id, 'None')    as auth_account_id,
              case when aa.public_id is null then 'None'
                   else 'password auth account'
                   end                          as auth_account_type,
              coalesce(apa.name, 'None')        as auth_account_name,
              coalesce(apa.description, 'None') as auth_account_description,
              coalesce(am.public_id, 'None')    as auth_method_id,
              case when am.public_id is null then 'None'
                   else 'password auth method'
                   end                          as auth_method_type,
              coalesce(apm.name, 'None')        as auth_method_name,
              coalesce(apm.description, 'None') as auth_method_description,
              org.public_id                     as user_organization_id,
              coalesce(org.name, 'None')        as user_organization_name,
              coalesce(org.description, 'None') as user_organization_description
         from iam_user as u
    left join auth_account as aa on           u.public_id = aa.iam_user_id
    left join auth_method as am on            aa.auth_method_id = am.public_id
    left join auth_password_account as apa on aa.public_id = apa.public_id
    left join auth_password_method as apm on  am.public_id = apm.public_id
         join iam_scope as org on             u.scope_id = org.public_id
  ;

  -- The whx_user_dimension_target view shows the rows in the wh_user_dimension
  -- table marked as 'Current'.
  create view whx_user_dimension_target as
    select id,
           user_id,
           user_name,
           user_description,
           auth_account_id,
           auth_account_type,
           auth_account_name,
           auth_account_description,
           auth_method_id,
           auth_method_type,
           auth_method_name,
           auth_method_description,
           user_organization_id,
           user_organization_name,
           user_organization_description
      from wh_user_dimension
     where current_row_indicator = 'Current'
  ;

commit;

`),
	},
	"migrations/66_wh_session_dimensions.down.sql": {
		name: "66_wh_session_dimensions.down.sql",
		bytes: []byte(`
begin;

  drop function wh_upsert_user;
  drop function wh_upsert_host;

commit;

`),
	},
	"migrations/66_wh_session_dimensions.up.sql": {
		name: "66_wh_session_dimensions.up.sql",
		bytes: []byte(`
begin;

  -- wh_upsert_host returns the wh_host_dimension id for p_host_id,
  -- p_host_set_id, and p_target_id. wh_upsert_host compares the current values
  -- in the wh_host_dimension with the current values in the operational tables
  -- for the provide parameters. If the values between the operational tables
  -- and the wh_host_dimension differ, a new row is inserted in the
  -- wh_host_dimension to match the current values in the operational tables and
  -- the new id is returned. If the values do not differ, the current id is
  -- returned.
  create or replace function wh_upsert_host(p_host_id wt_public_id, p_host_set_id wt_public_id, p_target_id wt_public_id)
    returns wh_dim_id
  as $$
  declare
    src     whx_host_dimension_target%rowtype;
    target  whx_host_dimension_target%rowtype;
    new_row wh_host_dimension%rowtype;
  begin
    select * into target
      from whx_host_dimension_target as t
     where t.host_id               = p_host_id
       and t.host_set_id           = p_host_set_id
       and t.target_id             = p_target_id;

    select target.id, t.* into src
      from whx_host_dimension_source as t
     where t.host_id               = p_host_id
       and t.host_set_id           = p_host_set_id
       and t.target_id             = p_target_id;

    if src is distinct from target then

      -- expire the current row
      update wh_host_dimension
         set current_row_indicator = 'Expired',
             row_expiration_time   = current_timestamp
       where host_id               = p_host_id
         and host_set_id           = p_host_set_id
         and target_id             = p_target_id
         and current_row_indicator = 'Current';

      -- insert a new row
      insert into wh_host_dimension (
             host_id,                    host_type,                  host_name,                       host_description,         host_address,
             host_set_id,                host_set_type,              host_set_name,                   host_set_description,
             host_catalog_id,            host_catalog_type,          host_catalog_name,               host_catalog_description,
             target_id,                  target_type,                target_name,                     target_description,
             target_default_port_number, target_session_max_seconds, target_session_connection_limit,
             project_id,                 project_name,               project_description,
             host_organization_id,       host_organization_name,     host_organization_description,
             current_row_indicator,      row_effective_time,         row_expiration_time
      )
      select host_id,                    host_type,                  host_name,                       host_description,         host_address,
             host_set_id,                host_set_type,              host_set_name,                   host_set_description,
             host_catalog_id,            host_catalog_type,          host_catalog_name,               host_catalog_description,
             target_id,                  target_type,                target_name,                     target_description,
             target_default_port_number, target_session_max_seconds, target_session_connection_limit,
             project_id,                 project_name,               project_description,
             host_organization_id,       host_organization_name,     host_organization_description,
             'Current',                  current_timestamp,          'infinity'::timestamptz
        from whx_host_dimension_source
       where host_id               = p_host_id
         and host_set_id           = p_host_set_id
         and target_id             = p_target_id
      returning * into new_row;

      return new_row.id;
    end if;
    return target.id;

  end;
  $$ language plpgsql;

  -- wh_upsert_user returns the wh_user_dimension id for p_user_id and
  -- p_auth_token_id. wh_upsert_user compares the current values in the
  -- wh_user_dimension with the current values in the operational tables for the
  -- provide parameters. If the values between the operational tables and the
  -- wh_user_dimension differ, a new row is inserted in the wh_user_dimension to
  -- match the current values in the operational tables and the new id is
  -- returned. If the values do not differ, the current id is returned.
  create or replace function wh_upsert_user(p_user_id wt_user_id, p_auth_token_id wt_public_id)
    returns wh_dim_id
  as $$
  declare
    src     whx_user_dimension_target%rowtype;
    target  whx_user_dimension_target%rowtype;
    new_row wh_user_dimension%rowtype;
    acct_id wt_public_id;
  begin
    select auth_account_id into strict acct_id
      from auth_token
     where public_id = p_auth_token_id;

    select * into target
      from whx_user_dimension_target as t
     where t.user_id               = p_user_id
       and t.auth_account_id       = acct_id;

    select target.id, t.* into src
      from whx_user_dimension_source as t
     where t.user_id               = p_user_id
       and t.auth_account_id       = acct_id;

    if src is distinct from target then

      -- expire the current row
      update wh_user_dimension
         set current_row_indicator = 'Expired',
             row_expiration_time   = current_timestamp
       where user_id               = p_user_id
         and auth_account_id       = acct_id
         and current_row_indicator = 'Current';

      -- insert a new row
      insert into wh_user_dimension (
             user_id,               user_name,              user_description,
             auth_account_id,       auth_account_type,      auth_account_name,             auth_account_description,
             auth_method_id,        auth_method_type,       auth_method_name,              auth_method_description,
             user_organization_id,  user_organization_name, user_organization_description,
             current_row_indicator, row_effective_time,     row_expiration_time
      )
      select user_id,               user_name,              user_description,
             auth_account_id,       auth_account_type,      auth_account_name,             auth_account_description,
             auth_method_id,        auth_method_type,       auth_method_name,              auth_method_description,
             user_organization_id,  user_organization_name, user_organization_description,
             'Current',             current_timestamp,      'infinity'::timestamptz
        from whx_user_dimension_source
       where user_id               = p_user_id
         and auth_account_id       = acct_id
      returning * into new_row;

      return new_row.id;
    end if;
    return target.id;

  end;
  $$ language plpgsql;
commit;

`),
	},
	"migrations/68_wh_session_facts.down.sql": {
		name: "68_wh_session_facts.down.sql",
		bytes: []byte(`
begin;

  drop table wh_session_connection_accumulating_fact;
  drop table wh_session_accumulating_fact;

commit;

`),
	},
	"migrations/68_wh_session_facts.up.sql": {
		name: "68_wh_session_facts.up.sql",
		bytes: []byte(`
begin;

  -- Column names for numeric fields that are not a measurement end in id or
  -- number. This naming convention enables automatic field type detection in
  -- certain data analysis tools.
  -- https://help.tableau.com/current/pro/desktop/en-us/data_clean_adm.htm

  -- The wh_session_accumulating_fact table is an accumulating snapshot.
  -- The table wh_session_accumulating_fact is an accumulating fact table.
  -- The grain of the fact table is one row per session.
  create table wh_session_accumulating_fact (
    session_id wt_public_id primary key,
    -- auth token id is a degenerate dimension
    auth_token_id wt_public_id not null,

    -- foreign keys to the dimension tables
    host_id wh_dim_id not null
      references wh_host_dimension (id)
      on delete restrict
      on update cascade,
    user_id wh_dim_id not null
      references wh_user_dimension (id)
      on delete restrict
      on update cascade,

    -- TODO(mgaffney) 09/2020: add dimension and foreign key for the session
    -- termination reason

    -- date and time foreign keys
    session_pending_date_id integer not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    session_pending_time_id integer not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    session_pending_time wh_timestamp,

    session_active_date_id integer default -1 not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    session_active_time_id integer default -1 not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    session_active_time wh_timestamp default 'infinity'::timestamptz,

    session_canceling_date_id integer default -1 not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    session_canceling_time_id integer default -1 not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    session_canceling_time wh_timestamp default 'infinity'::timestamptz,

    session_terminated_date_id integer default -1 not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    session_terminated_time_id integer default -1 not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    session_terminated_time wh_timestamp default 'infinity'::timestamptz,

    -- TODO(mgaffney) 09/2020: add columns for session expiration

    -- TODO(mgaffney) 09/2020: add connection limit. This may need a dimension
    -- table and foreign key column to represent unlimited connections.

    -- The total number of connections made during the session.
    total_connection_count bigint, -- will be null until the first connection is created

    -- The total number of bytes received by workers from the client and sent
    -- to the endpoint for this session.
    -- total_bytes_up is a fully additive measurement.
    total_bytes_up wh_bytes_transmitted, -- will be null until the first connection is closed
    -- The total number of bytes received by workers from the endpoint and sent
    -- to the client for this session.
    -- total_bytes_down is a fully additive measurement.
    total_bytes_down wh_bytes_transmitted -- will be null until the first connection is closed
  );

  -- The wh_session_connection_accumulating_fact table is an accumulating fact table.
  -- The grain of the fact table is one row per session connection.
  create table wh_session_connection_accumulating_fact  (
    connection_id wt_public_id primary key,
    -- session_id is a degenerate dimension
    session_id wt_public_id not null
      references wh_session_accumulating_fact (session_id)
      on delete cascade
      on update cascade,

    -- foreign keys to the dimension tables
    host_id wh_dim_id not null
      references wh_host_dimension (id)
      on delete restrict
      on update cascade,
    user_id wh_dim_id not null
      references wh_user_dimension (id)
      on delete restrict
      on update cascade,

    -- TODO(mgaffney) 09/2020: add dimension and foreign key for the connection
    -- closed reason

    -- date and time foreign keys and timestamps
    connection_authorized_date_id integer not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    connection_authorized_time_id integer not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    connection_authorized_time wh_timestamp,

    connection_connected_date_id integer default -1 not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    connection_connected_time_id integer default -1 not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    connection_connected_time wh_timestamp default 'infinity'::timestamptz,

    connection_closed_date_id integer default -1 not null
      references wh_date_dimension (id)
      on delete restrict
      on update cascade,
    connection_closed_time_id integer default -1 not null
      references wh_time_of_day_dimension (id)
      on delete restrict
      on update cascade,
    connection_closed_time wh_timestamp default 'infinity'::timestamptz,

    -- TODO(mgaffney) 09/2020: add a connection_duration_in_seconds column

    -- The client address and port are degenerate dimensions
    client_tcp_address inet, -- can be null
    client_tcp_port_number wh_inet_port, -- can be null

    -- The endpoint address and port are degenerate dimensions
    endpoint_tcp_address inet, -- can be null
    endpoint_tcp_port_number wh_inet_port, -- can be null

    -- the connection_count must always be 1
    -- this is a common pattern in data warehouse models
    -- See The Data Warehouse Toolkit, Third Edition
    -- by Ralph Kimball and Margy Ross for more information
    connection_count smallint default 1 not null
      constraint connection_count_must_be_1
      check(connection_count = 1),

    -- The total number of bytes received by the worker from the client and sent
    -- to the endpoint for this connection.
    -- bytes_up is a fully additive measurement.
    bytes_up wh_bytes_transmitted, -- can be null
    -- The total number of bytes received by the worker from the endpoint and sent
    -- to the client for this connection.
    -- bytes_down is a fully additive measurement.
    bytes_down wh_bytes_transmitted -- can be null
  );

  -- TODO(mgaffney) 09/2020: Research and test if the comment fields are used by
  -- data analysis tools.
  comment on table wh_session_connection_accumulating_fact is
    'The Wh Session Connection Accumulating Fact table is an accumulating fact table. '
    'The grain of the fact table is one row per session connection.';
  comment on column wh_session_connection_accumulating_fact.bytes_up is
    'Bytes Up is the total number of bytes received by the worker from the '
    'client and sent to the endpoint for this connection. Bytes Up is a fully '
    'additive measurement.';
  comment on column wh_session_connection_accumulating_fact.bytes_down is
    'Bytes Down is the total number of bytes received by the worker from the '
    'endpoint and sent to the client for this connection. Bytes Down is a fully '
    'additive measurement.';

  create index on wh_session_connection_accumulating_fact(session_id);

commit;

`),
	},
	"migrations/69_wh_session_facts.down.sql": {
		name: "69_wh_session_facts.down.sql",
		bytes: []byte(`
begin;

  drop trigger wh_insert_session_connection_state on session_connection_state;
  drop function wh_insert_session_connection_state;

  drop trigger wh_insert_session_state on session_state;
  drop function wh_insert_session_state;

  drop trigger wh_update_session_connection on session_connection;
  drop function wh_update_session_connection;

  drop trigger wh_insert_session_connection on session_connection;
  drop function wh_insert_session_connection;

  drop trigger wh_insert_session on session;
  drop function wh_insert_session;

  drop function wh_rollup_connections;

commit;

`),
	},
	"migrations/69_wh_session_facts.up.sql": {
		name: "69_wh_session_facts.up.sql",
		bytes: []byte(`
begin;

  -- wh_rollup_connections calculates the aggregate values from
  -- wh_session_connection_accumulating_fact for p_session_id and updates
  -- wh_session_accumulating_fact for p_session_id with those values.
  create or replace function wh_rollup_connections(p_session_id wt_public_id)
    returns void
  as $$
  declare
    session_row wh_session_accumulating_fact%rowtype;
  begin
    with
    session_totals (session_id, total_connection_count, total_bytes_up, total_bytes_down) as (
      select session_id,
             sum(connection_count),
             sum(bytes_up),
             sum(bytes_down)
        from wh_session_connection_accumulating_fact
       where session_id = p_session_id
       group by session_id
    )
    update wh_session_accumulating_fact
       set total_connection_count = session_totals.total_connection_count,
           total_bytes_up         = session_totals.total_bytes_up,
           total_bytes_down       = session_totals.total_bytes_down
      from session_totals
     where wh_session_accumulating_fact.session_id = session_totals.session_id
    returning wh_session_accumulating_fact.* into strict session_row;
  end;
  $$ language plpgsql;

  --
  -- Session triggers
  --

  -- wh_insert_session returns an after insert trigger for the session table
  -- which inserts a row in wh_session_accumulating_fact for the new session.
  -- wh_insert_session also calls the wh_upsert_host and wh_upsert_user
  -- functions which can result in new rows in wh_host_dimension and
  -- wh_user_dimension respectively.
  create or replace function wh_insert_session()
    returns trigger
  as $$
  declare
    new_row wh_session_accumulating_fact%rowtype;
  begin
    with
    pending_timestamp (date_dim_id, time_dim_id, ts) as (
      select wh_date_id(start_time), wh_time_id(start_time), start_time
        from session_state
       where session_id = new.public_id
         and state = 'pending'
    )
    insert into wh_session_accumulating_fact (
           session_id,
           auth_token_id,
           host_id,
           user_id,
           session_pending_date_id,
           session_pending_time_id,
           session_pending_time
    )
    select new.public_id,
           new.auth_token_id,
           wh_upsert_host(new.host_id, new.host_set_id, new.target_id),
           wh_upsert_user(new.user_id, new.auth_token_id),
           pending_timestamp.date_dim_id,
           pending_timestamp.time_dim_id,
           pending_timestamp.ts
      from pending_timestamp
      returning * into strict new_row;
    return null;
  end;
  $$ language plpgsql;

  create trigger wh_insert_session
    after insert on session
    for each row
    execute function wh_insert_session();

  --
  -- Session Connection triggers
  --

  -- wh_insert_session_connection returns an after insert trigger for the
  -- session_connection table which inserts a row in
  -- wh_session_connection_accumulating_fact for the new session connection.
  -- wh_insert_session_connection also calls wh_rollup_connections which can
  -- result in updates to wh_session_accumulating_fact.
  create or replace function wh_insert_session_connection()
    returns trigger
  as $$
  declare
    new_row wh_session_connection_accumulating_fact%rowtype;
  begin
    with
    authorized_timestamp (date_dim_id, time_dim_id, ts) as (
      select wh_date_id(start_time), wh_time_id(start_time), start_time
        from session_connection_state
       where connection_id = new.public_id
         and state = 'authorized'
    ),
    session_dimension (host_dim_id, user_dim_id) as (
      select host_id, user_id
        from wh_session_accumulating_fact
       where session_id = new.session_id
    )
    insert into wh_session_connection_accumulating_fact (
           connection_id,
           session_id,
           host_id,
           user_id,
           connection_authorized_date_id,
           connection_authorized_time_id,
           connection_authorized_time,
           client_tcp_address,
           client_tcp_port_number,
           endpoint_tcp_address,
           endpoint_tcp_port_number,
           bytes_up,
           bytes_down
    )
    select new.public_id,
           new.session_id,
           session_dimension.host_dim_id,
           session_dimension.user_dim_id,
           authorized_timestamp.date_dim_id,
           authorized_timestamp.time_dim_id,
           authorized_timestamp.ts,
           new.client_tcp_address,
           new.client_tcp_port,
           new.endpoint_tcp_address,
           new.endpoint_tcp_port,
           new.bytes_up,
           new.bytes_down
      from authorized_timestamp,
           session_dimension
      returning * into strict new_row;
    perform wh_rollup_connections(new.session_id);
    return null;
  end;
  $$ language plpgsql;

  create trigger wh_insert_session_connection
    after insert on session_connection
    for each row
    execute function wh_insert_session_connection();

  -- wh_update_session_connection returns an after update trigger for the
  -- session_connection table which updates a row in
  -- wh_session_connection_accumulating_fact for the session connection.
  -- wh_update_session_connection also calls wh_rollup_connections which can
  -- result in updates to wh_session_accumulating_fact.
  create or replace function wh_update_session_connection()
    returns trigger
  as $$
  declare
    updated_row wh_session_connection_accumulating_fact%rowtype;
  begin
        update wh_session_connection_accumulating_fact
           set client_tcp_address       = new.client_tcp_address,
               client_tcp_port_number   = new.client_tcp_port,
               endpoint_tcp_address     = new.endpoint_tcp_address,
               endpoint_tcp_port_number = new.endpoint_tcp_port,
               bytes_up                 = new.bytes_up,
               bytes_down               = new.bytes_down
         where connection_id = new.public_id
     returning * into strict updated_row;
    perform wh_rollup_connections(new.session_id);
    return null;
  end;
  $$ language plpgsql;

  create trigger wh_update_session_connection
    after update on session_connection
    for each row
    execute function wh_update_session_connection();

  --
  -- Session State trigger
  --

  -- wh_insert_session_state returns an after insert trigger for the
  -- session_state table which updates wh_session_accumulating_fact.
  create or replace function wh_insert_session_state()
    returns trigger
  as $$
  declare
    date_col text;
    time_col text;
    ts_col text;
    q text;
    session_row wh_session_accumulating_fact%rowtype;
  begin
    if new.state = 'pending' then
      -- The pending state is the first state which is handled by the
      -- wh_insert_session trigger. The update statement in this trigger will
      -- fail for the pending state because the row for the session has not yet
      -- been inserted into the wh_session_accumulating_fact table.
      return null;
    end if;

    date_col = 'session_' || new.state || '_date_id';
    time_col = 'session_' || new.state || '_time_id';
    ts_col   = 'session_' || new.state || '_time';

    q = format('update wh_session_accumulating_fact
                   set (%I, %I, %I) = (select wh_date_id(%L), wh_time_id(%L), %L::timestamptz)
                 where session_id = %L
                returning *',
                date_col,       time_col,       ts_col,
                new.start_time, new.start_time, new.start_time,
                new.session_id);
    execute q into strict session_row;

    return null;
  end;
  $$ language plpgsql;

  create trigger wh_insert_session_state
    after insert on session_state
    for each row
    execute function wh_insert_session_state();

  --
  -- Session Connection State trigger
  --

  -- wh_insert_session_connection_state returns an after insert trigger for the
  -- session_connection_state table which updates
  -- wh_session_connection_accumulating_fact.
  create or replace function wh_insert_session_connection_state()
    returns trigger
  as $$
  declare
    date_col text;
    time_col text;
    ts_col text;
    q text;
    connection_row wh_session_connection_accumulating_fact%rowtype;
  begin
    if new.state = 'authorized' then
      -- The authorized state is the first state which is handled by the
      -- wh_insert_session_connection trigger. The update statement in this
      -- trigger will fail for the authorized state because the row for the
      -- session connection has not yet been inserted into the
      -- wh_session_connection_accumulating_fact table.
      return null;
    end if;

    date_col = 'connection_' || new.state || '_date_id';
    time_col = 'connection_' || new.state || '_time_id';
    ts_col   = 'connection_' || new.state || '_time';

    q = format('update wh_session_connection_accumulating_fact
                   set (%I, %I, %I) = (select wh_date_id(%L), wh_time_id(%L), %L::timestamptz)
                 where connection_id = %L
                returning *',
                date_col,       time_col,       ts_col,
                new.start_time, new.start_time, new.start_time,
                new.connection_id);
    execute q into strict connection_row;

    return null;
  end;
  $$ language plpgsql;

  create trigger wh_insert_session_connection_state
    after insert on session_connection_state
    for each row
    execute function wh_insert_session_connection_state();

commit;

`),
	},
}
