diff --git a/go.mod b/go.mod index e279ef366..d7020b2a9 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-openapi/validate v0.19.8 // indirect github.com/go-swagger/go-swagger v0.23.0 github.com/golang-migrate/migrate/v4 v4.11.0 + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe github.com/golang/protobuf v1.4.2 github.com/grpc-ecosystem/grpc-gateway v1.14.5 github.com/hashicorp/errwrap v1.0.0 diff --git a/internal/db/domains_test.go b/internal/db/domains_test.go index 21b075922..3ded10a4d 100644 --- a/internal/db/domains_test.go +++ b/internal/db/domains_test.go @@ -2,8 +2,11 @@ package db import ( "testing" + "time" + "github.com/golang-sql/civil" _ "github.com/jackc/pgx/v4" + "github.com/stretchr/testify/assert" ) func TestDomain_PublicId(t *testing.T) { @@ -110,3 +113,200 @@ returning id; t.Errorf("want error, got no error") } } + +func TestDomain_immutable_create_time_func(t *testing.T) { + assert := assert.New(t) + + const ( + createTable = ` +create table if not exists test_immutable_create_time ( + id bigint generated always as identity primary key, + name text, + create_time wt_timestamp +); +` + addTriggers = ` +create trigger + test_update_immutable_create_time +before +update on test_immutable_create_time + for each row execute procedure immutable_create_time_func(); +` + query = ` +select create_time from test_immutable_create_time where id = $1 +` + countQuery = ` +select count(*) from test_immutable_create_time +` + insert = ` +insert into test_immutable_create_time (name) +values ('alice') +returning id; +` + // no where clause intentionally, since it's not needed for the test + update = ` +update test_immutable_create_time +set name = 'bob'; +` + + // no where clause intentionally, since it's not needed for the test + bad_update = ` +update test_immutable_create_time +set create_time = now(); +` + // no where clause intentionally, since it's not needed for the test + bad_update_null = ` +update test_immutable_create_time +set create_time = null; +` + ) + + cleanup, conn, _ := TestSetup(t, "postgres") + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + if err := cleanup(); err != nil { + t.Error(err) + } + }() + + db := conn.DB() + _, err := db.Exec(createTable) + assert.NoError(err) + + _, err = db.Exec(addTriggers) + assert.NoError(err) + + var id int + err = db.QueryRow(insert).Scan(&id) + assert.NoError(err) + + var origTime time.Time + err = db.QueryRow(query, id).Scan(&origTime) + assert.NoError(err) + + var count int + err = db.QueryRow(countQuery).Scan(&count) + assert.NoError(err) + assert.Equal(1, count) + + _, err = db.Exec(update) + assert.NoError(err) + + _, err = db.Exec(bad_update) + assert.NoError(err) + + var updateTime time.Time + err = db.QueryRow(query, id).Scan(&updateTime) + assert.NoError(err) + assert.Equal(origTime, updateTime) + + _, err = db.Exec(bad_update_null) + assert.NoError(err) + + err = db.QueryRow(query, id).Scan(&updateTime) + assert.NoError(err) + assert.Equal(origTime, updateTime) + +} + +func TestDomain_update_time_column(t *testing.T) { + assert := assert.New(t) + + const ( + createTable = ` +create table if not exists test_update_time_column ( + id bigint generated always as identity primary key, + name text, + update_time wt_timestamp +); +` + addTriggers = ` +create trigger + test_update_time_column +before +update on test_update_time_column + for each row execute procedure update_time_column(); +` + countQuery = ` +select count(*) from test_update_time_column +` + query = ` +select update_time from test_update_time_column where id = $1 +` + insert = ` +insert into test_update_time_column (name) +values ('bob') +returning id; +` + // no where clause intentionally, since it's not needed for the test + update = ` +update test_update_time_column +set name = 'alice'; +` + + // no where clause intentionally, since it's not needed for the test + bad_update = ` +update test_update_time_column +set update_time = now() - interval '3 days'; +` + // no where clause intentionally, since it's not needed for the test + bad_update_null = ` +update test_update_time_column +set update_time = null; +` + ) + + cleanup, conn, _ := TestSetup(t, "postgres") + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + if err := cleanup(); err != nil { + t.Error(err) + } + }() + + db := conn.DB() + _, err := db.Exec(createTable) + assert.NoError(err) + + _, err = db.Exec(addTriggers) + assert.NoError(err) + + var id int + err = db.QueryRow(insert).Scan(&id) + assert.NoError(err) + + var origTime time.Time + err = db.QueryRow(query, id).Scan(&origTime) + assert.NoError(err) + orig := civil.DateTimeOf(origTime) + + var count int + err = db.QueryRow(countQuery).Scan(&count) + assert.NoError(err) + assert.Equal(1, count) + + _, err = db.Exec(update) + assert.NoError(err) + + _, err = db.Exec(bad_update) + assert.NoError(err) + + var updateTime time.Time + err = db.QueryRow(query, id).Scan(&updateTime) + assert.NoError(err) + updated := civil.DateTimeOf(updateTime) + assert.True(updated.After(orig)) + + _, err = db.Exec(bad_update_null) + assert.NoError(err) + + var nextUpdateTime time.Time + err = db.QueryRow(query, id).Scan(&nextUpdateTime) + assert.NoError(err) + nextUpdated := civil.DateTimeOf(nextUpdateTime) + assert.True(nextUpdated.After(updated)) +} diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index 6684238b4..e41af1d1d 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -12,7 +12,8 @@ begin; drop domain wt_timestamp; drop domain wt_public_id; - +drop function update_time_column() cascade; +drop function immutable_create_time_func() cascade; commit; `), @@ -35,8 +36,87 @@ create domain wt_timestamp as 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 + immutable_create_time_func() + returns trigger +as $$ +begin + if new.create_time is distinct from old.create_time then + raise warning 'create_time cannot be set to %', new.create_time; + new.create_time = old.create_time; + end if; + return new; +end; +$$ language plpgsql; + +comment on function + immutable_create_time_func() +is + 'function used in before update triggers to make create_time column immutable'; + +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 + immutable_create_time_func() +is + 'function used in before update triggers to make create_time column immutable'; + commit; +`), + }, + "migrations/02_oplog.down.sql": { + name: "02_oplog.down.sql", + bytes: []byte(` +begin; + +drop table if exists oplog_entry cascade; + +drop trigger if exists update_oplog_entry_update_time on oplog_entry; +drop trigger if exists update_oplog_entry_create_time on oplog_entry; + +drop table if exists oplog_ticket cascade; + +drop trigger if exists update_oplog_ticket_update_time on oplog_ticket; +drop trigger if exists update_oplog_ticket_create_time on oplog_ticket; + +drop table if exists oplog_metadata cascade; + +drop trigger if exists update_oplog_metadata_update_time on oplog_metadata; +drop trigger if exists update_oplog_metadata_create_time on oplog_metadata; + +commit; `), }, "migrations/02_oplog.up.sql": { @@ -44,31 +124,91 @@ commit; bytes: []byte(` begin; -CREATE TABLE if not exists oplog_entry ( +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 + version text not null, + aggregate_name text not null, + "data" bytea not null ); -CREATE TABLE if not exists oplog_ticket ( + +create trigger + update_time_column +before +update on oplog_entry + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on oplog_entry + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on oplog_entry + for each row execute procedure default_create_time(); + +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 + "name" text not null unique, + "version" bigint not null ); -CREATE TABLE if not exists oplog_metadata ( + +create trigger + update_time_column +before +update on oplog_ticket + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on oplog_ticket + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on oplog_ticket + for each row execute procedure default_create_time(); + +create table if not exists oplog_metadata ( id bigint generated always as identity primary key, create_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 + 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 + create_time_column +before +update on oplog_metadata + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on oplog_metadata + for each row execute procedure default_create_time(); + 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) + +insert into oplog_ticket (name, version) values ('default', 1), ('iam_scope', 1), @@ -86,6 +226,7 @@ values commit; + `), }, "migrations/03_db.down.sql": { @@ -97,6 +238,15 @@ drop table if exists db_test_user; drop table if exists db_test_car; drop table if exists db_test_rental; +drop trigger if exists update_db_test_user_update_time on db_test_user; +drop trigger if exists update_db_test_user_create_time on db_test_user; + +drop trigger if exists update_db_test_car_update_time on db_test_car; +drop trigger if exists update_db_test_car_create_time on db_test_car; + +drop trigger if exists update_db_test_rental_update_time on db_test_rental; +drop trigger if exists update_db_test_rental_create_time on db_test_rental; + commit; `), @@ -108,35 +258,92 @@ 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) are not part --- of the Watchtower domain model... they are simply used for testing the internal/db package -CREATE TABLE if not exists db_test_user ( +-- of the watchtower 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, + public_id text not null unique, + name text unique, phone_number text, email text ); -CREATE TABLE if not exists db_test_car ( + +create trigger + update_time_column +before +update on db_test_user + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on db_test_user + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on db_test_user + for each row execute procedure default_create_time(); + +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, + public_id text not null unique, + name text unique, model text, mpg smallint ); -CREATE TABLE if not exists db_test_rental ( + +create trigger + update_time_column +before +update on db_test_car + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on db_test_car + for each row execute procedure immutable_create_time_func(); + +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) + 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(); + +create trigger + create_time_column +before +update on db_test_rental + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on db_test_rental + for each row execute procedure default_create_time(); + + commit; `), @@ -150,98 +357,128 @@ drop table if exists iam_scope CASCADE; drop trigger if exists iam_scope_insert; drop function if exists iam_sub_scopes_func; +drop trigger if exists update_iam_scope_update_time on iam_scope; +drop trigger if exists update_iam_scope_create_time on iam_scope; + COMMIT; `), }, "migrations/04_iam.up.sql": { name: "04_iam.up.sql", bytes: []byte(` -BEGIN; +begin; -CREATE TABLE iam_scope_type_enm ( - string text NOT NULL primary key CHECK(string IN ('unknown', 'organization', 'project')) +create table iam_scope_type_enm ( + string text not null primary key check(string in ('unknown', 'organization', 'project')) ); -INSERT INTO iam_scope_type_enm (string) + +insert into iam_scope_type_enm (string) values ('unknown'), ('organization'), ('project'); -CREATE TABLE iam_scope ( +create table iam_scope ( public_id wt_public_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, - type text NOT NULL REFERENCES iam_scope_type_enm(string) CHECK( + type text not null references iam_scope_type_enm(string) check( ( type = 'organization' - and parent_id = NULL + and parent_id = null ) or ( type = 'project' - and parent_id IS NOT NULL + and parent_id is not null ) ), description text, - parent_id text REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE + parent_id text references iam_scope(public_id) on delete cascade on update cascade ); + create table iam_scope_organization ( - scope_id wt_public_id NOT NULL UNIQUE REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE, - name text UNIQUE, + scope_id wt_public_id not null unique references iam_scope(public_id) on delete cascade on update cascade, + name text unique, primary key(scope_id) ); + create table iam_scope_project ( - scope_id wt_public_id NOT NULL REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE, - parent_id wt_public_id NOT NULL REFERENCES iam_scope_organization(scope_id) ON DELETE CASCADE ON UPDATE CASCADE, + scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + parent_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade, name text, unique(parent_id, name), primary key(scope_id, parent_id) ); - -CREATE - OR REPLACE FUNCTION iam_sub_scopes_func() RETURNS TRIGGER -SET SCHEMA - 'public' LANGUAGE plpgsql AS $$ DECLARE parent_type INT; -BEGIN IF new.type = 'organization' THEN -insert into iam_scope_organization (scope_id, name) -values - (new.public_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; -$$; +create or replace function + iam_sub_scopes_func() + returns trigger +as $$ +declare parent_type int; +begin + if new.type = 'organization' then + insert into iam_scope_organization (scope_id, name) + values + (new.public_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 trigger + iam_scope_insert +after +insert on iam_scope + for each row execute procedure iam_sub_scopes_func(); -CREATE - OR REPLACE FUNCTION iam_immutable_scope_type_func() RETURNS TRIGGER -SET SCHEMA - 'public' LANGUAGE plpgsql AS $$ DECLARE parent_type INT; -BEGIN IF new.type != old.type THEN -RAISE EXCEPTION 'scope type cannot be updated'; -END IF; -return NEW; -END; -$$; +create or replace function + iam_immutable_scope_type_func() + returns trigger +as $$ +declare parent_type int; +begin + if new.type != old.type then + raise exception 'scope type cannot be updated'; + end if; + return new; +end; +$$ language plpgsql; -CREATE TRIGGER iam_scope_update -BEFORE -update ON iam_scope FOR EACH ROW EXECUTE PROCEDURE iam_immutable_scope_type_func(); +create trigger + iam_scope_update +before +update on iam_scope + for each row execute procedure iam_immutable_scope_type_func(); -COMMIT; +create trigger + update_time_column +before update on iam_scope + for each row execute procedure update_time_column(); + +create trigger + immutable_create_time +before +update on iam_scope + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on iam_scope + for each row execute procedure default_create_time(); + +commit; `), }, diff --git a/internal/db/migrations/postgres/01_domain_types.down.sql b/internal/db/migrations/postgres/01_domain_types.down.sql index 41adb0fc9..987ef1c3b 100644 --- a/internal/db/migrations/postgres/01_domain_types.down.sql +++ b/internal/db/migrations/postgres/01_domain_types.down.sql @@ -2,5 +2,6 @@ begin; drop domain wt_timestamp; drop domain wt_public_id; - +drop function update_time_column() cascade; +drop function immutable_create_time_func() cascade; commit; diff --git a/internal/db/migrations/postgres/01_domain_types.up.sql b/internal/db/migrations/postgres/01_domain_types.up.sql index 45e63e5ff..634213ba9 100644 --- a/internal/db/migrations/postgres/01_domain_types.up.sql +++ b/internal/db/migrations/postgres/01_domain_types.up.sql @@ -13,4 +13,60 @@ create domain wt_timestamp as 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 + immutable_create_time_func() + returns trigger +as $$ +begin + if new.create_time is distinct from old.create_time then + raise warning 'create_time cannot be set to %', new.create_time; + new.create_time = old.create_time; + end if; + return new; +end; +$$ language plpgsql; + +comment on function + immutable_create_time_func() +is + 'function used in before update triggers to make create_time column immutable'; + +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 + immutable_create_time_func() +is + 'function used in before update triggers to make create_time column immutable'; + commit; diff --git a/internal/db/migrations/postgres/02_oplog.down.sql b/internal/db/migrations/postgres/02_oplog.down.sql new file mode 100644 index 000000000..649b22268 --- /dev/null +++ b/internal/db/migrations/postgres/02_oplog.down.sql @@ -0,0 +1,18 @@ +begin; + +drop table if exists oplog_entry cascade; + +drop trigger if exists update_oplog_entry_update_time on oplog_entry; +drop trigger if exists update_oplog_entry_create_time on oplog_entry; + +drop table if exists oplog_ticket cascade; + +drop trigger if exists update_oplog_ticket_update_time on oplog_ticket; +drop trigger if exists update_oplog_ticket_create_time on oplog_ticket; + +drop table if exists oplog_metadata cascade; + +drop trigger if exists update_oplog_metadata_update_time on oplog_metadata; +drop trigger if exists update_oplog_metadata_create_time on oplog_metadata; + +commit; \ No newline at end of file diff --git a/internal/db/migrations/postgres/02_oplog.up.sql b/internal/db/migrations/postgres/02_oplog.up.sql index 7a7a79649..6c11dd1d6 100644 --- a/internal/db/migrations/postgres/02_oplog.up.sql +++ b/internal/db/migrations/postgres/02_oplog.up.sql @@ -1,30 +1,90 @@ begin; -CREATE TABLE if not exists oplog_entry ( +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 + version text not null, + aggregate_name text not null, + "data" bytea not null ); -CREATE TABLE if not exists oplog_ticket ( + +create trigger + update_time_column +before +update on oplog_entry + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on oplog_entry + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on oplog_entry + for each row execute procedure default_create_time(); + +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 + "name" text not null unique, + "version" bigint not null ); -CREATE TABLE if not exists oplog_metadata ( + +create trigger + update_time_column +before +update on oplog_ticket + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on oplog_ticket + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on oplog_ticket + for each row execute procedure default_create_time(); + +create table if not exists oplog_metadata ( id bigint generated always as identity primary key, create_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 + 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 + create_time_column +before +update on oplog_metadata + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on oplog_metadata + for each row execute procedure default_create_time(); + 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) + +insert into oplog_ticket (name, version) values ('default', 1), ('iam_scope', 1), @@ -41,3 +101,4 @@ values ('db_test_rental', 1); commit; + diff --git a/internal/db/migrations/postgres/03_db.down.sql b/internal/db/migrations/postgres/03_db.down.sql index d4c3b5217..c8b98a43b 100644 --- a/internal/db/migrations/postgres/03_db.down.sql +++ b/internal/db/migrations/postgres/03_db.down.sql @@ -4,4 +4,13 @@ drop table if exists db_test_user; drop table if exists db_test_car; drop table if exists db_test_rental; +drop trigger if exists update_db_test_user_update_time on db_test_user; +drop trigger if exists update_db_test_user_create_time on db_test_user; + +drop trigger if exists update_db_test_car_update_time on db_test_car; +drop trigger if exists update_db_test_car_create_time on db_test_car; + +drop trigger if exists update_db_test_rental_update_time on db_test_rental; +drop trigger if exists update_db_test_rental_create_time on db_test_rental; + commit; diff --git a/internal/db/migrations/postgres/03_db.up.sql b/internal/db/migrations/postgres/03_db.up.sql index 07ba34cf0..b5277d93c 100644 --- a/internal/db/migrations/postgres/03_db.up.sql +++ b/internal/db/migrations/postgres/03_db.up.sql @@ -2,33 +2,90 @@ 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) are not part --- of the Watchtower domain model... they are simply used for testing the internal/db package -CREATE TABLE if not exists db_test_user ( +-- of the watchtower 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, + public_id text not null unique, + name text unique, phone_number text, email text ); -CREATE TABLE if not exists db_test_car ( + +create trigger + update_time_column +before +update on db_test_user + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on db_test_user + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on db_test_user + for each row execute procedure default_create_time(); + +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, + public_id text not null unique, + name text unique, model text, mpg smallint ); -CREATE TABLE if not exists db_test_rental ( + +create trigger + update_time_column +before +update on db_test_car + for each row execute procedure update_time_column(); + +create trigger + create_time_column +before +update on db_test_car + for each row execute procedure immutable_create_time_func(); + +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) + 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(); + +create trigger + create_time_column +before +update on db_test_rental + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on db_test_rental + for each row execute procedure default_create_time(); + + commit; diff --git a/internal/db/migrations/postgres/04_iam.down.sql b/internal/db/migrations/postgres/04_iam.down.sql index 12cabee1b..74363b865 100644 --- a/internal/db/migrations/postgres/04_iam.down.sql +++ b/internal/db/migrations/postgres/04_iam.down.sql @@ -4,4 +4,7 @@ drop table if exists iam_scope CASCADE; drop trigger if exists iam_scope_insert; drop function if exists iam_sub_scopes_func; +drop trigger if exists update_iam_scope_update_time on iam_scope; +drop trigger if exists update_iam_scope_create_time on iam_scope; + COMMIT; \ No newline at end of file diff --git a/internal/db/migrations/postgres/04_iam.up.sql b/internal/db/migrations/postgres/04_iam.up.sql index 1309f9790..1722bd1dd 100644 --- a/internal/db/migrations/postgres/04_iam.up.sql +++ b/internal/db/migrations/postgres/04_iam.up.sql @@ -1,86 +1,113 @@ -BEGIN; +begin; -CREATE TABLE iam_scope_type_enm ( - string text NOT NULL primary key CHECK(string IN ('unknown', 'organization', 'project')) +create table iam_scope_type_enm ( + string text not null primary key check(string in ('unknown', 'organization', 'project')) ); -INSERT INTO iam_scope_type_enm (string) + +insert into iam_scope_type_enm (string) values ('unknown'), ('organization'), ('project'); -CREATE TABLE iam_scope ( +create table iam_scope ( public_id wt_public_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, - type text NOT NULL REFERENCES iam_scope_type_enm(string) CHECK( + type text not null references iam_scope_type_enm(string) check( ( type = 'organization' - and parent_id = NULL + and parent_id = null ) or ( type = 'project' - and parent_id IS NOT NULL + and parent_id is not null ) ), description text, - parent_id text REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE + parent_id text references iam_scope(public_id) on delete cascade on update cascade ); + create table iam_scope_organization ( - scope_id wt_public_id NOT NULL UNIQUE REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE, - name text UNIQUE, + scope_id wt_public_id not null unique references iam_scope(public_id) on delete cascade on update cascade, + name text unique, primary key(scope_id) ); + create table iam_scope_project ( - scope_id wt_public_id NOT NULL REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE, - parent_id wt_public_id NOT NULL REFERENCES iam_scope_organization(scope_id) ON DELETE CASCADE ON UPDATE CASCADE, + scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + parent_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade, name text, unique(parent_id, name), primary key(scope_id, parent_id) ); - -CREATE - OR REPLACE FUNCTION iam_sub_scopes_func() RETURNS TRIGGER -SET SCHEMA - 'public' LANGUAGE plpgsql AS $$ DECLARE parent_type INT; -BEGIN IF new.type = 'organization' THEN -insert into iam_scope_organization (scope_id, name) -values - (new.public_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; -$$; +create or replace function + iam_sub_scopes_func() + returns trigger +as $$ +declare parent_type int; +begin + if new.type = 'organization' then + insert into iam_scope_organization (scope_id, name) + values + (new.public_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 trigger + iam_scope_insert +after +insert on iam_scope + for each row execute procedure iam_sub_scopes_func(); -CREATE - OR REPLACE FUNCTION iam_immutable_scope_type_func() RETURNS TRIGGER -SET SCHEMA - 'public' LANGUAGE plpgsql AS $$ DECLARE parent_type INT; -BEGIN IF new.type != old.type THEN -RAISE EXCEPTION 'scope type cannot be updated'; -END IF; -return NEW; -END; -$$; +create or replace function + iam_immutable_scope_type_func() + returns trigger +as $$ +declare parent_type int; +begin + if new.type != old.type then + raise exception 'scope type cannot be updated'; + end if; + return new; +end; +$$ language plpgsql; -CREATE TRIGGER iam_scope_update -BEFORE -update ON iam_scope FOR EACH ROW EXECUTE PROCEDURE iam_immutable_scope_type_func(); +create trigger + iam_scope_update +before +update on iam_scope + for each row execute procedure iam_immutable_scope_type_func(); -COMMIT; +create trigger + update_time_column +before update on iam_scope + for each row execute procedure update_time_column(); + +create trigger + immutable_create_time +before +update on iam_scope + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on iam_scope + for each row execute procedure default_create_time(); + +commit;