From bba1124ed866ee96148f8badf3f72906480599ba Mon Sep 17 00:00:00 2001 From: Arslan Giniiatullin Date: Thu, 23 Jan 2025 17:48:13 +0300 Subject: [PATCH 1/2] Added flyway versioning migrations --- Dockerfile | 20 +- build.gradle.kts | 5 +- docker-compose.yml | 2 +- gradlew | 0 src/main/resources/application.yaml | 12 +- .../db/migrations/V1__2025-23-01.init-db.sql | 334 ++++++++++++++++++ .../itmochan/ItmochanApplicationTests.kt | 13 - 7 files changed, 364 insertions(+), 22 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 src/main/resources/db/migrations/V1__2025-23-01.init-db.sql delete mode 100644 src/test/kotlin/io/github/secsdev/itmochan/ItmochanApplicationTests.kt diff --git a/Dockerfile b/Dockerfile index b9f28f8..18e3e3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,23 @@ +FROM gradle:7.4.2-jdk17 as builder + +WORKDIR /app + +COPY gradlew /app/ +COPY gradle /app/gradle +COPY build.gradle.kts /app/ +COPY settings.gradle.kts /app/ +COPY src /app/src + +RUN chmod +x gradlew + +RUN ./gradlew build + FROM openjdk:17 + WORKDIR /app -EXPOSE 8080 + COPY ./build/libs/ItmochanBackend.jar . + CMD ["java","-jar", "ItmochanBackend.jar"] + +EXPOSE 8080 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4d24bf6..840d1bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,9 +28,12 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.postgresql:postgresql") + implementation("org.postgresql:postgresql:42.7.3") testImplementation("org.springframework.boot:spring-boot-starter-test") implementation("io.minio:minio:8.5.17") + implementation("org.flywaydb:flyway-core:10.13.0") + runtimeOnly("org.flywaydb:flyway-database-postgresql:10.13.0") + //spring security implementation("io.jsonwebtoken:jjwt-api:0.12.3") implementation("io.jsonwebtoken:jjwt-impl:0.12.3") diff --git a/docker-compose.yml b/docker-compose.yml index 5a74640..32824cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - itmochan-network postgres: container_name: postgres-container-itmochan - image: postgres + image: postgres:latest environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8a611d0..2961f82 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -5,17 +5,17 @@ spring: allow-bean-definition-overriding: true datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://postgres:5432/ + url: jdbc:postgresql://postgres:5432/postgres username: postgres password: password jpa: generate-ddl: true show-sql: true - sql: - init: - schema-locations: classpath:sql/schema.sql - data-locations: classpath:sql/data.sql - mode: always + flyway: + enabled: true + locations: classpath:db/migrations + baseline-on-migrate: true + validate-on-migrate: true servlet.multipart: max-file-size: 10MB max-request-size: 10MB diff --git a/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql b/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql new file mode 100644 index 0000000..4ba0037 --- /dev/null +++ b/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql @@ -0,0 +1,334 @@ +/* tables */ + +CREATE TABLE IF NOT EXISTS "Users" ( + user_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + username varchar(255) NOT NULL UNIQUE, + isu_id integer NULL UNIQUE, + permissions integer NOT NULL DEFAULT 1, -- permissions is sum of roles id -- TODO Maybe future problem with max int postgres (31 roles) but it looks pretty nice as for me if change to bigint + password varchar(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS "Roles" ( + role_id integer PRIMARY KEY, + name varchar(255) NOT NULL UNIQUE, + description text NOT NULL +); + +CREATE TABLE IF NOT EXISTS "Comments" ( + comment_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + thread_id integer NOT NULL, + title varchar(255), + content text NOT NULL, + user_id integer NOT NULL, + reactions_id integer, + creation_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + trashed bool NOT NULL DEFAULT FALSE, + deleted bool NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS "Replies" ( + comment_id integer NOT NULL, + reply_comment_id integer NOT NULL, + PRIMARY KEY (comment_id, reply_comment_id) +); + +CREATE TABLE IF NOT EXISTS "Threads" ( + thread_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + topic_id integer NOT NULL, + init_comment_id integer NULL, + popularity INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS "Topics" ( + topic_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name varchar(255) NOT NULL, + description text +); + +CREATE TABLE IF NOT EXISTS "Trash" ( + trash_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + comment_id integer NOT NULL, + reason text, + recycle_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS "Reaction_sets" ( + r_set_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + reactions jsonb NOT NULL DEFAULT '{}'::jsonb +); + +CREATE TABLE IF NOT EXISTS "Files" ( + file_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(255) NOT NULL, + content_type varchar(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS "File_attachments" ( + comment_id integer NOT NULL, + file_id uuid NOT NULL, + PRIMARY KEY (comment_id, file_id) +); + +CREATE TABLE IF NOT EXISTS "Polls" ( + poll_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + comment_id integer NOT NULL, + title varchar(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS "Voted_users" ( + poll_id integer NOT NULL, + user_id integer NOT NULL, + PRIMARY KEY (poll_id, user_id) +); + +CREATE TABLE IF NOT EXISTS "Poll_answers" ( + poll_answer_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + poll_id integer NOT NULL, + answer_title varchar(255) NOT NULL, + votes_number integer NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS "Captcha" ( + captcha_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + answer varchar(255), + file_id uuid NOT NULL +); + +/* constraints */ + +/* Users */ + +ALTER TABLE "Users" DROP CONSTRAINT IF EXISTS "username_minimal_length"; +ALTER TABLE "Users" + ADD CONSTRAINT "username_minimal_length" + CHECK (length(username) >= 3); + +ALTER TABLE "Users" DROP CONSTRAINT IF EXISTS "isu_id_range"; +ALTER TABLE "Users" + ADD CONSTRAINT "isu_id_range" + CHECK (isu_id > 0); + +/* Comments */ +/*TODO FIX ADD FK WITH VALIDATION THAN DOESN'T EXISTS*/ +ALTER TABLE "Comments" + ADD FOREIGN KEY (reactions_id) + REFERENCES "Reaction_sets" (r_set_id) + ON DELETE SET NULL; + +ALTER TABLE "Comments" + ADD FOREIGN KEY (user_id) + REFERENCES "Users" (user_id) + ON DELETE RESTRICT; + +ALTER TABLE "Comments" + ADD FOREIGN KEY (thread_id) + REFERENCES "Threads" (thread_id) + ON DELETE RESTRICT; + +/* Replies */ + +ALTER TABLE "Replies" + ADD FOREIGN KEY (comment_id) + REFERENCES "Comments" (comment_id) + ON DELETE NO ACTION; + +ALTER TABLE "Replies" + ADD FOREIGN KEY (reply_comment_id) + REFERENCES "Comments" (comment_id) + ON DELETE NO ACTION; + +/* Threads */ + +ALTER TABLE "Threads" + ADD FOREIGN KEY (topic_id) + REFERENCES "Topics" (topic_id) + ON DELETE RESTRICT; + +ALTER TABLE "Threads" + ADD FOREIGN KEY (init_comment_id) + REFERENCES "Comments" (comment_id) + ON DELETE RESTRICT; + +/* Trash */ + +ALTER TABLE "Trash" + ADD FOREIGN KEY (comment_id) + REFERENCES "Comments" (comment_id) + ON DELETE NO ACTION; + +/* Captcha */ + +ALTER TABLE "Captcha" + ADD FOREIGN KEY (file_id) + REFERENCES "Files" (file_id) + ON DELETE CASCADE; + +/* File_attachments */ + +ALTER TABLE "File_attachments" + ADD FOREIGN KEY (file_id) + REFERENCES "Files" (file_id) + ON DELETE CASCADE; + +ALTER TABLE "File_attachments" + ADD FOREIGN KEY (comment_id) + REFERENCES "Comments" (comment_id) + ON DELETE CASCADE; + +/* Polls */ + +ALTER TABLE "Polls" + ADD FOREIGN KEY (comment_id) + REFERENCES "Comments" (comment_id) + ON DELETE CASCADE; + +/* Voted_users */ + +ALTER TABLE "Voted_users" + ADD FOREIGN KEY (poll_id) + REFERENCES "Polls" (poll_id) + ON DELETE CASCADE; + +ALTER TABLE "Voted_users" + ADD FOREIGN KEY (user_id) + REFERENCES "Users" (user_id) + ON DELETE CASCADE; +/* +ALTER TABLE "Voted_users" + ADD PRIMARY KEY (poll_id, user_id); +*/ +/* Poll_answers */ + +ALTER TABLE "Poll_answers" + ADD FOREIGN KEY (poll_id) + REFERENCES "Polls" (poll_id) + ON DELETE CASCADE; + +/* functions */ + +CREATE OR REPLACE PROCEDURE append_reaction_to_comment(reaction_text varchar, c_id bigint) +AS ' + DECLARE +r_id bigint; + r_cnt bigint; +BEGIN + IF length(reaction_text) < 7 THEN +SELECT reactions_id +INTO r_id +FROM "Comments" +WHERE "Comments".comment_id = append_reaction_to_comment.c_id; + +SELECT reactions -> reaction_text +into r_cnt +FROM "Reaction_sets" +WHERE r_set_id = r_id; + +IF r_cnt IS NULL THEN + r_cnt := 0; +END IF; + +UPDATE "Reaction_sets" +SET reactions = jsonb_set(reactions, array[reaction_text], to_jsonb(r_cnt + 1), TRUE) +WHERE r_set_id = r_id; +ELSE + RAISE EXCEPTION $$Reaction is too long$$; +END IF; +END; +' LANGUAGE PLPGSQL; + +/* EXAMPLE + call append_reaction_to_comment('tl;dr', 15) +*/ + +CREATE OR REPLACE PROCEDURE throw_in_trash(comment_id bigint, reason text) +AS ' + INSERT INTO "Trash"(comment_id, reason) VALUES(comment_id, reason); +UPDATE "Comments" +SET trashed = true +WHERE comment_id = throw_in_trash.comment_id; +' LANGUAGE SQL; + +/* EXAMPLE + CALL throw_in_trash(5, 'You made a mistake when wrote there smth'); +*/ + +CREATE OR REPLACE PROCEDURE vote_in_poll(user_id_arg bigint, poll_id_arg bigint, answer_ids_arg bigint[]) +AS ' + DECLARE +answer bigint; +BEGIN + FOREACH answer IN ARRAY answer_ids_arg + LOOP +UPDATE "Poll_answers" +SET votes_number = votes_number + 1 +WHERE poll_answer_id = answer AND poll_id = poll_id_arg; +END LOOP; +INSERT INTO "Voted_users"(poll_id, user_id) VALUES(poll_id_arg, user_id_arg); +END; +' LANGUAGE PLPGSQL; + +/* EXAMPLE + CALL vote_in_poll(5, 2, array[1,2]); +*/ + +/* triggers */ + +CREATE OR REPLACE FUNCTION create_reaction_set_for_comment() +RETURNS TRIGGER +AS +' + DECLARE +r_id integer := 0; +BEGIN + IF (NEW.reactions_id IS NULL) THEN + with tempa as ( + INSERT INTO "Reaction_sets" + DEFAULT VALUES + RETURNING r_set_id as id + ) +SELECT SUM(tempa.id) INTO r_id +FROM tempa; +UPDATE "Comments" +SET reactions_id = r_id +where comment_id = NEW.comment_id; +END IF; +RETURN NEW; +END +' LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION update_popularity_in_thread() + RETURNS TRIGGER +AS +' +DECLARE +BEGIN +UPDATE "Threads" +SET popularity = popularity + 1 +WHERE thread_id = NEW.thread_id; +RETURN NEW; +END +' LANGUAGE PLPGSQL; + +CREATE OR REPLACE TRIGGER after_inserting_comment_reaction_set + AFTER INSERT ON "Comments" + FOR EACH ROW EXECUTE PROCEDURE create_reaction_set_for_comment(); + +CREATE OR REPLACE TRIGGER after_inserting_comment_popularity + AFTER INSERT ON "Comments" + FOR EACH ROW EXECUTE PROCEDURE update_popularity_in_thread(); + +CREATE OR REPLACE FUNCTION delete_expired_trashed_comments() RETURNS TRIGGER +AS ' +BEGIN +UPDATE "Comments" +SET deleted = true +WHERE comment_id IN + (SELECT comment_id FROM "Trash" + WHERE recycle_date < NOW() - INTERVAL $$1 hour$$); +RETURN NEW; +END; +' LANGUAGE PLPGSQL; + +CREATE OR REPLACE TRIGGER trash_insert_comment_trigger + AFTER INSERT ON "Trash" + EXECUTE PROCEDURE delete_expired_trashed_comments(); \ No newline at end of file diff --git a/src/test/kotlin/io/github/secsdev/itmochan/ItmochanApplicationTests.kt b/src/test/kotlin/io/github/secsdev/itmochan/ItmochanApplicationTests.kt deleted file mode 100644 index 6a924bb..0000000 --- a/src/test/kotlin/io/github/secsdev/itmochan/ItmochanApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.secsdev.itmochan - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ItmochanApplicationTests { - - @Test - fun contextLoads() { - } - -} From 99e7587cd17f6b08d40c429f33716ecab716eeb8 Mon Sep 17 00:00:00 2001 From: Arslan Giniiatullin Date: Thu, 23 Jan 2025 18:46:14 +0300 Subject: [PATCH 2/2] Added flyway versioning migrations --- .../repository/PollAnswerRepository.kt | 13 ++++---- .../itmochan/repository/TrashRepository.kt | 16 ++++++---- .../itmochan/service/impl/PollServiceImpl.kt | 20 +++++++++--- .../itmochan/service/impl/TrashServiceImpl.kt | 11 +++++-- .../db/migrations/V1__2025-23-01.init-db.sql | 31 ------------------- 5 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/main/kotlin/io/github/secsdev/itmochan/repository/PollAnswerRepository.kt b/src/main/kotlin/io/github/secsdev/itmochan/repository/PollAnswerRepository.kt index ed18d5b..c187cc3 100644 --- a/src/main/kotlin/io/github/secsdev/itmochan/repository/PollAnswerRepository.kt +++ b/src/main/kotlin/io/github/secsdev/itmochan/repository/PollAnswerRepository.kt @@ -19,12 +19,13 @@ interface PollAnswerRepository : CrudRepository { @Modifying @Transactional - @Query("CALL vote_in_poll(:user_id, :poll_id, array[:answers])") - fun voteInPoll( - @Param("user_id") userId: Long, - @Param("poll_id") pollId: Long, - @Param("answers") answersIds: List, - ) + @Query("UPDATE PollAnswer pa SET pa.votesNumber = pa.votesNumber + 1 WHERE pa.pollAnswerId IN :answers AND pa.pollId = :pollId") + fun incrementVotes(@Param("pollId") pollId: Long, @Param("answers") answers: List) + + @Modifying + @Transactional + @Query("INSERT INTO VotedUsers (poll_id, user_id) VALUES (:pollId, :userId)") + fun insertVotedUser(@Param("pollId") pollId: Long, @Param("userId") userId: Long) fun findPollAnswersByPollId(pollId: Long) : List } diff --git a/src/main/kotlin/io/github/secsdev/itmochan/repository/TrashRepository.kt b/src/main/kotlin/io/github/secsdev/itmochan/repository/TrashRepository.kt index 3a80576..e293cf6 100644 --- a/src/main/kotlin/io/github/secsdev/itmochan/repository/TrashRepository.kt +++ b/src/main/kotlin/io/github/secsdev/itmochan/repository/TrashRepository.kt @@ -5,15 +5,19 @@ import org.springframework.data.jdbc.repository.query.Modifying import org.springframework.data.jdbc.repository.query.Query import org.springframework.data.repository.CrudRepository import org.springframework.data.repository.query.Param -import java.util.* +import org.springframework.transaction.annotation.Transactional +import java.util.Optional interface TrashRepository : CrudRepository { @Modifying - @Query("CALL throw_in_trash(:comment_id, :reason)") - fun throwInTrash( - @Param("comment_id") commentId : Long, - @Param("reason") reason : String?, - ) + @Transactional + @Query("INSERT INTO Trash (comment_id, reason) VALUES (:commentId, :reason)") + fun insertIntoTrash(@Param("commentId") commentId: Long, @Param("reason") reason: String?) + + @Modifying + @Transactional + @Query("UPDATE Comments SET trashed = true WHERE comment_id = :commentId") + fun updateCommentAsTrashed(@Param("commentId") commentId: Long) fun findTrashByCommentId(commentId: Long) : Optional } diff --git a/src/main/kotlin/io/github/secsdev/itmochan/service/impl/PollServiceImpl.kt b/src/main/kotlin/io/github/secsdev/itmochan/service/impl/PollServiceImpl.kt index 75cf28f..3ce5838 100644 --- a/src/main/kotlin/io/github/secsdev/itmochan/service/impl/PollServiceImpl.kt +++ b/src/main/kotlin/io/github/secsdev/itmochan/service/impl/PollServiceImpl.kt @@ -1,16 +1,17 @@ package io.github.secsdev.itmochan.service.impl import io.github.secsdev.itmochan.entity.PollDTO +import io.github.secsdev.itmochan.exception.EmptyAnswersListException +import io.github.secsdev.itmochan.exception.NoSuchPollException +import io.github.secsdev.itmochan.exception.UserAlreadyVotedException import io.github.secsdev.itmochan.repository.PollAnswerRepository import io.github.secsdev.itmochan.repository.PollRepository import io.github.secsdev.itmochan.repository.VotedUsersRepository import io.github.secsdev.itmochan.response.PollResponse -import io.github.secsdev.itmochan.exception.EmptyAnswersListException -import io.github.secsdev.itmochan.exception.NoSuchPollException -import io.github.secsdev.itmochan.exception.UserAlreadyVotedException import io.github.secsdev.itmochan.service.PollService import io.github.secsdev.itmochan.service.UserService import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class PollServiceImpl ( @@ -45,8 +46,7 @@ class PollServiceImpl ( val votedUser = votedUsersRepository.findVotedUsersByUserIdAndPollId(user.userId, pollId) if (votedUser.isPresent) throw UserAlreadyVotedException("You have already voted in this poll") - - pollAnswerRepository.voteInPoll(user.userId, pollId, answersIds) + performVoteOperation(pollId = pollId, answersIds = answersIds, userId = user.userId) } override fun getPoll(pollId: Long): PollResponse { @@ -65,4 +65,14 @@ class PollServiceImpl ( throw NoSuchPollException("No such poll was found") return poll.get().pollId } + + @Transactional + private fun performVoteOperation( + userId: Long, + pollId: Long, + answersIds: List, + ) { + pollAnswerRepository.incrementVotes(pollId = pollId, answers = answersIds) + pollAnswerRepository.insertVotedUser(pollId = pollId, userId = userId) + } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/secsdev/itmochan/service/impl/TrashServiceImpl.kt b/src/main/kotlin/io/github/secsdev/itmochan/service/impl/TrashServiceImpl.kt index e764295..23f262b 100644 --- a/src/main/kotlin/io/github/secsdev/itmochan/service/impl/TrashServiceImpl.kt +++ b/src/main/kotlin/io/github/secsdev/itmochan/service/impl/TrashServiceImpl.kt @@ -2,12 +2,13 @@ package io.github.secsdev.itmochan.service.impl import io.github.secsdev.itmochan.entity.Trash import io.github.secsdev.itmochan.entity.TrashDTO -import io.github.secsdev.itmochan.repository.TrashRepository import io.github.secsdev.itmochan.exception.AlreadyTrashedException import io.github.secsdev.itmochan.exception.NoSuchTrashException +import io.github.secsdev.itmochan.repository.TrashRepository import io.github.secsdev.itmochan.service.CommentService import io.github.secsdev.itmochan.service.TrashService import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class TrashServiceImpl( @@ -18,7 +19,7 @@ class TrashServiceImpl( val trashO = trashRepository.findTrashByCommentId(trash.commentId) if (trashO.isPresent) throw AlreadyTrashedException("This comment has been already trashed") - trashRepository.throwInTrash(trash.commentId, trash.reason) + performTrashOperation(trash = trash) } override fun getTrash(commentId : Long) : Trash { @@ -28,4 +29,10 @@ class TrashServiceImpl( throw NoSuchTrashException("No such trash was found") return trash.get() } + + @Transactional + private fun performTrashOperation(trash: TrashDTO) { + trashRepository.insertIntoTrash(commentId = trash.commentId, reason = trash.reason) + trashRepository.updateCommentAsTrashed(commentId = trash.commentId) + } } \ No newline at end of file diff --git a/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql b/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql index 4ba0037..8e71b4c 100644 --- a/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql +++ b/src/main/resources/db/migrations/V1__2025-23-01.init-db.sql @@ -240,37 +240,6 @@ END; call append_reaction_to_comment('tl;dr', 15) */ -CREATE OR REPLACE PROCEDURE throw_in_trash(comment_id bigint, reason text) -AS ' - INSERT INTO "Trash"(comment_id, reason) VALUES(comment_id, reason); -UPDATE "Comments" -SET trashed = true -WHERE comment_id = throw_in_trash.comment_id; -' LANGUAGE SQL; - -/* EXAMPLE - CALL throw_in_trash(5, 'You made a mistake when wrote there smth'); -*/ - -CREATE OR REPLACE PROCEDURE vote_in_poll(user_id_arg bigint, poll_id_arg bigint, answer_ids_arg bigint[]) -AS ' - DECLARE -answer bigint; -BEGIN - FOREACH answer IN ARRAY answer_ids_arg - LOOP -UPDATE "Poll_answers" -SET votes_number = votes_number + 1 -WHERE poll_answer_id = answer AND poll_id = poll_id_arg; -END LOOP; -INSERT INTO "Voted_users"(poll_id, user_id) VALUES(poll_id_arg, user_id_arg); -END; -' LANGUAGE PLPGSQL; - -/* EXAMPLE - CALL vote_in_poll(5, 2, array[1,2]); -*/ - /* triggers */ CREATE OR REPLACE FUNCTION create_reaction_set_for_comment()