diff --git a/src/main/resources/sql/fixtures/04-article.sql b/src/main/resources/sql/fixtures/04-article.sql index 4ae4e29..ee260c2 100644 --- a/src/main/resources/sql/fixtures/04-article.sql +++ b/src/main/resources/sql/fixtures/04-article.sql @@ -13,7 +13,7 @@ begin delete from article_relations; delete from article; - insert into article (id, version_id, created_by_id, title, anonymous, content, description, tags, created_at) + insert into article (id, version_id, created_by_id, title, anonymous, content, description, tags, created_at, is_draft) select uuid_in(md5('article'||row_number() over ())::cstring), uuid_in(md5('article_v'||row_number() over () % (_citizen_count / 2))::cstring), @@ -23,7 +23,8 @@ begin 'content' || row_number() over (), 'description' || row_number() over (), _tags[(row_number() over () % 5):(row_number() over () % 9)], - now() + (row_number() over () * interval '7 minute 3 second') + now() + (row_number() over () * interval '7 minute 3 second'), + (row_number() over () % 7) = 0 from citizen z; insert into article_relations (source_id, target_id, created_by_id, comment) diff --git a/src/main/resources/sql/functions/article/find_last_article_by_version_id.sql b/src/main/resources/sql/functions/article/find_last_article_by_version_id.sql index 795b5aa..2f03dee 100644 --- a/src/main/resources/sql/functions/article/find_last_article_by_version_id.sql +++ b/src/main/resources/sql/functions/article/find_last_article_by_version_id.sql @@ -11,6 +11,8 @@ begin into resource from article as a where a.version_id = _version_id + and a.is_draft = false + and a.deleted_at is null order by a.version_number desc limit 1 ) as t; diff --git a/src/main/resources/sql/migrations/0000-init_schema.down.sql b/src/main/resources/sql/migrations/0000-init_schema.down.sql index 8e55653..f2885a1 100644 --- a/src/main/resources/sql/migrations/0000-init_schema.down.sql +++ b/src/main/resources/sql/migrations/0000-init_schema.down.sql @@ -27,11 +27,19 @@ drop table if exists title; drop function if exists set_constitution_link(); drop trigger if exists generate_version_number_trigger on article; +drop trigger if exists set_to_last_version_trigger on article; +drop trigger if exists set_last_version_trigger on article; drop table if exists article; drop function if exists generate_version_number(regclass, uuid); +drop function if exists set_all_version_to_old(regclass, uuid); drop trigger if exists generate_version_number_trigger on constitution; +drop trigger if exists set_to_last_version_trigger on constitution; +drop trigger if exists set_last_version_trigger on constitution; drop table if exists constitution; drop function if exists set_version_number(); +drop function if exists set_to_last_version(); +drop function if exists set_last_version(); +drop function if exists set_correct_last_version(); -- User drop table if exists moderator; diff --git a/src/main/resources/sql/migrations/0000-init_schema.up.sql b/src/main/resources/sql/migrations/0000-init_schema.up.sql index c66e0bb..6316c77 100644 --- a/src/main/resources/sql/migrations/0000-init_schema.up.sql +++ b/src/main/resources/sql/migrations/0000-init_schema.up.sql @@ -51,7 +51,10 @@ create table moderator user_id uuid not null references "user" (id) ); --- Article & Constitution + +------------------------------------- +-- Article & Constitution triggers -- +------------------------------------- create or replace function generate_version_number(tablename regclass, version_id uuid, out generated_number int) language plpgsql as @@ -83,6 +86,65 @@ begin end; $$; +create or replace function set_all_version_to_old(tablename regclass, version_id uuid) returns void + language plpgsql as +$$ +declare + _version_id alias for version_id; +begin + if (tablename = 'article'::regclass) then + update article a + set is_last_version = false + where a.version_id = _version_id and a.is_last_version = true; + elseif (tablename = 'constitution'::regclass) then + update constitution c + set is_last_version = false + where c.version_id = _version_id and c.is_last_version = true; + else + raise exception '% is not implemented', tablename::text; + end if; +end; +$$; + +create or replace function set_correct_last_version(tablename regclass, version_id uuid) returns void + language plpgsql as +$$ +declare + _version_id alias for version_id; +begin + perform set_all_version_to_old(tablename, _version_id); + + if (tablename = 'article'::regclass) then + update article a1 + set is_last_version = true + from ( + select id from article a2 + where a2.version_id = _version_id + and a2.is_draft = false + and a2.deleted_at is null + order by version_number desc + limit 1 + ) as a3 + where a1.version_id = _version_id and a1.id = a3.id; + elseif (tablename = 'constitution'::regclass) then + update constitution c1 + set is_last_version = true + from ( + select id from constitution c2 + where c2.version_id = _version_id + and c2.is_draft = false + and c2.deleted_at is null + order by version_number desc + limit 1 + ) as c3 + where c1.version_id = _version_id and c1.id = c3.id; + else + raise exception '% is not implemented', tablename::text; + end if; +end; +$$; + + create or replace function set_version_number() returns trigger language plpgsql as $$ @@ -92,48 +154,115 @@ begin end; $$; +create or replace function set_to_last_version() returns trigger + language plpgsql as +$$ +begin + if (new.is_draft = false and new.deleted_at is null) then + perform set_all_version_to_old(tg_table_name::regclass, new.version_id); + new.is_last_version = true; + else + new.is_last_version = false; + end if; + return new; +end; +$$; + +create or replace function set_last_version() returns trigger + language plpgsql as +$$ +begin + if (new.is_draft != old.is_draft or new.deleted_at != old.deleted_at) then + perform set_correct_last_version(tg_table_name::regclass, new.version_id); + end if; + return new; +end; +$$; + +------------- +-- Article -- +------------- create table article ( - id uuid default uuid_generate_v4() not null primary key, - created_at timestamptz default now() not null, - created_by_id uuid not null references citizen (id), - version_id uuid default uuid_generate_v4() not null, - version_number int not null, - title text not null check ( length(title) < 128 ), - anonymous boolean default false not null, - content text not null check ( content != '' and length(content) < 4096 ), - description text null check ( description != '' and length(description) < 4096 ), - tags varchar(32)[] default '{}' not null, - deleted_at timestamptz default null null, + id uuid default uuid_generate_v4() not null primary key, + created_at timestamptz default now() not null, + created_by_id uuid not null references citizen (id), + version_id uuid default uuid_generate_v4() not null, + version_number int not null, + title text not null check ( length(title) < 128 ), + anonymous boolean default false not null, + content text not null check ( content != '' and length(content) < 4096 ), + description text null check ( description != '' and length(description) < 4096 ), + tags varchar(32)[] default '{}' not null, + deleted_at timestamptz default null null, + is_draft boolean default false not null, + is_last_version boolean default false not null, unique (version_id, version_number) ); +create unique index last_version_article_idx on article (is_last_version, version_id) where is_last_version = true; + create trigger generate_version_number_trigger before insert on article for each row execute function set_version_number(); +create trigger set_to_last_version_trigger + before insert + on article + for each row +execute function set_to_last_version(); + +create trigger set_last_version_trigger + after update + on article + for each row +execute function set_last_version(); + +------------------ +-- Constitution -- +------------------ + create table constitution ( - id uuid default uuid_generate_v4() not null primary key, - created_at timestamptz default now() not null, - created_by_id uuid not null references citizen (id), - version_id uuid default uuid_generate_v4() not null, - version_number int not null, - title text not null check ( length(title) < 128 ), - intro text null check ( length(intro) < 4096 ), - anonymous boolean default false not null, - deleted_at timestamptz default null null, + id uuid default uuid_generate_v4() not null primary key, + created_at timestamptz default now() not null, + created_by_id uuid not null references citizen (id), + version_id uuid default uuid_generate_v4() not null, + version_number int not null, + title text not null check ( length(title) < 128 ), + intro text null check ( length(intro) < 4096 ), + anonymous boolean default false not null, + deleted_at timestamptz default null null, + is_draft boolean default false not null, + is_last_version boolean default false not null, unique (version_id, version_number) ); +create unique index last_version_constitution_idx on constitution (is_last_version, version_id) where is_last_version = true; + create trigger generate_version_number_trigger before insert on constitution for each row execute procedure set_version_number(); +create trigger set_to_last_version_trigger + before insert + on constitution + for each row +execute function set_to_last_version(); + +create trigger set_last_version_trigger + after update + on constitution + for each row +execute function set_last_version(); + +------ + + create table title ( id uuid default uuid_generate_v4() not null primary key, @@ -364,196 +493,207 @@ create table resource_view - - -------------- -- ZOMBO DB -- -------------- -- Filter -SELECT zdb.define_filter('french_stop', '{ - "type": "stop", - "stopwords": "_french_", - "ignore_case": true +select zdb.define_filter('french_stop', '{ + "type": "stop", + "stopwords": "_french_", + "ignore_case": true }'); -SELECT zdb.define_filter('french_elision', '{ - "type": "elision", - "articles": [ - "à", - "ainsi", - "alors", - "assez", - "au", - "aussi", - "aux", - "c", - "ça", - "car", - "ce", - "cela", - "ces", - "ceux", - "ci", - "celle", - "celles", - "d", - "de", - "déjà", - "depuis", - "des", - "donc", - "du", - "et", - "ici", - "l", - "la", - "là", - "le", - "les", - "leur", - "leurs", - "ma", - "mais", - "même", - "mes", - "mon", - "ne", - "ni", - "notre", - "nous", - "ou", - "où", - "s", - "sa", - "ses", - "son", - "t", - "ta", - "tant", - "tantôt", - "tels", - "tes", - "ton", - "tôt", - "toujours", - "trop", - "un", - "une", - "votre", - "vos" - ], - "ignore_case": true +select zdb.define_filter('french_elision', '{ + "type": "elision", + "articles": [ + "à", + "ainsi", + "alors", + "assez", + "au", + "aussi", + "aux", + "c", + "ça", + "car", + "ce", + "cela", + "ces", + "ceux", + "ci", + "celle", + "celles", + "d", + "de", + "déjà", + "depuis", + "des", + "donc", + "du", + "et", + "ici", + "l", + "la", + "là", + "le", + "les", + "leur", + "leurs", + "ma", + "mais", + "même", + "mes", + "mon", + "ne", + "ni", + "notre", + "nous", + "ou", + "où", + "s", + "sa", + "ses", + "son", + "t", + "ta", + "tant", + "tantôt", + "tels", + "tes", + "ton", + "tôt", + "toujours", + "trop", + "un", + "une", + "votre", + "vos" + ], + "ignore_case": true }'); -SELECT zdb.define_filter('french_stemmer', '{ - "type": "stemmer", - "language": "light_french" +select zdb.define_filter('french_stemmer', '{ + "type": "stemmer", + "language": "light_french" }'); -SELECT zdb.define_filter('worddelimiter', '{ - "type": "word_delimiter" +select zdb.define_filter('worddelimiter', '{ + "type": "word_delimiter" }'); -- Tokenizer -SELECT zdb.define_tokenizer('ngram_tokenizer', '{ +select zdb.define_tokenizer('ngram_tokenizer', '{ "type": "nGram", "min_gram": 3, "max_gram": 7, - "token_chars": ["letter", "digit"] + "token_chars": [ + "letter", + "digit" + ] }'); -- Analyzer -SELECT zdb.define_analyzer('name_analyzer', '{ - "type": "custom", - "tokenizer": "ngram_tokenizer", - "filter": ["lowercase", "asciifolding"] +select zdb.define_analyzer('name_analyzer', '{ + "type": "custom", + "tokenizer": "ngram_tokenizer", + "filter": [ + "lowercase", + "asciifolding" + ] }'); -SELECT zdb.define_analyzer('fr_analyzer', '{ - "tokenizer": "standard", - "filter": ["french_elision", "worddelimiter", "asciifolding", "lowercase", "french_stop", "french_stemmer"] +select zdb.define_analyzer('fr_analyzer', '{ + "tokenizer": "standard", + "filter": [ + "french_elision", + "worddelimiter", + "asciifolding", + "lowercase", + "french_stop", + "french_stemmer" + ] }'); -- INDEX article table -SELECT zdb.define_field_mapping('article', 'title', '{ - "type": "text", - "analyzer": "fr_analyzer", - "search_analyzer": "fr_analyzer" +select zdb.define_field_mapping('article', 'title', '{ + "type": "text", + "analyzer": "fr_analyzer", + "search_analyzer": "fr_analyzer" }'); -SELECT zdb.define_field_mapping('article', 'content', '{ - "type": "text", - "analyzer": "fr_analyzer", - "search_analyzer": "fr_analyzer" +select zdb.define_field_mapping('article', 'content', '{ + "type": "text", + "analyzer": "fr_analyzer", + "search_analyzer": "fr_analyzer" }'); -SELECT zdb.define_field_mapping('article', 'description', '{ - "type": "text", - "analyzer": "fr_analyzer", - "search_analyzer": "fr_analyzer" +select zdb.define_field_mapping('article', 'description', '{ + "type": "text", + "analyzer": "fr_analyzer", + "search_analyzer": "fr_analyzer" }'); -CREATE INDEX article_idx - ON article - USING zombodb ((article.*)) - WITH (ALIAS='article_idx'); +create index article_idx + on article + using zombodb ((article.*)) + with (alias ='article_idx'); -REINDEX INDEX article_idx; +reindex index article_idx; -- INDEX constitution table -SELECT zdb.define_field_mapping('constitution', 'title', '{ - "type": "text", - "analyzer": "fr_analyzer", - "search_analyzer": "fr_analyzer" +select zdb.define_field_mapping('constitution', 'title', '{ + "type": "text", + "analyzer": "fr_analyzer", + "search_analyzer": "fr_analyzer" }'); -SELECT zdb.define_field_mapping('constitution', 'intro', '{ - "type": "text", - "analyzer": "fr_analyzer", - "search_analyzer": "fr_analyzer" +select zdb.define_field_mapping('constitution', 'intro', '{ + "type": "text", + "analyzer": "fr_analyzer", + "search_analyzer": "fr_analyzer" }'); -CREATE INDEX constitution_idx - ON constitution - USING zombodb ((constitution.*)) - WITH (ALIAS='constitution_idx'); +create index constitution_idx + on constitution + using zombodb ((constitution.*)) + with (alias ='constitution_idx'); -REINDEX INDEX constitution_idx; +reindex index constitution_idx; -- INDEX coment table -SELECT zdb.define_field_mapping('comment', 'content', '{ - "type": "text", - "analyzer": "fr_analyzer", - "search_analyzer": "fr_analyzer" +select zdb.define_field_mapping('comment', 'content', '{ + "type": "text", + "analyzer": "fr_analyzer", + "search_analyzer": "fr_analyzer" }'); -CREATE INDEX comment_idx - ON comment - USING zombodb ((comment.*)) - WITH (ALIAS='comment_idx'); +create index comment_idx + on comment + using zombodb ((comment.*)) + with (alias ='comment_idx'); -REINDEX INDEX comment_idx; +reindex index comment_idx; -- INDEX citizen table -SELECT zdb.define_field_mapping('citizen', 'first_name', '{ - "type": "text", - "analyzer": "name_analyzer", - "search_analyzer": "name_analyzer" +select zdb.define_field_mapping('citizen', 'first_name', '{ + "type": "text", + "analyzer": "name_analyzer", + "search_analyzer": "name_analyzer" }'); -SELECT zdb.define_field_mapping('citizen', 'last_name', '{ - "type": "text", - "analyzer": "name_analyzer", - "search_analyzer": "name_analyzer" +select zdb.define_field_mapping('citizen', 'last_name', '{ + "type": "text", + "analyzer": "name_analyzer", + "search_analyzer": "name_analyzer" }'); -CREATE INDEX citizen_idx - ON citizen - USING zombodb ((citizen.*)) - WITH (ALIAS='citizen_idx'); +create index citizen_idx + on citizen + using zombodb ((citizen.*)) + with (alias ='citizen_idx'); -REINDEX INDEX citizen_idx; \ No newline at end of file +reindex index citizen_idx; \ No newline at end of file diff --git a/src/test/sql/article.sql b/src/test/sql/article.sql index 412d81a..43e9653 100644 --- a/src/test/sql/article.sql +++ b/src/test/sql/article.sql @@ -6,6 +6,9 @@ declare _citizen_id uuid; created_citizen json := '{"name": {"first_name":"George", "last_name":"MICHEL"}, "birthday": "2001-01-01"}'; created_article json := '{"version_id":"933b6a1b-50c9-42b6-989f-c02a57814ef9", "title": "Love the world", "anonymous": false, "content": "bla bal bla", "tags": ["love", "test"]}'; + created_article_v2 json; + first_article_id uuid; + second_article_id uuid; selected_article json; selected_total int; begin @@ -25,9 +28,14 @@ begin select upsert_article(created_article) into created_article; assert created_article->>'version_id' is not null, 'version_id should not be null'; assert (created_article->>'version_number')::int = 1, format('version_number must be equal to 1, %s instead', created_article->>'version_number'); + assert (created_article->>'is_last_version')::bool = true, 'The first insert must be set to the last version'; + first_article_id = (created_article->>'id')::uuid; + -- try to create new version - select upsert_article(created_article) into created_article; - assert (created_article->>'version_number')::int = 2, format('version_number must be equal to 2, %s instead', created_article->>'version_number'); + select upsert_article(created_article) into created_article_v2; + assert (created_article_v2->>'version_number')::int = 2, format('version_number must be equal to 2, %s instead', created_article_v2->>'version_number'); + assert (created_article_v2->>'is_last_version')::bool = true, 'The second insert must be set to the last version'; + second_article_id = (created_article_v2->>'id')::uuid; -- get articles versions by version_id select resource, total into selected_article, selected_total from find_articles_versions_by_version_id((created_article->>'version_id')::uuid); @@ -42,13 +50,29 @@ begin assert selected_total = 2, format('the total must be 2, %s instead', selected_total); -- get article by id and check the title - select find_article_by_id((created_article->>'id')::uuid) into selected_article; + select find_article_by_id((created_article_v2->>'id')::uuid) into selected_article; assert selected_article->>'title' = 'Love the world', format('title must be "Love the world", %s', selected_article->>'title'); -- get article by version_id and check the title - select find_last_article_by_version_id((created_article->>'version_id')::uuid) into selected_article; + select find_last_article_by_version_id((created_article_v2->>'version_id')::uuid) into selected_article; assert selected_article->>'title' = 'Love the world', format('title must be "Love the world", %s', selected_article->>'title'); assert (selected_article->>'version_number')::int = 2, format('version_id must be 2, %s instead', selected_article->>'version_number'); + + -- update to draft, then the last_version column must be change + update article + set is_draft = true + where id = second_article_id; + + select find_last_article_by_version_id((created_article_v2->>'version_id')::uuid) into selected_article; + assert (selected_article->>'version_number')::int = 1, format('version_id must be 1, %s instead', selected_article->>'version_number'); + + update article + set is_draft = false + where id = second_article_id; + + select find_last_article_by_version_id((created_article_v2->>'version_id')::uuid) into selected_article; + assert (selected_article->>'version_number')::int = 2, format('version_id must be 2, %s instead', selected_article->>'version_number'); + -- -- check if user id is returned -- assert (selected_article#>>'{created_by, user, id}')::uuid = _user_id, format('user_id must be %s instead of %s', _user_id, (selected_article#>>'{created_by, user, id}')::uuid);