All Downloads are FREE. Search and download functionalities are using the official Maven repository.

db.changeLog.xml Maven / Gradle / Ivy

The newest version!
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext
                                       http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
                                       http://www.liquibase.org/xml/ns/dbchangelog
                                       http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">

    <!-- This should be overridden in production by a system property -->
    <property name="horreum.db.secret" value="secret"/>
    <property name="quarkus.datasource.username" value="appuser" />

    <!-- Primary model -->
    <changeSet id="0" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="dbsecret">
            <column name="passphrase" type="text" />
        </createTable>
        <insert tableName="dbsecret">
            <column name="passphrase" value="${horreum.db.secret}" />
        </insert>
        <createSequence sequenceName="hibernate_sequence" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="hook">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="active" type="boolean">
                <constraints nullable="false" />
            </column>
            <column name="target" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="type" type="text">
                <constraints nullable="false" />
            </column>
            <column name="url" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addUniqueConstraint tableName="hook" columnNames="url, type, target" />
        <createSequence sequenceName="hook_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="run">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="data" type="jsonb">
                <constraints nullable="false" />
            </column>
            <column name="start" type="timestamp without time zone">
                <constraints nullable="false" />
            </column>
            <column name="stop" type="timestamp without time zone">
                <constraints nullable="false" />
            </column>
            <column name="description" type="text" />
            <column name="testid" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="owner" type="text">
                <constraints nullable="false" />
            </column>
            <column name="access" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="token" type="text" />
            <column name="trashed" type="boolean" defaultValue="false" />
        </createTable>
        <createSequence sequenceName="run_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="schema">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="uri" type="text">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="description" type="text" />
            <column name="name" type="text">
                <constraints nullable="false" unique="true" />
            </column>
            <column name="schema" type="jsonb" />
            <column name="testpath" type="text" />
            <column name="startpath" type="text" />
            <column name="stoppath" type="text" />
            <column name="descriptionpath" type="text" />
            <column name="owner" type="text">
                <constraints nullable="false" />
            </column>
            <column name="token" type="text" />
            <column name="access" type="integer">
                <constraints nullable="false" />
            </column>
        </createTable>
        <createSequence sequenceName="schema_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="schemaextractor">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="accessor" type="text">
                <constraints nullable="false" />
            </column>
            <column name="jsonpath" type="text">
                <constraints nullable="false" />
            </column>
            <column name="schema_id" type="integer">
                <constraints nullable="false" foreignKeyName="fk_extractor_schema_id" references="schema(id)"/>
            </column>
        </createTable>
        <createTable tableName="test">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="description" type="text"/>
            <column name="name" type="text">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="owner" type="text">
                <constraints nullable="false" />
            </column>
            <column name="token" type="text" />
            <column name="access" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="defaultview_id" type="integer" />
            <column name="compareurl" type="text" />
        </createTable>
        <createSequence sequenceName="test_id_seq" startValue="10" incrementBy="1" cacheSize="1" />
        <createTable tableName="view">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="test_id" type="integer">
                <constraints nullable="false" foreignKeyName="fk_view_test_id" references="test(id)"/>
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_test_view_id"
                                 baseTableName="test" baseColumnNames="defaultview_id"
                                 referencedTableName="view" referencedColumnNames="id" />
        <createTable tableName="viewcomponent">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="accessors" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="headername" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="headerorder" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="render" type="text" />
            <column name="view_id" type="integer">
                <constraints nullable="false" foreignKeyName="fk_component_view_id" references="view(id)"/>
            </column>
        </createTable>
        <addUniqueConstraint tableName="viewcomponent" columnNames="view_id, headername" />
        <sql>
            GRANT USAGE ON SCHEMA public TO "${quarkus.datasource.username}";
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE hook, run, schema, schemaextractor, test, view, viewcomponent TO "${quarkus.datasource.username}";
            GRANT ALL ON SEQUENCE hibernate_sequence, hook_id_seq, run_id_seq, schema_id_seq, test_id_seq TO "${quarkus.datasource.username}";
        </sql>
    </changeSet>

    <!-- Auto-updated read-only tables -->
    <changeSet id="1" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="run_schemas">
            <column name="runid" type="integer" />
            <column name="testid" type="integer" />
            <column name="uri" type="text" />
            <column name="schemaid" type="integer" />
            <column name="prefix" type="text" />
        </createTable>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_run_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE runid = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_run_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE runid = OLD.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_func() RETURNS TRIGGER AS $$
            DECLARE
                v_schema text;
                v_schemaid integer;
            BEGIN
                FOR v_schema IN (SELECT jsonb_path_query(NEW.data, '$.\$schema'::jsonpath)#>>'{}') LOOP
                    v_schemaid := (SELECT id FROM schema WHERE uri = v_schema);
                    IF v_schemaid IS NOT NULL THEN
                        INSERT INTO run_schemas (runid, testid, prefix, uri, schemaid)
                        VALUES (NEW.id, NEW.testid, '$', v_schema, v_schemaid);
                    END IF;
                END LOOP;
                FOR v_schema IN (SELECT jsonb_path_query(NEW.data, '$.*.\$schema'::jsonpath)#>>'{}') LOOP
                    v_schemaid := (SELECT id FROM schema WHERE uri = v_schema);
                    IF v_schemaid IS NOT NULL THEN
                        INSERT INTO run_schemas (runid, testid, prefix, uri, schemaid)
                        VALUES (NEW.id, NEW.testid, '$.*', v_schema, v_schemaid);
                    END IF;
                END LOOP;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_schema_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE schemaid = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE schemaid = OLD.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH rs AS (
                    SELECT id, owner, access, token, testid, '$' as prefix,
                    jsonb_path_query(RUN.data, '$.\$schema'::jsonpath)#>>'{}' as uri
                    FROM run
                    UNION
                    SELECT id, owner, access, token, testid, '$.*' as prefix,
                    jsonb_path_query(RUN.data, '$.*.\$schema'::jsonpath)#>>'{}' as uri
                    FROM run
                )
                INSERT INTO run_schemas
                SELECT rs.id as runid, rs.testid, rs.uri, NEW.id as schemaid, prefix
                FROM rs WHERE rs.uri = NEW.uri;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createTable tableName="view_data">
            <column name="vcid" type="integer" />
            <column name="runid" type="integer" />
            <column name="extractor_ids" type="int[]" />
            <column name="object" type="jsonb" />
        </createTable>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_before_delete_run_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM view_data WHERE runid = OLD.runid;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_after_insert_run_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH vcs AS (
                    SELECT id, unnest(regexp_split_to_array(accessors, ';')) as aa
                    FROM viewcomponent
                )
                INSERT INTO view_data
                SELECT vcs.id as vcid, rs.runid, array_agg(se.id) as extractor_ids,
                    jsonb_object_agg(se.accessor, (CASE
                        WHEN aa like '%[]' THEN jsonb_path_query_array(run.data, (rs.prefix || se.jsonpath)::jsonpath)
                        ELSE jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath)
                    END)) as object
                FROM vcs
                JOIN schemaextractor se ON se.accessor = replace(aa, '[]', '')
                JOIN run_schemas rs ON rs.schemaid = se.schema_id
                JOIN run on run.id = rs.runid
                WHERE run.id = NEW.runid
                GROUP BY runid, vcid;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_before_delete_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM view_data WHERE OLD.id = ANY(extractor_ids);
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_before_update_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM view_data WHERE OLD.id = ANY(extractor_ids);
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_after_update_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH vcs AS (
                    SELECT id, unnest(regexp_split_to_array(accessors, ';')) as aa
                    FROM viewcomponent
                )
                INSERT INTO view_data
                SELECT vcs.id as vcid, rs.runid, array_agg(se.id) as extractor_ids,
                    jsonb_object_agg(se.accessor, (CASE
                        WHEN aa like '%[]' THEN jsonb_path_query_array(run.data, (rs.prefix || se.jsonpath)::jsonpath)
                        ELSE jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath)
                    END)) as object
                FROM vcs
                JOIN schemaextractor se ON se.accessor = replace(aa, '[]', '')
                JOIN run_schemas rs ON rs.schemaid = se.schema_id
                JOIN run on run.id = rs.runid
                WHERE se.id = NEW.id
                GROUP BY runid, vcid;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_before_delete_vc_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM view_data WHERE vcid = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_before_update_vc_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM view_data WHERE vcid = OLD.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION vd_after_update_vc_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH vcs AS (
                    SELECT id, unnest(regexp_split_to_array(accessors, ';')) as aa
                    FROM viewcomponent
                    WHERE id = NEW.id
                )
                INSERT INTO view_data
                SELECT vcs.id as vcid, rs.runid, array_agg(se.id) as extractor_ids,
                    jsonb_object_agg(se.accessor, (CASE
                        WHEN aa like '%[]' THEN jsonb_path_query_array(run.data, (rs.prefix || se.jsonpath)::jsonpath)
                        ELSE jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath)
                    END)) as object
                FROM vcs
                JOIN schemaextractor se ON se.accessor = replace(aa, '[]', '')
                JOIN run_schemas rs ON rs.schemaid = se.schema_id
                JOIN run on run.id = rs.runid
                GROUP BY runid, vcid;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <!-- Triggers run with privileges of the user running the update = appuser -->
        <sql>
            GRANT select, insert, delete, update ON TABLE run_schemas, view_data TO "${quarkus.datasource.username}";
        </sql>
        <sql>
            CREATE TRIGGER before_run_delete BEFORE DELETE ON run FOR EACH ROW EXECUTE FUNCTION before_run_delete_func();
            CREATE TRIGGER before_run_update BEFORE UPDATE ON run FOR EACH ROW EXECUTE FUNCTION before_run_update_func();
            CREATE TRIGGER after_run_update AFTER INSERT OR UPDATE ON run FOR EACH ROW EXECUTE FUNCTION after_run_update_func();
            CREATE TRIGGER before_schema_delete BEFORE DELETE ON schema FOR EACH ROW EXECUTE FUNCTION before_schema_delete_func();
            CREATE TRIGGER before_schema_update BEFORE UPDATE OF uri ON schema FOR EACH ROW EXECUTE FUNCTION before_schema_update_func();
            CREATE TRIGGER after_schema_update AFTER INSERT OR UPDATE OF uri ON schema FOR EACH ROW EXECUTE FUNCTION after_schema_update_func();

            CREATE TRIGGER vd_before_delete BEFORE DELETE ON run_schemas FOR EACH ROW EXECUTE FUNCTION vd_before_delete_run_func();
            CREATE TRIGGER vd_after_insert AFTER INSERT ON run_schemas FOR EACH ROW EXECUTE FUNCTION vd_after_insert_run_func();
            CREATE TRIGGER vd_before_delete BEFORE DELETE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION vd_before_delete_extractor_func();
            CREATE TRIGGER vd_before_update BEFORE UPDATE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION vd_before_update_extractor_func();
            CREATE TRIGGER vd_after_update AFTER INSERT OR UPDATE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION vd_after_update_extractor_func();
            CREATE TRIGGER vd_before_delete BEFORE DELETE ON viewcomponent FOR EACH ROW EXECUTE FUNCTION vd_before_delete_vc_func();
            CREATE TRIGGER vd_before_update BEFORE UPDATE OF id, accessors ON viewcomponent FOR EACH ROW EXECUTE FUNCTION vd_before_update_vc_func();
            CREATE TRIGGER vd_after_update AFTER INSERT OR UPDATE OF id, accessors ON viewcomponent FOR EACH ROW EXECUTE FUNCTION vd_after_update_vc_func();
        </sql>
        <rollback>
            DROP TRIGGER IF EXISTS before_run_delete ON run;
            DROP TRIGGER IF EXISTS before_run_update ON run;
            DROP TRIGGER IF EXISTS after_run_update ON run;
            DROP TRIGGER IF EXISTS before_schema_delete ON run;
            DROP TRIGGER IF EXISTS before_schema_update ON run;
            DROP TRIGGER IF EXISTS after_schema_update ON run;

            DROP TRIGGER IF EXISTS vd_before_delete ON run_schemas;
            DROP TRIGGER IF EXISTS vd_after_insert ON run_schemas;
            DROP TRIGGER IF EXISTS vd_before_delete ON schemaextractor;
            DROP TRIGGER IF EXISTS vd_before_update ON schemaextractor;
            DROP TRIGGER IF EXISTS vd_after_update ON schemaextractor;
            DROP TRIGGER IF EXISTS vd_before_delete ON viewcomponent;
            DROP TRIGGER IF EXISTS vd_before_update ON viewcomponent;
            DROP TRIGGER IF EXISTS vd_after_update ON viewcomponent;
        </rollback>
    </changeSet>

    <!-- Security constraints -->
    <changeSet id="2" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            <!-- Install pgcrypto plugin -->
            CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
        </sql>
        <createProcedure>
            <!-- Verify that what user has in horreum.userroles is correctly signed -->
            CREATE OR REPLACE FUNCTION has_role(owner TEXT) RETURNS boolean AS $$
            DECLARE
                v_passphrase TEXT;
                v_userroles TEXT;
                v_role_salt_sign TEXT;
                v_parts TEXT[];
                v_role TEXT;
                v_salt TEXT;
                v_signature TEXT;
                v_computed TEXT;
            BEGIN
                SELECT passphrase INTO v_passphrase FROM dbsecret;
                v_userroles := current_setting('horreum.userroles', true);

                IF v_userroles = '' OR v_userroles IS NULL THEN
                     RETURN 0;
                END IF;

                FOREACH v_role_salt_sign IN ARRAY regexp_split_to_array(v_userroles, ',')
                LOOP
                    v_parts := regexp_split_to_array(v_role_salt_sign, ':');
                    v_role := v_parts[1];
                    IF v_role = owner THEN
                        v_salt := v_parts[2];
                        v_signature := v_parts[3];
                        v_computed := encode(digest(v_role || v_salt || v_passphrase, 'sha256'), 'base64');
                        IF v_computed = v_signature THEN
                            RETURN 1;
                        ELSE
                            RAISE EXCEPTION 'invalid role + salt + signature';
                        END IF;
                    END IF;
                END LOOP;
                RETURN 0;
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION can_view(access INTEGER, owner TEXT, token TEXT) RETURNS boolean AS $$
            BEGIN
                RETURN (
                    access = 0
                    OR (access = 1 AND has_role('viewer'))
                    OR (access = 2 AND has_role(owner) AND has_role('viewer'))
                    OR token = current_setting('horreum.token', true)
                );
            END;
            $$ LANGUAGE plpgsql STABLE;
        </createProcedure>
        <sqlFile path="policies.sql" relativeToChangelogFile="true" />
    </changeSet>

    <changeSet id="3" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="variable">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="name" type="text">
                <constraints nullable="false" />
            </column>
            <column name="testid" type="integer">
                <!-- No foregin key constraint -->
                <constraints nullable="false" />
            </column>
            <column name="accessors" type="text">
                <constraints nullable="false" />
            </column>
            <column name="calculation" type="text"/>
            <column name="maxwindow" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="deviationfactor" type="double precision">
                <constraints nullable="false"/>
            </column>
            <column name="confidence" type="double precision">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <createTable tableName="datapoint">
            <column name="id" type="integer" autoIncrement="true">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="runid" type="integer">
                <!-- No foreign key constraint -->
                <constraints nullable="false" />
            </column>
            <column name="timestamp" type="timestamp">
                <constraints nullable="false" />
            </column>
            <column name="variable_id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="value" type="double precision">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_datapoint_variable_id"
                                 baseTableName="datapoint" baseColumnNames="variable_id"
                                 referencedTableName="variable" referencedColumnNames="id" />
        <createTable tableName="change">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="confirmed" type="boolean">
                <constraints nullable="false"/>
            </column>
            <column name="description" type="text"/>
            <column name="variable_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="runid" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="timestamp" type="timestamp">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_change_variable_id"
                                 baseTableName="change" baseColumnNames="variable_id"
                                 referencedTableName="variable" referencedColumnNames="id" />
        <createTable tableName="watch">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="testid" type="integer">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addUniqueConstraint tableName="watch" columnNames="testid"/>
        <createTable tableName="watch_users">
            <column name="watch_id" type="integer"/>
            <column name="users" type="text" />
        </createTable>
        <addForeignKeyConstraint constraintName="fk_watch_users"
                                 baseTableName="watch_users" baseColumnNames="watch_id"
                                 referencedTableName="watch" referencedColumnNames="id" />
        <createTable tableName="watch_teams">
            <column name="watch_id" type="integer"/>
            <column name="teams" type="text" />
        </createTable>
        <addForeignKeyConstraint constraintName="fk_watch_teams"
                                 baseTableName="watch_teams" baseColumnNames="watch_id"
                                 referencedTableName="watch" referencedColumnNames="id" />
        <createTable tableName="notificationsettings">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="disabled" type="boolean" defaultValue="false">
                <constraints nullable="false"/>
            </column>
            <column name="isteam" type="boolean">
                <constraints nullable="false"/>
            </column>
            <column name="method" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="data" type="text" />
        </createTable>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE change, datapoint, notificationsettings, variable, watch, watch_teams, watch_users TO "${quarkus.datasource.username}";
            <!-- In case of ALL policy the USING is applied on the inserted data as well -->
            ALTER TABLE change ENABLE ROW LEVEL SECURITY;
            ALTER TABLE datapoint ENABLE ROW LEVEL SECURITY;
            ALTER TABLE notificationsettings ENABLE ROW LEVEL SECURITY;
            ALTER TABLE variable ENABLE ROW LEVEL SECURITY;
            ALTER TABLE watch ENABLE ROW LEVEL SECURITY;
            ALTER TABLE watch_teams ENABLE ROW LEVEL SECURITY;
            ALTER TABLE watch_users ENABLE ROW LEVEL SECURITY;
            CREATE POLICY change_select ON change FOR SELECT
                USING (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND can_view(run.access, run.owner, run.token)
                ));
            CREATE POLICY change_insert ON change FOR INSERT
                WITH CHECK (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                ));
            CREATE POLICY change_update ON change FOR UPDATE
                USING (exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                )) WITH CHECK (has_role('tester') AND exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                ));
            CREATE POLICY change_delete ON change FOR DELETE
                USING (has_role('tester') AND exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                ));
            CREATE POLICY datapoint_select ON datapoint FOR SELECT
                USING (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND can_view(run.access, run.owner, run.token)
                ));
            CREATE POLICY datapoint_insert ON datapoint FOR INSERT WITH CHECK (has_role('horreum.alerting'));
            CREATE POLICY datapoint_update ON datapoint FOR UPDATE USING (has_role('horreum.alerting'));
            CREATE POLICY datapoint_delete ON datapoint FOR DELETE
                USING (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                ));
            CREATE POLICY notificationsettings_policies ON notificationsettings FOR ALL
                USING (has_role('horreum.alerting') OR has_role(name));
            CREATE POLICY variable_select ON variable FOR SELECT
                USING (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND can_view(test.access, test.owner, test.token)
                ));
            CREATE POLICY variable_insert ON variable FOR INSERT
                WITH CHECK (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND has_role(test.owner)
                ));
            CREATE POLICY variable_update ON variable FOR UPDATE
                USING (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND has_role(test.owner)
                ));
            CREATE POLICY variable_delete ON variable FOR DELETE
                USING (has_role('horreum.alerting') OR exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND has_role(test.owner)
                ));
            CREATE POLICY watch_select ON watch FOR SELECT USING (true);
            CREATE POLICY watch_insert ON watch FOR INSERT WITH CHECK (true);
            CREATE POLICY watch_update ON watch FOR UPDATE USING (has_role('horreum.alerting'));
            CREATE POLICY watch_delete ON watch FOR DELETE USING (has_role('horreum.alerting'));
            CREATE POLICY watch_teams_policies ON watch_teams FOR ALL USING (has_role('horreum.alerting') OR has_role(teams));
            CREATE POLICY watch_users_policies ON watch_users FOR ALL USING (has_role('horreum.alerting') OR has_role(users));
        </sql>
    </changeSet>

    <changeSet id="4" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="grafana_dashboard">
            <column name="uid" type="text">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="testid" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="url" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <createTable tableName="grafana_dashboard_variable">
            <column name="grafana_dashboard_uid" type="text">
                <constraints nullable="false" />
            </column>
            <column name="variables_id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="variables_order" type="integer">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addPrimaryKey tableName="grafana_dashboard_variable" columnNames="grafana_dashboard_uid, variables_order" />
        <addForeignKeyConstraint baseTableName="grafana_dashboard_variable" baseColumnNames="grafana_dashboard_uid"
                                 constraintName="fk_grafana_dashboard_uid"
                                 referencedTableName="grafana_dashboard" referencedColumnNames="uid" />
        <addForeignKeyConstraint baseTableName="grafana_dashboard_variable" baseColumnNames="variables_id"
                                 constraintName="fk_grafana_dashboard_variables_id"
                                 referencedTableName="variable" referencedColumnNames="id" />
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE grafana_dashboard, grafana_dashboard_variable TO "${quarkus.datasource.username}";
            ALTER TABLE grafana_dashboard ENABLE ROW LEVEL SECURITY;
            ALTER TABLE grafana_dashboard_variable ENABLE ROW LEVEL SECURITY;
            <!-- Anyone who could view the test/dashboard can also create it -->
            CREATE POLICY grafana_dashboard_policies ON grafana_dashboard FOR ALL
                USING (exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND can_view(test.access, test.owner, test.token)
               ));
            CREATE POLICY grafana_dashboard_variable_policies ON grafana_dashboard_variable FOR ALL
                USING (exists(
                    SELECT 1 FROM test
                    JOIN grafana_dashboard gd ON gd.uid = grafana_dashboard_uid
                    WHERE test.id = gd.testid AND can_view(test.access, test.owner, test.token)
                ));
        </sql>
    </changeSet>
    
    <changeSet id="5" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="userinfo">
            <column name="username" type="text">
                <constraints primaryKey="true" nullable="false" />
            </column>
        </createTable>
        <createTable tableName="userinfo_teams">
            <column name="username" type="text">
                <constraints nullable="false" />
            </column>
            <column name="team" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_userinfo_username"
                                 baseTableName="userinfo_teams" baseColumnNames="username"
                                 referencedTableName="userinfo" referencedColumnNames="username" />
        <addUniqueConstraint tableName="userinfo_teams" columnNames="username,team" />
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE userinfo, userinfo_teams TO "${quarkus.datasource.username}";
            ALTER TABLE userinfo ENABLE ROW LEVEL SECURITY;
            ALTER TABLE userinfo_teams ENABLE ROW LEVEL SECURITY;
            CREATE POLICY userinfo_policies ON userinfo FOR ALL
                USING (has_role('horreum.alerting'));
            CREATE POLICY userinfo_teams_policies ON userinfo_teams FOR ALL
                USING (has_role('horreum.alerting'));
        </sql>
    </changeSet>

    <changeSet id="6" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="variable">
            <column name="group" type="text" />
        </addColumn>
        <addColumn tableName="variable">
            <column name="order" type="integer" defaultValue="0">
                <constraints nullable="false"/>
            </column>
        </addColumn>
    </changeSet>

    <changeSet id="7" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropTable tableName="grafana_dashboard_variable" />
        <createTable tableName="grafana_panel">
            <column name="id" type="integer">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="order" type="integer" />
            <column name="grafana_dashboard_uid" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_grafana_panel_dashboard_uid"
                                 baseTableName="grafana_panel" baseColumnNames="grafana_dashboard_uid"
                                 referencedTableName="grafana_dashboard" referencedColumnNames="uid" />
        <createTable tableName="grafana_panel_variable">
            <column name="grafana_panel_id" type="integer" />
            <column name="variables_id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="variables_order" type="integer">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addPrimaryKey tableName="grafana_panel_variable" columnNames="grafana_panel_id, variables_order" />
        <addForeignKeyConstraint baseTableName="grafana_panel_variable" baseColumnNames="grafana_panel_id"
                                 constraintName="fk_grafana_panel_id"
                                 referencedTableName="grafana_panel" referencedColumnNames="id" />
        <addForeignKeyConstraint baseTableName="grafana_panel_variable" baseColumnNames="variables_id"
                                 constraintName="fk_grafana_panel_variables_id"
                                 referencedTableName="variable" referencedColumnNames="id" />
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE grafana_panel, grafana_panel_variable TO "${quarkus.datasource.username}";
            ALTER TABLE grafana_panel ENABLE ROW LEVEL SECURITY;
            ALTER TABLE grafana_panel_variable ENABLE ROW LEVEL SECURITY;
            CREATE POLICY grafana_panel_policies ON grafana_panel FOR ALL
                USING (exists(
                    SELECT 1 FROM test
                    JOIN grafana_dashboard gd ON gd.testid = test.id
                    WHERE gd.uid = grafana_dashboard_uid AND can_view(test.access, test.owner, test.token)
                ));
            CREATE POLICY grafana_panel_variable_policies ON grafana_panel_variable FOR ALL
                USING (exists(
                    SELECT 1 FROM test
                    JOIN grafana_dashboard gd ON gd.testid = test.id
                    JOIN grafana_panel gp ON gp.grafana_dashboard_uid = gd.uid
                    WHERE gp.id = grafana_panel_id AND can_view(test.access, test.owner, test.token)
                ));
        </sql>
    </changeSet>

    <changeSet id="8" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            GRANT ALL ON SEQUENCE datapoint_id_seq TO "${quarkus.datasource.username}";
        </sql>
    </changeSet>

    <changeSet id="9" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION to_double(v_input text)
            RETURNS double precision AS $$
            BEGIN
                BEGIN
                    RETURN v_input::double precision;
                EXCEPTION WHEN OTHERS THEN
                    RETURN NULL;
                END;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="10" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="variable">
            <column name="minwindow" type="integer" defaultValue="0"></column>
        </addColumn>
    </changeSet>

    <changeSet id="11" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropColumn tableName="variable">
            <column name="maxwindow" />
            <column name="confidence" />
            <column name="deviationfactor" />
        </dropColumn>
        <addColumn tableName="variable">
            <column name="maxdifferencelastdatapoint" type="double precision" defaultValue="0.05">
                <constraints nullable="false"/>
            </column>
            <column name="maxdifferencefloatingwindow" type="double precision" defaultValue="0.05">
                <constraints nullable="false"/>
            </column>
            <column name="floatingwindow" type="integer" defaultValue="7">
                <constraints nullable="false"/>
            </column>
        </addColumn>
    </changeSet>

    <changeSet id="12" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="test">
            <column name="tags" type="text" />
        </addColumn>
        <createTable tableName="run_tags">
            <column name="runid" type="integer">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="tags" type="jsonb" />
            <column name="extractor_ids" type="jsonb" />
        </createTable>
        <addForeignKeyConstraint constraintName="fk_run_tags_runid"
                                 baseTableName="run_tags" baseColumnNames="runid"
                                 referencedTableName="run" referencedColumnNames="id" />
        <sql>
            GRANT select, insert, delete ON TABLE run_tags TO "${quarkus.datasource.username}";
        </sql>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_delete_test_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE runid IN (SELECT id FROM run WHERE testid = OLD.id);
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_update_test_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE runid IN (SELECT id FROM run WHERE testid = OLD.id);
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_delete_run_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE runid = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <!-- We piggy-back on run_schemas -->
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_insert_run_schemas_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), tags AS (
                    SELECT rs.runid, se.id as extractor_id, se.accessor, jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath) AS value
                        FROM schemaextractor se
                        JOIN test_tags ON se.accessor = test_tags.accessor
                        JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                        JOIN run ON run.id = rs.runid
                        WHERE rs.runid = NEW.runid
                )
                INSERT INTO run_tags
                    SELECT tags.runid, jsonb_object_agg(tags.accessor, tags.value) AS tags, jsonb_agg(tags.extractor_id) AS extractor_ids FROM tags GROUP BY runid;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_insert_test_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), tags AS (
                    SELECT rs.runid, se.id AS extractor_id, se.accessor, jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath) AS value
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    JOIN run ON run.id = rs.runid
                    WHERE rs.testid = NEW.id
                )
                INSERT INTO run_tags
                    SELECT tags.runid, jsonb_object_agg(tags.accessor, tags.value) AS tags, jsonb_agg(tags.extractor_id) AS extractor_ids FROM tags GROUP BY runid;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_delete_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE OLD.id = ANY(extractor_ids);
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_update_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE OLD.id = ANY(extractor_ids);
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_update_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), matching_runs AS (
                    SELECT runid
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    WHERE se.id = NEW.id
                ), tags AS (
                    SELECT rs.runid, se.id AS extractor_id, se.accessor, jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath) AS value
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    JOIN run ON run.id = rs.runid
                    WHERE run.id = ANY(SELECT runid FROM matching_runs)
                )
                INSERT INTO run_tags
                    SELECT tags.runid, jsonb_object_agg(tags.accessor, tags.value) AS tags, jsonb_agg(tags.extractor_id) AS extractor_ids FROM tags GROUP BY runid;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER rt_before_delete BEFORE DELETE ON test FOR EACH ROW EXECUTE FUNCTION rt_before_delete_test_func();
            CREATE TRIGGER rt_before_update BEFORE UPDATE ON test FOR EACH ROW EXECUTE FUNCTION rt_before_update_test_func();
            CREATE TRIGGER rt_after_insert AFTER INSERT OR UPDATE ON test FOR EACH ROW EXECUTE FUNCTION rt_after_insert_test_func();
            CREATE TRIGGER rt_before_delete BEFORE DELETE ON run FOR EACH ROW EXECUTE FUNCTION rt_before_delete_run_func();
            CREATE TRIGGER rt_after_insert AFTER INSERT ON run_schemas FOR EACH ROW EXECUTE FUNCTION rt_after_insert_run_schemas_func();
            CREATE TRIGGER rt_before_delete BEFORE DELETE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION rt_before_delete_extractor_func();
            CREATE TRIGGER rt_before_update BEFORE UPDATE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION rt_before_delete_extractor_func();
            CREATE TRIGGER rt_after_update AFTER INSERT OR UPDATE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION rt_after_update_extractor_func();
        </sql>
        <rollback>
            DROP TRIGGER IF EXISTS rt_before_delete ON test;
            DROP TRIGGER IF EXISTS rt_before_update ON test;
            DROP TRIGGER IF EXISTS rt_after_insert ON test;
            DROP TRIGGER IF EXISTS rt_before_delete ON run;
            DROP TRIGGER IF EXISTS rt_after_insert ON run_schemas;
            DROP TRIGGER IF EXISTS rt_before_delete ON schemaextractor;
            DROP TRIGGER IF EXISTS rt_before_update ON schemaextractor;
            DROP TRIGGER IF EXISTS rt_after_update ON schemaextractor;
        </rollback>
    </changeSet>

    <changeSet id="13" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="grafana_dashboard">
            <column name="tags" type="text" />
        </addColumn>
    </changeSet>

    <changeSet id="14" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropColumn tableName="run_tags" columnName="extractor_ids" />
        <addColumn tableName="run_tags">
            <column name="extractor_ids" type="int[]" />
        </addColumn>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_insert_run_schemas_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), tags AS (
                    SELECT rs.runid, se.id as extractor_id, se.accessor, jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath) AS value
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    JOIN run ON run.id = rs.runid
                    WHERE rs.runid = NEW.runid
                )
                INSERT INTO run_tags
                    SELECT tags.runid, jsonb_object_agg(tags.accessor, tags.value) AS tags, array_agg(tags.extractor_id) AS extractor_ids FROM tags GROUP BY runid;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_insert_test_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), tags AS (
                    SELECT rs.runid, se.id AS extractor_id, se.accessor, jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath) AS value
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    JOIN run ON run.id = rs.runid
                    WHERE rs.testid = NEW.id
                )
                INSERT INTO run_tags
                    SELECT tags.runid, jsonb_object_agg(tags.accessor, tags.value) AS tags, array_agg(tags.extractor_id) AS extractor_ids FROM tags GROUP BY runid;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_update_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), matching_runs AS (
                    SELECT runid
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    WHERE se.id = NEW.id
                ), tags AS (
                    SELECT rs.runid, se.id AS extractor_id, se.accessor, jsonb_path_query_first(run.data, (rs.prefix || se.jsonpath)::jsonpath) AS value
                    FROM schemaextractor se
                    JOIN test_tags ON se.accessor = test_tags.accessor
                    JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                    JOIN run ON run.id = rs.runid
                    WHERE run.id = ANY(SELECT runid FROM matching_runs)
                )
                INSERT INTO run_tags
                    SELECT tags.runid, jsonb_object_agg(tags.accessor, tags.value) AS tags, array_agg(tags.extractor_id) AS extractor_ids FROM tags GROUP BY runid;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="15" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER rt_before_update ON schemaextractor;
            CREATE TRIGGER rt_before_update BEFORE UPDATE ON schemaextractor FOR EACH ROW EXECUTE FUNCTION rt_before_update_extractor_func();
        </sql>
    </changeSet>

    <changeSet id="16" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_insert_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE runid IN (SELECT runid FROM run_schemas WHERE run_schemas.schemaid = NEW.schema_id);
            RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER rt_before_insert BEFORE INSERT ON schemaextractor FOR EACH ROW EXECUTE FUNCTION rt_before_insert_extractor_func();
        </sql>
    </changeSet>

    <changeSet id="17" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_before_delete_run_schemas_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_tags WHERE run_tags.runid = OLD.runid;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER rt_before_delete BEFORE DELETE ON run_schemas FOR EACH ROW EXECUTE FUNCTION rt_before_delete_run_schemas_func();
        </sql>
    </changeSet>

    <changeSet id="18" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="grafana_panel">
            <column name="name" type="text" />
        </addColumn>
    </changeSet>

    <changeSet id="19" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="test_stalenesssettings">
            <column name="test_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="maxstaleness" type="bigint">
                <constraints nullable="false"/>
            </column>
            <column name="tags" type="jsonb" />
        </createTable>
        <addForeignKeyConstraint constraintName="fk_ss_test_id"
             baseTableName="test_stalenesssettings" baseColumnNames="test_id"
             referencedTableName="test" referencedColumnNames="id" />
        <createTable tableName="lastmissingrunnotification">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="lastnotification" type="timestamp without time zone"/>
            <column name="tags" type="jsonb"/>
            <column name="testid" type="integer">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <sql>
            GRANT select, insert, delete ON TABLE test_stalenesssettings TO "${quarkus.datasource.username}";
            GRANT select, insert, delete ON TABLE lastmissingrunnotification TO "${quarkus.datasource.username}";
            ALTER TABLE test_stalenesssettings ENABLE ROW LEVEL SECURITY;
            CREATE POLICY ss_select ON test_stalenesssettings FOR SELECT
                USING (exists(
                    SELECT 1 FROM test
                    WHERE test.id = test_id AND can_view(test.access, test.owner, test.token)
            ));
            CREATE POLICY ss_insert ON test_stalenesssettings FOR INSERT
                WITH CHECK (exists(
                    SELECT 1 FROM test
                    WHERE test.id = test_id AND has_role(test.owner)
            ));
            CREATE POLICY ss_update ON test_stalenesssettings FOR UPDATE
                USING (exists(
                    SELECT 1 FROM test
                    WHERE test.id = test_id AND has_role(test.owner)
                )) WITH CHECK (has_role('tester') AND exists(
                    SELECT 1 FROM test
                    WHERE test.id = test_id AND has_role(test.owner)
                ));
            CREATE POLICY ss_delete ON test_stalenesssettings FOR DELETE
                USING (has_role('tester') AND exists(
                    SELECT 1 FROM test
                    WHERE test.id = test_id AND has_role(test.owner)
            ));
            ALTER TABLE lastmissingrunnotification ENABLE ROW LEVEL SECURITY;
            CREATE POLICY lmrn_all ON lastmissingrunnotification FOR ALL
                USING (has_role('horreum.alerting'));
        </sql>
    </changeSet>
    <changeSet id="20" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="calculationlog">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="level" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="timestamp" type="timestamp without time zone">
                <constraints nullable="false" />
            </column>
            <column name="testid" type="integer" />
            <column name="runid" type="integer" />
            <column name="message" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <sql>
            GRANT select, insert, delete ON TABLE calculationlog TO "${quarkus.datasource.username}";
            ALTER TABLE calculationlog ENABLE ROW LEVEL SECURITY;
            CREATE POLICY cl_all ON calculationlog FOR ALL
                USING (has_role('horreum.alerting') OR (exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND has_role(test.owner)
                ) AND exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                )));
        </sql>
    </changeSet>
    <changeSet id="21" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            GRANT update ON TABLE test_stalenesssettings TO "${quarkus.datasource.username}";
            GRANT update ON TABLE lastmissingrunnotification TO "${quarkus.datasource.username}";
            GRANT update ON TABLE calculationlog TO "${quarkus.datasource.username}";
        </sql>
    </changeSet>
    <changeSet id="22" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropTable tableName="grafana_panel_variable"/>
        <dropTable tableName="grafana_panel"/>
        <dropTable tableName="grafana_dashboard"/>
    </changeSet>

    <changeSet id="23" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="test">
            <column name="notificationsenabled" type="boolean" defaultValue="true">
                <constraints nullable="false" />
            </column>
        </addColumn>
        <createProcedure>
            CREATE OR REPLACE FUNCTION can_view(access INTEGER, owner TEXT, token TEXT) RETURNS boolean AS $$
            BEGIN
                RETURN (
                    access = 0
                    OR (access = 1 AND has_role('viewer'))
                    OR (access = 2 AND has_role(owner) AND has_role('viewer'))
                    OR token = current_setting('horreum.token', true)
                    OR has_role('horreum.alerting')
                );
            END;
            $$ LANGUAGE plpgsql STABLE;
        </createProcedure>
    </changeSet>

    <changeSet id="24" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="allowedhookprefix">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="prefix" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <!-- Only admin and test owners can both view and modify webhooks. -->
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE allowedhookprefix TO "${quarkus.datasource.username}";
            ALTER TABLE allowedhookprefix ENABLE ROW LEVEL SECURITY;
            CREATE POLICY prefix_select ON allowedhookprefix FOR SELECT USING(true);
            CREATE POLICY prefix_insert ON allowedhookprefix FOR INSERT WITH CHECK (has_role('admin'));
            CREATE POLICY prefix_update ON allowedhookprefix FOR UPDATE USING (has_role('admin'));
            CREATE POLICY prefix_delete ON allowedhookprefix FOR DELETE USING (has_role('admin'));

            ALTER TABLE hook ENABLE ROW LEVEL SECURITY;
            CREATE POLICY hook_policies ON hook
                USING (has_role('admin') OR exists(
                    SELECT 1 FROM test WHERE (type = 'change/new' OR type = 'run/new') AND target = test.id AND can_view(test.access, test.owner, test.token)
                ));
            CREATE POLICY hook_write_check ON hook
                WITH CHECK (exists(SELECT 1 FROM allowedhookprefix ahp WHERE left(url, length(ahp.prefix)) = ahp.prefix));
        </sql>
    </changeSet>

    <changeSet id="25" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="test_token">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="test_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="value" type="text">
                <constraints nullable="false" />
            </column>
            <column name="permissions" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="description" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_token_test_id"
                                 baseTableName="test_token" baseColumnNames="test_id"
                                 referencedTableName="test" referencedColumnNames="id" />
        <sql>
            DROP POLICY view_select ON view;
            DROP POLICY vc_select ON viewcomponent;
            DROP POLICY variable_select ON variable;
            DROP POLICY ss_select ON test_stalenesssettings;
            DROP POLICY hook_policies ON hook;
            DROP POLICY test_select ON TEST;
        </sql>
        <dropColumn tableName="test" columnName="token" />
        <createProcedure>
            CREATE OR REPLACE FUNCTION has_role2(owner TEXT, type TEXT) RETURNS boolean AS $$
            BEGIN
                RETURN (right(owner, 4) = 'team' AND has_role(left(owner, -4) || type));
            END;
            $$ LANGUAGE plpgsql STABLE;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION can_view2(access INTEGER, owner TEXT) RETURNS boolean AS $$
            BEGIN
                RETURN (
                    access = 0
                    OR (access = 1 AND has_role('viewer'))
                    OR (access = 2 AND has_role(owner) AND has_role('viewer'))
                );
            END;
            $$ LANGUAGE plpgsql STABLE;
        </createProcedure>
        <createProcedure>
            -- this function is a security definer and as such avoids regular policies on test_token
            CREATE OR REPLACE FUNCTION has_read_token(testid INTEGER) RETURNS boolean AS $$
            BEGIN
                RETURN (exists(
                    SELECT 1 FROM test_token
                    WHERE test_id = testid AND (permissions &amp; 1) != 0 AND value = current_setting('horreum.token', true)
                ));
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <createProcedure>
            -- this function is a security definer and as such avoids regular policies on test_token
            CREATE OR REPLACE FUNCTION has_modify_token(testid INTEGER) RETURNS boolean AS $$
            BEGIN
                RETURN (exists(
                    SELECT 1 FROM test_token
                    WHERE test_id = testid AND (permissions &amp; 2) != 0 AND value = current_setting('horreum.token', true)
                ));
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <createProcedure>
            -- this function is a security definer and as such avoids regular policies on test_token
            CREATE OR REPLACE FUNCTION has_upload_token(testid INTEGER) RETURNS boolean AS $$
            BEGIN
                RETURN (exists(
                    SELECT 1 FROM test_token
                    WHERE test_id = testid AND (permissions &amp; 4) != 0 AND value = current_setting('horreum.token', true)
                ));
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE test_token TO "${quarkus.datasource.username}";
            ALTER TABLE test_token ENABLE ROW LEVEL SECURITY;
            CREATE POLICY token_policy ON test_token
                USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));
        </sql>
        <sql>
            -- run_select unchanged
            DROP POLICY run_insert ON run;
            CREATE POLICY run_insert ON run FOR INSERT
                WITH CHECK (has_role2(owner, 'uploader') OR has_upload_token(testid));
            DROP POLICY run_update ON run;
            CREATE POLICY run_update ON run FOR UPDATE
                USING (has_role2(owner, 'tester'));
            DROP POLICY run_delete ON run;
            -- Testers update runs' trashed flag but never really delete these
            CREATE POLICY run_delete ON run FOR DELETE
                USING (has_role('horreum.system'));
        </sql>
        <sql>
            CREATE POLICY test_select ON test FOR SELECT
                USING (can_view2(access, owner) OR has_read_token(id));
            DROP POLICY test_insert ON test;
            CREATE POLICY test_insert ON test FOR INSERT
                WITH CHECK (has_role2(owner, 'tester'));
            DROP POLICY test_update ON test;
            CREATE POLICY test_update ON test FOR UPDATE
                USING (has_role2(owner, 'tester') OR has_modify_token(id));
            DROP POLICY test_delete ON test;
            CREATE POLICY test_delete ON test FOR DELETE
                USING (has_role2(owner, 'tester') OR has_modify_token(id));
        </sql>
        <sql>
            -- schema_select unchanged
            DROP POLICY schema_insert ON schema;
            CREATE POLICY schema_insert ON schema FOR INSERT
                WITH CHECK (has_role2(owner, 'tester'));
            DROP POLICY schema_update ON schema;
            CREATE POLICY schema_update ON schema FOR UPDATE
                USING (has_role2(owner, 'tester'));
            DROP POLICY schema_delete ON schema;
            CREATE POLICY schema_delete ON schema FOR DELETE
                USING (has_role2(owner, 'tester'));
        </sql>
        <sql>
            DROP POLICY hook_policy ON hook;
            CREATE POLICY hook_policy ON hook
                USING (has_role('admin') OR (
                    (type = 'change/new' OR type = 'run/new')
                    AND (
                        has_role2((SELECT owner FROM test WHERE test.id = target), 'tester')
                        OR has_modify_token(target)
                    )
                ));
            DROP POLICY hook_write_check ON hook;
            CREATE POLICY hook_write_check ON hook AS RESTRICTIVE
                WITH CHECK (exists(SELECT 1 FROM allowedhookprefix ahp WHERE left(url, length(ahp.prefix)) = ahp.prefix));
        </sql>
        <sql>
            -- se_select unchanged
            DROP POLICY se_insert ON schemaextractor;
            CREATE POLICY se_insert ON schemaextractor FOR INSERT
                WITH CHECK (has_role2((SELECT owner FROM schema WHERE schema.id = schema_id), 'tester'));
            DROP POLICY se_update ON schemaextractor;
            CREATE POLICY se_update ON schemaextractor FOR UPDATE
                USING (has_role2((SELECT owner FROM schema WHERE schema.id = schema_id), 'tester'));
            DROP POLICY se_delete ON schemaextractor;
            CREATE POLICY se_delete ON schemaextractor FOR DELETE
                USING (has_role2((SELECT owner FROM schema WHERE schema.id = schema_id), 'tester'));
        </sql>
        <sql>
            CREATE POLICY view_select ON view FOR SELECT
                USING (exists(
                    SELECT 1 FROM test WHERE test.id = test_id AND can_view2(test.access, test.owner)
                ) OR has_read_token(test_id));
            DROP POLICY view_insert ON view;
            CREATE POLICY view_insert ON view FOR INSERT
                WITH CHECK (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester') OR has_modify_token(test_id));
            DROP POLICY view_update ON view;
            CREATE POLICY view_update ON view FOR UPDATE
                USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester') OR has_modify_token(test_id));
            DROP POLICY view_delete ON view;
            CREATE POLICY view_delete ON view FOR DELETE
                USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester') OR has_modify_token(test_id));
        </sql>
        <sql>
            CREATE POLICY vc_select ON viewcomponent FOR SELECT
                USING (exists(
                    SELECT 1 FROM test
                    JOIN view ON view.test_id = test.id
                    WHERE view.id = view_id AND (can_view2(test.access, test.owner) OR has_read_token(test.id))
                ));
            DROP POLICY vc_insert ON viewcomponent;
            CREATE POLICY vc_insert ON viewcomponent FOR INSERT
                WITH CHECK (exists(
                    SELECT 1 FROM test
                    JOIN view ON view.test_id = test.id
                    WHERE view.id = view_id AND (has_role2(test.owner, 'tester') OR has_modify_token(test.id))
                ));
            DROP POLICY vc_update ON viewcomponent;
            CREATE POLICY vc_update ON viewcomponent FOR UPDATE
                USING (exists(
                    SELECT 1 FROM test
                    JOIN view ON view.test_id = test.id
                    WHERE view.id = view_id AND (has_role2(test.owner, 'tester') OR has_modify_token(test.id))
                ));
            DROP POLICY vc_delete ON viewcomponent;
            CREATE POLICY vc_delete ON viewcomponent FOR DELETE
                USING (exists(
                    SELECT 1 FROM test
                    JOIN view ON view.test_id = test.id
                    WHERE view.id = view_id AND (has_role2(test.owner, 'tester') OR has_modify_token(test.id))
                ));
        </sql>
        <sql>
            -- rs_select unchanged
            DROP POLICY rs_insert ON run_schemas;
            CREATE POLICY rs_insert ON run_schemas FOR INSERT
                WITH CHECK (
                    has_role2((SELECT owner FROM run WHERE run.id = runid), 'uploader')
                    OR has_upload_token(testid)
                    OR has_role2((SELECT owner FROM schema WHERE schema.id = schemaid), 'tester')
                );
            -- This policy is here to prevent rogue records. However since the schema owner has access
            -- it can claim that certain run is using his schema even if it is not.
            CREATE POLICY rs_insert_validate ON run_schemas AS RESTRICTIVE FOR INSERT
                WITH CHECK (exists(SELECT 1 FROM run WHERE run.id = runid AND run.testid = testid));
            DROP POLICY rs_update ON run_schemas;
            -- run_schemas are never updated, just dropped and inserted
            DROP POLICY rs_delete ON run_schemas;
            -- both run update and schema update/delete should drop the row; privilege to any of those is sufficient to remove the record
            -- though deleting a schema with matching rows isn
            -- horreum.system can delete the run
            CREATE POLICY rs_delete ON run_schemas FOR DELETE
                USING (
                    has_role('horreum.system')
                    OR has_role2((SELECT owner FROM run WHERE run.id = runid), 'tester')
                    OR has_role2((SELECT owner FROM schema WHERE schema.id = schemaid), 'tester')
                );
        </sql>
        <sql>
            -- vd_select unchanged
            DROP POLICY vd_insert ON view_data;
            CREATE POLICY vd_insert ON view_data FOR INSERT
                WITH CHECK (exists(
                    SELECT 1 FROM run
                    JOIN test ON test.id = run.testid
                    WHERE run.id = runid AND (has_role2(run.owner, 'uploader') OR has_role2(test.owner, 'tester') OR has_upload_token(test.id))
                ) OR exists (
                    SELECT 1 FROM schema
                    JOIN schemaextractor se ON se.schema_id = schema.id
                    WHERE se.id = ANY(extractor_ids) AND has_role2(schema.owner, 'tester')
                ));
            DROP POLICY vd_update ON view_data;
            -- view_data is never updated
            DROP POLICY vd_delete ON view_data;
            CREATE POLICY vd_delete ON view_data FOR DELETE
                USING (has_role('horreum.system') OR exists(
                    SELECT 1 FROM run
                    JOIN test ON test.id = run.testid
                    WHERE run.id = runid AND (has_role2(run.owner, 'tester') OR has_role2(test.owner, 'tester'))
                ) OR exists(
                    SELECT 1 FROM schema
                    JOIN schemaextractor se ON se.schema_id = schema.id
                    WHERE se.id = ANY(extractor_ids) AND has_role2(schema.owner, 'tester')
                ));
        </sql>
        <sql>
            -- change_select unchanged
            DROP POLICY change_insert ON change;
            -- only alerting can create changes
            CREATE POLICY change_insert ON change FOR INSERT
                WITH CHECK (has_role('horreum.alerting'));
            DROP POLICY change_update ON change;
            CREATE POLICY change_update ON change FOR UPDATE
                USING (has_role2((SELECT owner FROM run WHERE run.id = runid), 'tester'));
            DROP POLICY change_delete ON change;
            CREATE POLICY change_delete ON change FOR DELETE
                USING (has_role2((SELECT owner FROM run WHERE run.id = runid), 'tester'));
        </sql>
        <sql>
            -- The user deletes datapoints when changing test variables
            DROP POLICY datapoint_delete ON datapoint;
            CREATE POLICY datapoint_delete ON datapoint FOR DELETE
                USING (has_role('horreum.alerting') OR has_role2((SELECT owner FROM run WHERE run.id = runid), 'tester'));
        </sql>
        <sql>
            CREATE POLICY variable_access ON variable USING (has_role('horreum.alerting'));
            CREATE POLICY variable_select ON variable FOR SELECT
                USING (
                    exists(SELECT 1 FROM test WHERE test.id = testid AND can_view2(test.access, test.owner))
                    OR has_read_token(testid)
                );
            DROP POLICY variable_insert ON variable;
            CREATE POLICY variable_insert ON variable FOR INSERT
                WITH CHECK (
                    has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester')
                    OR has_modify_token(testid)
                );
            DROP POLICY variable_update ON variable;
            CREATE POLICY variable_update ON variable FOR UPDATE
                USING (
                    has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester')
                    OR has_modify_token(testid)
                );
            DROP POLICY variable_delete ON variable;
            CREATE POLICY variable_delete ON variable FOR DELETE
                USING (
                    has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester')
                    OR has_modify_token(testid)
                );
        </sql>
        <sql>
            CREATE POLICY ss_select ON test_stalenesssettings FOR SELECT
                USING (
                    exists(SELECT 1 FROM test WHERE test.id = test_id AND can_view2(test.access, test.owner))
                    OR has_read_token(test_id)
                );
            DROP POLICY ss_insert ON test_stalenesssettings;
            CREATE POLICY ss_insert ON test_stalenesssettings FOR INSERT
                WITH CHECK (
                    has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester')
                    OR has_modify_token(test_id)
                );
            DROP POLICY ss_update ON test_stalenesssettings;
            CREATE POLICY ss_update ON test_stalenesssettings FOR UPDATE
                USING (
                    has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester')
                    OR has_modify_token(test_id)
                );
            DROP POLICY ss_delete ON test_stalenesssettings;
            CREATE POLICY ss_delete ON test_stalenesssettings FOR DELETE
                USING (
                    has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester')
                    OR has_modify_token(test_id)
                );
        </sql>
        <sql>
            DROP POLICY cl_all ON calculationlog;
            CREATE POLICY cl_all_alerting ON calculationlog USING (has_role('horreum.alerting'));
            CREATE POLICY cl_all ON calculationlog FOR ALL
                USING ((
                    (has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester')
                    OR has_modify_token(testid))
                ) AND has_role2((SELECT owner FROM run WHERE run.id = runid), 'tester'));
        </sql>
    </changeSet>

    <changeSet id="26" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropColumn tableName="schema" columnName="testpath" />
        <dropColumn tableName="schema" columnName="startpath" />
        <dropColumn tableName="schema" columnName="stoppath" />
        <dropColumn tableName="schema" columnName="descriptionpath" />
    </changeSet>

    <changeSet id="27" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="watch_optout">
            <column name="watch_id" type="integer"/>
            <column name="optout" type="text" />
        </createTable>
        <addForeignKeyConstraint constraintName="fk_watch_optout"
                                 baseTableName="watch_optout" baseColumnNames="watch_id"
                                 referencedTableName="watch" referencedColumnNames="id" />
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE watch_optout TO "${quarkus.datasource.username}";
            ALTER TABLE watch_optout ENABLE ROW LEVEL SECURITY;
            CREATE POLICY watch_optout_policies ON watch_optout FOR ALL USING (has_role('horreum.alerting') OR has_role(optout));
        </sql>
    </changeSet>

    <changeSet id="28" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            CREATE POLICY watch_teams_owner ON watch_teams FOR ALL USING (has_role2((SELECT test.owner FROM test JOIN watch ON test.id = watch.testid WHERE watch.id = watch_id), 'tester'));
            CREATE POLICY watch_users_owner ON watch_users FOR ALL USING (has_role2((SELECT test.owner FROM test JOIN watch ON test.id = watch.testid WHERE watch.id = watch_id), 'tester'));
            CREATE POLICY watch_optout_owner ON watch_optout FOR ALL USING (has_role2((SELECT test.owner FROM test JOIN watch ON test.id = watch.testid WHERE watch.id = watch_id), 'tester'));
        </sql>
    </changeSet>

    <changeSet id="29" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH pairs AS (
                    SELECT NEW.data->>'$schema' as uri, '$' as prefix
                UNION
                    SELECT kv.value->>'$schema' as uri, '$."' || replace(kv.key, '"', '\"') || '"' as prefix
                    FROM jsonb_each(NEW.data) AS kv WHERE jsonb_typeof(NEW.data) = 'object'
                UNION
                    SELECT value->>'$schema' as uri, '$[' || (row_number() over () - 1) || ']' as prefix
                    FROM jsonb_array_elements(NEW.data) AS value WHERE jsonb_typeof(NEW.data) = 'array'
                )
                INSERT INTO run_schemas
                    SELECT NEW.id as runid, NEW.testid, schema.uri, schema.id as schemaid, prefix
                    FROM pairs JOIN schema ON pairs.uri = schema.uri;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH rs AS (
                    SELECT id, testid, '$' as prefix, run.data->>'$schema' as uri
                    FROM run WHERE run.data->>'$schema' = NEW.uri
                UNION
                    SELECT id, testid, '$."' || replace(kv.key, '"', '\"') || '"' as prefix, kv.value->>'$schema' as uri
                    FROM run, jsonb_each(run.data) AS kv WHERE jsonb_typeof(data) = 'object' AND kv.value->>'$schema' = NEW.uri
                UNION
                    SELECT id, testid, '$[' || (row_number() over () - 1) || ']' as prefix, value->>'$schema' as uri
                    FROM run, jsonb_array_elements(data) AS value WHERE jsonb_typeof(data) = 'array' AND value->>'$schema' = NEW.uri
                )
                INSERT INTO run_schemas
                    SELECT rs.id as runid, rs.testid, rs.uri, NEW.id as schemaid, prefix FROM rs;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>UPDATE run SET id = id;</sql>
    </changeSet>

    <changeSet id="30" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createIndex tableName="view_data" indexName="view_data_runid">
            <column name="runid" />
        </createIndex>
    </changeSet>

    <changeSet id="31" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="userinfo">
            <column name="defaultteam" type="text" />
        </addColumn>
        <sql>
            DROP POLICY userinfo_policies ON userinfo;
            DROP POLICY userinfo_teams_policies ON userinfo_teams;

            CREATE POLICY userinfo_rw ON userinfo FOR ALL
                USING (has_role(username));
            CREATE POLICY userinfo_teams_rw ON userinfo_teams FOR ALL
                USING (has_role(username));
            CREATE POLICY userinfo_read ON userinfo_teams FOR SELECT
                USING (has_role('horreum.alerting'));
            CREATE POLICY userinfo_teams_read ON userinfo_teams FOR SELECT
                USING (has_role('horreum.alerting'));
        </sql>
    </changeSet>

    <changeSet id="32" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="test">
            <column name="tagscalculation" type="text"/>
        </addColumn>
        <createProcedure>
            CREATE OR REPLACE FUNCTION auth_suffix(prefix TEXT) RETURNS text AS $$
            BEGIN
                RETURN concat(prefix, ';', current_setting('horreum.token', true), ';', current_setting('horreum.userroles', true));
            END;
            $$ LANGUAGE plpgsql STABLE;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_insert_run_schemas_func() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM pg_notify('calculate_tags', auth_suffix(NEW.runid::text) );
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_insert_test_func() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM pg_notify('calculate_tags', auth_suffix(run.id::text)) FROM run where run.testid = NEW.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rt_after_update_extractor_func() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM * FROM (WITH test_tags AS (
                    SELECT id AS testid, unnest(regexp_split_to_array(tags, ';')) AS accessor FROM test
                ), matching_runs AS (
                    SELECT runid
                        FROM schemaextractor se
                        JOIN test_tags ON se.accessor = test_tags.accessor
                        JOIN run_schemas rs ON rs.testid = test_tags.testid AND rs.schemaid = se.schema_id
                        WHERE se.id = NEW.id
                )
                SELECT pg_notify('calculate_tags', auth_suffix(runid::text)) FROM matching_runs) AS notify;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="33" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="tablereportconfig">
            <column name="id" type="integer">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="title" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="testid" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="filteraccessors" type="text" />
            <column name="filterfunction" type="text" />
            <column name="categoryaccessors" type="text" />
            <column name="categoryfunction" type="text" />
            <column name="categoryformatter" type="text" />
            <column name="seriesaccessors" type="text">
                <constraints nullable="false" />
            </column>
            <column name="seriesfunction" type="text" />
            <column name="seriesformatter" type="text" />
            <column name="labelaccessors" type="text" />
            <column name="labelfunction" type="text" />
            <column name="labelformatter" type="text" />
        </createTable>
        <createTable tableName="reportcomponent">
            <column name="id" type="integer">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="reportconfig_id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="name" type="text">
                <constraints nullable="false" />
            </column>
            <column name="component_order" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="accessors" type="text">
                <constraints nullable="false" />
            </column>
            <column name="function" type="text" />
        </createTable>
        <addForeignKeyConstraint constraintName="reportcomponent_report_fk"
                                 baseTableName="reportcomponent" baseColumnNames="reportconfig_id"
                                 referencedTableName="tablereportconfig" referencedColumnNames="id" />
        <createTable tableName="tablereport">
            <column name="id" type="integer">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="config_id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="created" type="timestamp">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="tablereport_config_id"
                                 baseTableName="tablereport" baseColumnNames="config_id"
                                 referencedTableName="tablereportconfig" referencedColumnNames="id" />
        <createTable tableName="tablereport_rundata">
            <column name="report_id" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="runid" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="category" type="text">
                <constraints nullable="false" />
            </column>
            <column name="series" type="text">
                <constraints nullable="false" />
            </column>
            <column name="label" type="text">
                <constraints nullable="false" />
            </column>
            <!-- Liquibase would mess up 'double precision[]' -->
            <column name="values" type="float8[]">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="tablereport_rundata_report_id"
                                 baseTableName="tablereport_rundata" baseColumnNames="report_id"
                                 referencedTableName="tablereport" referencedColumnNames="id" />
        <addUniqueConstraint tableName="tablereport_rundata" columnNames="report_id, runid" />
        <sql>
            GRANT select, insert, delete, update ON TABLE tablereportconfig, reportcomponent, tablereport, tablereport_rundata TO "${quarkus.datasource.username}";
            CREATE POLICY tablereportconfig_select ON tablereportconfig FOR SELECT
                USING (exists(SELECT 1 FROM test WHERE test.id = testid AND can_view2(test.access, test.owner)));
            CREATE POLICY tablereportconfig_insert ON tablereportconfig FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM test WHERE test.id = testid), 'tester'));
            CREATE POLICY tablereportconfig_update ON tablereportconfig FOR UPDATE
                USING (has_role2((SELECT test.owner FROM test WHERE test.id = testid), 'tester'));
            CREATE POLICY tablereportconfig_delete ON tablereportconfig FOR DELETE
                USING (has_role2((SELECT test.owner FROM test WHERE test.id = testid), 'tester'));
            CREATE POLICY reportcomponent_select ON reportcomponent FOR SELECT
                USING (exists(SELECT 1 FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = reportconfig_id AND can_view2(test.access, test.owner)));
            CREATE POLICY reportcomponent_insert ON reportcomponent FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = reportconfig_id), 'tester'));
            CREATE POLICY reportcomponent_update ON reportcomponent FOR UPDATE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = reportconfig_id), 'tester'));
            CREATE POLICY reportcomponent_delete ON reportcomponent FOR DELETE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = reportconfig_id), 'tester'));
            CREATE POLICY tablereport_select ON tablereport FOR SELECT
                USING (exists(SELECT 1 FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = config_id AND can_view2(test.access, test.owner)));
            CREATE POLICY tablereport_insert ON tablereport FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = config_id), 'tester'));
            CREATE POLICY tablereport_update ON tablereport FOR UPDATE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = config_id), 'tester'));
            CREATE POLICY tablereport_delete ON tablereport FOR DELETE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid WHERE trc.id = config_id), 'tester'));
            CREATE POLICY tablereport_rundata_select ON tablereport_rundata FOR SELECT
                USING (exists(SELECT run.owner FROM run WHERE run.id = runid AND can_view2(run.access, run.owner)));
            CREATE POLICY tablereport_rundata_insert ON tablereport_rundata FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON tr.config_id = trc.id WHERE tr.id = report_id), 'tester'));
            CREATE POLICY tablereport_rundata_update ON tablereport_rundata FOR UPDATE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON tr.config_id = trc.id WHERE tr.id = report_id), 'tester'));
            CREATE POLICY tablereport_rundata_delete ON tablereport_rundata FOR DELETE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON tr.config_id = trc.id WHERE tr.id = report_id), 'tester'));
        </sql>
    </changeSet>
    
    <changeSet id="34" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="reportcomment">
            <column name="id" type="integer">
                <constraints primaryKey="true" />
            </column>
            <column name="report_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="level" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="category" type="text" />
            <column name="component_id" type="integer" />
            <column name="comment" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <sql>
            GRANT select, insert, delete, update ON TABLE reportcomment TO "${quarkus.datasource.username}";
            CREATE POLICY reportcomment_select ON reportcomment FOR SELECT
                USING (exists(SELECT 1 FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON trc.id = tr.config_id WHERE tr.id = report_id AND can_view2(test.access, test.owner)));
            CREATE POLICY reportcomment_insert ON reportcomment FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON trc.id = tr.config_id WHERE tr.id = report_id), 'tester'));
            CREATE POLICY reportcomment_update ON reportcomment FOR UPDATE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON trc.id = tr.config_id WHERE tr.id = report_id), 'tester'));
            CREATE POLICY reportcomment_delete ON reportcomment FOR DELETE
                USING (has_role2((SELECT test.owner FROM test JOIN tablereportconfig trc ON test.id = trc.testid JOIN tablereport tr ON trc.id = tr.config_id WHERE tr.id = report_id), 'tester'));
        </sql>
        <addForeignKeyConstraint constraintName="fk_tablereport_comment_report"
                                 baseTableName="reportcomment" baseColumnNames="report_id"
                                 referencedTableName="tablereport" referencedColumnNames="id" />
        <createIndex tableName="reportcomment" indexName="report_ids">
            <column name="report_id" />
        </createIndex>
    </changeSet>

    <changeSet id="35" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropNotNullConstraint tableName="tablereportconfig" columnName="testid"/>
        <sql>
            CREATE POLICY tablereportconfig_update_system ON tablereportconfig FOR UPDATE
                USING (has_role('horreum.system'));
        </sql>
    </changeSet>

    <changeSet id="36" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="run_expectation">
            <column name="id" type="bigint">
                <constraints primaryKey="true" />
            </column>
            <column name="testid" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="tags" type="jsonb" />
            <column name="expectedbefore" type="timestamp">
                <constraints nullable="false"/>
            </column>
            <column name="expectedby" type="text"/>
            <column name="backlink" type="text"/>
        </createTable>
        <sql>
            GRANT select, insert, delete ON TABLE run_expectation TO "${quarkus.datasource.username}";
            CREATE POLICY run_expectation_select ON run_expectation FOR SELECT
                USING (has_role('horreum.alerting'));
            CREATE POLICY run_expectation_insert ON run_expectation FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM test WHERE test.id = testid), 'uploader'));
            CREATE POLICY run_expectation_delete ON run_expectation FOR DELETE
                USING (has_role('horreum.alerting'));
        </sql>
    </changeSet>

    <!-- Even if the run does not have any schema we should attempt to calculate its tags (and find these to be null) -->
    <changeSet id="37" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_func() RETURNS TRIGGER AS $$
            DECLARE
                v_schema text;
                v_schemaid integer;
                v_has_schema boolean := false;
            BEGIN
                FOR v_schema IN (SELECT jsonb_path_query(NEW.data, '$.\$schema'::jsonpath)#>>'{}') LOOP
                    v_schemaid := (SELECT id FROM schema WHERE uri = v_schema);
                    IF v_schemaid IS NOT NULL THEN
                        INSERT INTO run_schemas (runid, testid, prefix, uri, schemaid)
                            VALUES (NEW.id, NEW.testid, '$', v_schema, v_schemaid);
                        v_has_schema := true;
                    END IF;
                END LOOP;
                FOR v_schema IN (SELECT jsonb_path_query(NEW.data, '$.*.\$schema'::jsonpath)#>>'{}') LOOP
                    v_schemaid := (SELECT id FROM schema WHERE uri = v_schema);
                    IF v_schemaid IS NOT NULL THEN
                        INSERT INTO run_schemas (runid, testid, prefix, uri, schemaid)
                            VALUES (NEW.id, NEW.testid, '$.*', v_schema, v_schemaid);
                        v_has_schema := true;
                    END IF;
                END LOOP;
                IF NOT v_has_schema THEN
                    PERFORM pg_notify('calculate_tags', auth_suffix(NEW.id::text) );
                END IF;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="38" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="tablereport_rundata">
            <column name="data" type="jsonb" defaultValue="{}">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <sql>
            UPDATE tablereport_rundata SET data = array_to_json(values);
            ALTER TABLE tablereport_rundata ALTER COLUMN data DROP DEFAULT;
        </sql>
        <dropColumn tableName="tablereport_rundata" columnName="values"/>
        <renameColumn tableName="tablereport_rundata" oldColumnName="data" newColumnName="values"/>
    </changeSet>

    <changeSet id="39" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="banner">
            <column name="id" type="integer">
                <constraints primaryKey="true" />
            </column>
            <column name="created" type="timestamp">
                <constraints nullable="false" />
            </column>
            <column name="active" type="boolean">
                <constraints nullable="false" />
            </column>
            <column name="severity" type="text">
                <constraints nullable="false" />
            </column>
            <column name="title" type="text">
                <constraints nullable="false" />
            </column>
            <column name="message" type="text" />
        </createTable>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE banner TO "${quarkus.datasource.username}";
            ALTER TABLE banner ENABLE ROW LEVEL SECURITY;
            CREATE POLICY banner_select ON banner FOR SELECT USING(true);
            CREATE POLICY banner_insert ON banner FOR INSERT WITH CHECK (has_role('admin'));
            CREATE POLICY banner_update ON banner FOR UPDATE USING (has_role('admin'));
            CREATE POLICY banner_delete ON banner FOR DELETE USING (has_role('admin'));
        </sql>
    </changeSet>

    <changeSet id="40" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="tablereportconfig">
            <column name="labeldescription" type="text" />
        </addColumn>
        <addColumn tableName="reportcomponent">
            <column name="unit" type="text" />
        </addColumn>
    </changeSet>

    <changeSet id="41" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="calculationlog">
            <column name="source" type="text" defaultValue="variables">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <sql>
            ALTER TABLE calculationlog ALTER COLUMN source DROP DEFAULT;
            ALTER POLICY run_select ON run
                USING (can_view(access, owner, token) OR has_role('horreum.system'));
            ALTER POLICY cl_all ON calculationlog
                USING (has_role('horreum.alerting') OR has_role('horreum.system') OR (exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND has_role(test.owner)
                ) AND exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                )));
        </sql>
    </changeSet>

    <changeSet id="42" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="schemaextractor" >
            <column name="deprecatedby_id" type="integer" />
            <column name="deleted" type="boolean" defaultValue="false">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <addUniqueConstraint tableName="schemaextractor" columnNames="deprecatedby_id"/>
        <addUniqueConstraint tableName="schemaextractor" columnNames="accessor,schema_id" />
        <addForeignKeyConstraint constraintName="singledeprecation"
                                 baseTableName="schemaextractor" baseColumnNames="deprecatedby_id"
                                 referencedTableName="schemaextractor" referencedColumnNames="id" />
        <dropDefaultValue tableName="schemaextractor" columnName="deleted"/>
    </changeSet>

    <changeSet id="43" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="test">
            <column name="folder" type="text" />
        </addColumn>
    </changeSet>

    <changeSet id="44" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <comment>Duplicate the config for each report</comment>
        <sql>
            CREATE TEMPORARY TABLE tempconfig AS SELECT tr.id AS reportid, c.* FROM tablereportconfig c JOIN tablereport tr ON tr.config_id = c.id;
            CREATE TEMPORARY TABLE tempcomponent AS SELECT tr.id AS reportid, rc.* FROM reportcomponent rc JOIN tablereport tr ON rc.reportconfig_id = tr.config_id;
            UPDATE tempconfig SET id = nextval('hibernate_sequence');
            UPDATE tempcomponent tc SET id = nextval('hibernate_sequence'), reportconfig_id = (SELECT id FROM tempconfig WHERE tempconfig.reportid = tc.reportid);
            INSERT INTO tablereportconfig SELECT id, title, testid, filteraccessors, filterfunction, categoryaccessors, categoryfunction, categoryformatter, seriesaccessors, seriesfunction, seriesformatter, labelaccessors, labelfunction, labelformatter, labeldescription FROM tempconfig;
            INSERT INTO reportcomponent SELECT id, reportconfig_id, name, component_order, accessors, function, unit FROM tempcomponent;
            UPDATE tablereport tr SET config_id = (SELECT id FROM tempconfig WHERE reportid = tr.id);
            DELETE FROM reportcomponent WHERE id NOT IN (SELECT id from tempcomponent);
            DELETE FROM tablereportconfig WHERE id NOT IN (SELECT id from tempconfig);
            DROP TABLE tempconfig;
            DROP TABLE tempcomponent;
        </sql>
    </changeSet>

    <changeSet id="45" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <comment>Remove leaked watches</comment>
        <sql>
            DELETE FROM watch_users WHERE watch_id IN (SELECT id FROM watch WHERE testid NOT IN (SELECT id FROM test));
            DELETE FROM watch_teams WHERE watch_id IN (SELECT id FROM watch WHERE testid NOT IN (SELECT id FROM test));
            DELETE FROM watch_optout WHERE watch_id IN (SELECT id FROM watch WHERE testid NOT IN (SELECT id FROM test));
            DELETE FROM watch WHERE testid NOT IN (SELECT id FROM test);
        </sql>
    </changeSet>

    <changeSet id="46" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DELETE FROM calculationlog WHERE testid NOT IN (SELECT id FROM test) OR runid NOT IN (SELECT id FROM run);
        </sql>
    </changeSet>

    <changeSet id="47" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="regressiondetection">
            <column name="id" type="integer">
                <constraints primaryKey="true"/>
            </column>
            <column name="variable_id" type="integer">
                <constraints nullable="false" foreignKeyName="fk_regression_variable_id" referencedTableName="variable" referencedColumnNames="id"/>
            </column>
            <column name="model" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="config" type="jsonb">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <sql>
            INSERT INTO regressiondetection (SELECT nextval('hibernate_sequence') AS id, id AS variable_id,
                'relativeDifference' AS model, jsonb_build_object(
                    'threshold', maxdifferencelastdatapoint,
                    'minPrevious', minwindow,
                    'window', 1,
                    'filter', 'mean'
                ) as config FROM variable);
            INSERT INTO regressiondetection (SELECT nextval('hibernate_sequence') AS id, id AS variable_id,
                'relativeDifference' AS model, jsonb_build_object(
                    'threshold', maxdifferencefloatingwindow,
                    'minPrevious', floatingwindow,
                    'window', floatingwindow,
                    'filter', 'mean'
                ) AS config FROM variable);
        </sql>
        <dropColumn tableName="variable" columnName="minwindow"/>
        <dropColumn tableName="variable" columnName="maxdifferencelastdatapoint"/>
        <dropColumn tableName="variable" columnName="floatingwindow"/>
        <dropColumn tableName="variable" columnName="maxdifferencefloatingwindow"/>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE regressiondetection TO "${quarkus.datasource.username}";
            ALTER TABLE regressiondetection ENABLE ROW LEVEL SECURITY;
            CREATE POLICY rd_access ON regressiondetection USING (has_role('horreum.alerting'));
            CREATE POLICY rd_select ON regressiondetection FOR SELECT
                USING (
                    exists(SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id
                        AND (can_view2(test.access, test.owner) OR has_read_token(testid)))
                );
            CREATE POLICY rd_insert ON regressiondetection FOR INSERT
                WITH CHECK (
                    exists (SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id
                        AND (has_role2(test.owner, 'tester') OR has_modify_token(variable.testid)))
                );
            CREATE POLICY rd_update ON regressiondetection FOR UPDATE
                USING (
                    exists (SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id
                        AND (has_role2(test.owner, 'tester') OR has_modify_token(variable.testid)))
                );
            CREATE POLICY rd_delete ON regressiondetection FOR DELETE
                USING (
                    exists (SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id
                        AND (has_role2(test.owner, 'tester') OR has_modify_token(variable.testid)))
                );
        </sql>
    </changeSet>

    <changeSet id="48" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER rt_before_update ON test;
            DROP TRIGGER rt_after_insert ON test;
            CREATE TRIGGER rt_before_update BEFORE UPDATE OF tags ON test FOR EACH ROW EXECUTE FUNCTION rt_before_update_test_func();
            CREATE TRIGGER rt_after_insert AFTER INSERT OR UPDATE OF tags ON test FOR EACH ROW EXECUTE FUNCTION rt_after_insert_test_func();
        </sql>
    </changeSet>

    <changeSet id="49" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION json_equals(a jsonb, b jsonb) RETURNS boolean AS $$
            BEGIN
                RETURN ((a IS NULL AND b IS NULL) OR (a @> b AND b @> a));
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="50" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <renameTable oldTableName="regressiondetection" newTableName="changedetection" />
    </changeSet>

    <changeSet id="51" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER before_run_update ON run;
            DROP TRIGGER after_run_update ON run;
            CREATE TRIGGER before_run_update BEFORE UPDATE OF data ON run FOR EACH ROW EXECUTE FUNCTION before_run_update_func();
            CREATE TRIGGER after_run_update AFTER INSERT OR UPDATE OF data ON run FOR EACH ROW EXECUTE FUNCTION after_run_update_func();
        </sql>
    </changeSet>

    <changeSet id="52" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="transformer">
            <column name="id" type="integer">
                <constraints primaryKey="true"/>
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="description" type="text"/>
            <column name="schema_id" type="integer">
                <constraints foreignKeyName="transformer_schema_id" referencedTableName="schema" referencedColumnNames="id" nullable="false"/>
            </column>
            <column name="function" type="text"/>
            <column name="owner" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="access" type="integer">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <createTable tableName="transformer_extractors">
            <column name="transformer_id" type="integer">
                <constraints foreignKeyName="transformer_id" referencedTableName="transformer" referencedColumnNames="id" />
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="jsonpath" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addUniqueConstraint tableName="transformer_extractors" columnNames="transformer_id,name" />
        <createIndex tableName="transformer_extractors" indexName="transformer_index">
            <column name="transformer_id" />
        </createIndex>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE transformer, transformer_extractors TO "${quarkus.datasource.username}";
            ALTER TABLE transformer ENABLE ROW LEVEL SECURITY;
            ALTER TABLE transformer_extractors ENABLE ROW LEVEL SECURITY;
            CREATE POLICY tf_select ON transformer FOR SELECT USING (can_view2(access, owner) OR has_role('horreum.system'));
            CREATE POLICY tf_insert ON transformer FOR INSERT WITH CHECK (has_role2(owner, 'tester'));
            CREATE POLICY tf_update ON transformer FOR UPDATE USING (has_role2(owner, 'tester'));
            CREATE POLICY tf_delete ON transformer FOR DELETE USING (has_role2(owner, 'tester'));
            CREATE POLICY te_select ON transformer_extractors FOR SELECT
                USING (exists(SELECT 1 FROM transformer tf WHERE tf.id = transformer_id AND can_view2(tf.access, tf.owner)) OR has_role('horreum.system'));
            CREATE POLICY te_insert ON transformer_extractors FOR INSERT
                WITH CHECK (exists (SELECT 1 FROM transformer tf WHERE tf.id = transformer_id AND has_role2(tf.owner, 'tester')));
            CREATE POLICY te_update ON transformer_extractors FOR UPDATE
                USING (exists (SELECT 1 FROM transformer tf WHERE tf.id = transformer_id AND has_role2(tf.owner, 'tester')));
            CREATE POLICY te_delete ON transformer_extractors FOR DELETE
                USING (exists (SELECT 1 FROM transformer tf WHERE tf.id = transformer_id AND has_role2(tf.owner, 'tester')));
        </sql>
    </changeSet>

    <changeSet id="53" author="jwhiting">
        <validCheckSum>ANY</validCheckSum>
        <createSequence sequenceName="dataset_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="dataset">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="data" type="jsonb">
                <constraints nullable="false" />
            </column>
            <column name="start" type="timestamp without time zone">
                <constraints nullable="false" />
            </column>
            <column name="stop" type="timestamp without time zone">
                <constraints nullable="false" />
            </column>
            <column name="description" type="text" />
            <column name="testid" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="runid" type="integer">
                <constraints nullable="false" foreignKeyName="fk_dataset_run_id" references="run(id)"/>
            </column>
            <column name="owner" type="text">
                <constraints nullable="false" />
            </column>
            <column name="access" type="integer">
                <constraints nullable="false" />
            </column>
        </createTable>
        <sql>
            GRANT ALL ON SEQUENCE dataset_id_seq TO "${quarkus.datasource.username}";
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE dataset TO "${quarkus.datasource.username}";
            ALTER TABLE dataset ENABLE ROW LEVEL SECURITY;
            CREATE POLICY dataset_select ON dataset FOR SELECT
                USING (can_view2(access, owner) OR has_role('horreum.system'));
            CREATE POLICY dataset_insert ON dataset FOR INSERT
                WITH CHECK (has_role2(owner, 'uploader') OR has_upload_token(testid));
            CREATE POLICY dataset_delete ON dataset FOR DELETE
                USING (has_role('horreum.system'));
        </sql>
    </changeSet>

    <changeSet id="54" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="test_transformers">
            <column name="test_id" type="integer">
                <constraints foreignKeyName="fk_test_id" referencedTableName="test" referencedColumnNames="id" nullable="false"/>
            </column>
            <column name="transformer_id" type="integer">
                <constraints foreignKeyName="fk_transformer_id" referencedTableName="transformer" referencedColumnNames="id" nullable="false"/>
            </column>
        </createTable>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE test_transformers TO "${quarkus.datasource.username}";
            ALTER TABLE test_transformers ENABLE ROW LEVEL SECURITY;
            CREATE POLICY tt_select ON test_transformers FOR SELECT
                USING (exists(SELECT 1 FROM test WHERE test.id = test_id AND can_view2(test.access, test.owner)) OR has_role('horreum.system'));
            CREATE POLICY tt_insert ON test_transformers FOR INSERT
                WITH CHECK (exists (SELECT 1 FROM test WHERE test.id = test_id AND has_role2(test.owner, 'tester')));
            CREATE POLICY tt_update ON test_transformers FOR UPDATE
                USING (exists (SELECT 1 FROM test WHERE test.id = test_id AND has_role2(test.owner, 'tester')));
            CREATE POLICY tt_delete ON test_transformers FOR DELETE
                USING (exists (SELECT 1 FROM test WHERE test.id = test_id AND has_role2(test.owner, 'tester')));
        </sql>
    </changeSet>

    <changeSet id="55" author="jwhiting">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_func() RETURNS TRIGGER AS $$
            DECLARE
                v_schema text;
                v_schemaid integer;
                v_has_schema boolean := false;
            BEGIN
                FOR v_schema IN (SELECT jsonb_path_query(NEW.data, '$.\$schema'::jsonpath)#>>'{}') LOOP
                    v_schemaid := (SELECT id FROM schema WHERE uri = v_schema);
                    IF v_schemaid IS NOT NULL THEN
                        INSERT INTO run_schemas (runid, testid, prefix, uri, schemaid)
                            VALUES (NEW.id, NEW.testid, '$', v_schema, v_schemaid);
                        v_has_schema := true;
                    END IF;
                END LOOP;
                FOR v_schema IN (SELECT jsonb_path_query(NEW.data, '$.*.\$schema'::jsonpath)#>>'{}') LOOP
                    v_schemaid := (SELECT id FROM schema WHERE uri = v_schema);
                    IF v_schemaid IS NOT NULL THEN
                        INSERT INTO run_schemas (runid, testid, prefix, uri, schemaid)
                            VALUES (NEW.id, NEW.testid, '$.*', v_schema, v_schemaid);
                        v_has_schema := true;
                    END IF;
                END LOOP;
                IF NOT v_has_schema THEN
                    PERFORM pg_notify('calculate_tags', auth_suffix(NEW.id::text) );
                END IF;
                DELETE FROM dataset WHERE runid = OLD.id;
                PERFORM pg_notify('calculate_datasets', auth_suffix(NEW.id::text) );
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset WHERE runid = OLD.id;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER after_run_delete AFTER DELETE ON run FOR EACH ROW EXECUTE FUNCTION after_run_delete_func();
            INSERT INTO dataset (id, data, start, stop, description, testid, runid, owner, access)
               SELECT nextval('dataset_id_seq') as id, data, start, stop, description, testid, id as runid, owner, access FROM run;
        </sql>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_run_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE runid = OLD.id;
                DELETE FROM dataset WHERE runid = OLD.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="56" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="dataset">
            <column name="ordinal" type="integer" defaultValue="0">
                <constraints nullable="false" />
            </column>
        </addColumn>
        <addUniqueConstraint tableName="dataset" columnNames="runid,ordinal"/>
        <dropDefaultValue tableName="dataset" columnName="ordinal"/>
    </changeSet>

    <changeSet id="57" author="rvansa">
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_non_data_func() RETURNS TRIGGER AS $$
            BEGIN
                UPDATE dataset SET owner = NEW.owner, access = NEW.access, description = NEW.description WHERE runid = NEW.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER after_run_update_non_data AFTER UPDATE OF owner, access, description ON run FOR EACH ROW EXECUTE FUNCTION after_run_update_non_data_func();
            <!-- It would be nice to prevent updates of other fields but we don't do that right now -->
            CREATE POLICY dataset_update ON dataset FOR UPDATE USING (has_role2(owner, 'tester'));
        </sql>
    </changeSet>

    <changeSet id="58" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="transformer_extractors">
            <column name="isarray" type="boolean" defaultValue="false">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <dropDefaultValue tableName="transformer_extractors" columnName="isarray"/>
        <addColumn tableName="transformer">
            <column name="targetschemauri" type="text" />
        </addColumn>
    </changeSet>

    <changeSet id="59" author="jwhiting,rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createSequence sequenceName="label_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="label">
            <column name="id" type="integer">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="schema_id" type="integer">
                <constraints nullable="false" referencedTableName="schema" referencedColumnNames="id" foreignKeyName="label_schema_id"/>
            </column>
            <column name="function" type="text"/>
            <column name="filtering" type="boolean" defaultValue="true">
                <constraints nullable="false"/>
            </column>
            <column name="metrics" type="boolean" defaultValue="true">
                <constraints nullable="false"/>
            </column>
            <column name="owner" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="access" type="integer">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <createTable tableName="label_extractors">
            <column name="label_id" type="integer">
                <constraints foreignKeyName="label_id" referencedTableName="label" referencedColumnNames="id" />
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="jsonpath" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="isarray" type="boolean">
                <constraints nullable="false" />
            </column>
        </createTable>
        <addUniqueConstraint tableName="label_extractors" columnNames="label_id,name" />
        <createIndex tableName="label_extractors" indexName="label_index">
            <column name="label_id" />
        </createIndex>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE label, label_extractors TO "${quarkus.datasource.username}";
            GRANT ALL ON SEQUENCE label_id_seq TO "${quarkus.datasource.username}";
            ALTER TABLE label ENABLE ROW LEVEL SECURITY;
            ALTER TABLE label_extractors ENABLE ROW LEVEL SECURITY;
            CREATE POLICY l_select ON label FOR SELECT USING (can_view2(access, owner) OR has_role('horreum.system'));
            CREATE POLICY l_insert ON label FOR INSERT WITH CHECK (has_role2(owner, 'tester'));
            CREATE POLICY l_update ON label FOR UPDATE USING (has_role2(owner, 'tester'));
            CREATE POLICY l_delete ON label FOR DELETE USING (has_role2(owner, 'tester'));

            CREATE POLICY le_select ON label_extractors FOR SELECT
                USING (exists(SELECT 1 FROM label WHERE label.id = label_id AND can_view2(label.access, label.owner)) OR has_role('horreum.system'));
            CREATE POLICY le_insert ON label_extractors FOR INSERT
                WITH CHECK (exists (SELECT 1 FROM label WHERE label.id = label_id AND has_role2(label.owner, 'tester')));
            CREATE POLICY le_update ON label_extractors FOR UPDATE
                USING (exists(SELECT 1 FROM label WHERE label.id = label_id AND has_role2(label.owner, 'tester')));
            CREATE POLICY le_delete ON label_extractors FOR DELETE
                USING (exists(SELECT 1 FROM label WHERE label.id = label_id AND has_role2(label.owner, 'tester')));
        </sql>
    </changeSet>
    
    <changeSet id="60" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="label_values">
            <column name="dataset_id" type="integer">
                <!-- No FK constraint: the row will be removed via trigger -->
                <constraints nullable="false"/>
            </column>
            <column name="label_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <!-- null value is not represented as JSON null but as null column value -->
            <column name="value" type="jsonb" />
        </createTable>
        <addUniqueConstraint tableName="label_values" columnNames="dataset_id,label_id" />
        <createIndex tableName="label_values" indexName="by_dataset">
            <column name="dataset_id" />
        </createIndex>
        <createTable tableName="dataset_schemas">
            <column name="dataset_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="schema_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="uri" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="index" type="integer">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <!-- This is an intermediate table for combining all updates to a label/label extractors into one notification -->
        <createTable tableName="label_recalc_queue">
            <column name="schema_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="label_id" type="integer">
                <constraints nullable="false" unique="true" uniqueConstraintName="unique_label"/>
            </column>
        </createTable>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_before_dataset_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM label_values WHERE dataset_id = OLD.id;
                DELETE FROM dataset_schemas WHERE dataset_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_before_label_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM label_values WHERE label_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_before_label_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM label_values WHERE label_id = OLD.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_before_le_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM label_values WHERE label_id = OLD.label_id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_before_le_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM label_values WHERE label_id = OLD.label_id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION ds_after_dataset_insert_func() RETURNS TRIGGER AS $$
            BEGIN
                <!-- This should not happen after #148 -->
                IF jsonb_path_query(NEW.data, '$.type() != "array"') THEN
                    RETURN NEW;
                END IF;
                WITH uris AS (
                    SELECT jsonb_array_elements(NEW.data)->>'$schema' AS uri
                ), indexed as (
                    SELECT uri, row_number() over () - 1 as index FROM uris
                ) INSERT INTO dataset_schemas(dataset_id, uri, index, schema_id)
                    SELECT NEW.id as dataset_id, indexed.uri, indexed.index, schema.id FROM indexed JOIN schema ON schema.uri = indexed.uri;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_after_label_update_func() RETURNS TRIGGER AS $$
            BEGIN
                INSERT INTO label_recalc_queue(schema_id, label_id) VALUES
                    (NEW.schema_id, NEW.id) ON CONFLICT (label_id) DO NOTHING;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_after_le_update_func() RETURNS TRIGGER AS $$
            DECLARE
                v_schema integer;
            BEGIN
                SELECT schema_id INTO v_schema FROM label WHERE id = NEW.label_id;
                IF v_schema IS NOT NULL THEN
                    INSERT INTO label_recalc_queue(schema_id, label_id) VALUES
                        (v_schema, NEW.label_id) ON CONFLICT (label_id) DO NOTHING;
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION lv_after_le_delete_func() RETURNS TRIGGER AS $$
            DECLARE
                v_schema integer;
            BEGIN
                SELECT schema_id INTO v_schema FROM label WHERE id = NEW.label_id;
                IF v_schema IS NOT NULL THEN
                    INSERT INTO label_recalc_queue(schema_id, label_id) VALUES
                        (v_schema, OLD.label_id) ON CONFLICT (label_id) DO NOTHING;
                END IF;
            RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            <!-- Note: labels are deleted before schema, so these clear label_values -->
            CREATE OR REPLACE FUNCTION before_schema_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE schemaid = OLD.id;
                DELETE FROM dataset_schemas WHERE schema_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION recalc_label_values() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM pg_notify('calculate_labels', dataset_id::text || ';' || NEW.label_id) FROM dataset_schemas
                    WHERE dataset_schemas.schema_id = NEW.schema_id;
                DELETE FROM label_recalc_queue WHERE label_id = NEW.label_id;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            <!-- dataset.data should not be updated -->
            CREATE TRIGGER ds_after_insert AFTER INSERT ON dataset FOR EACH ROW EXECUTE FUNCTION ds_after_dataset_insert_func();
            CREATE TRIGGER lv_before_delete BEFORE DELETE ON dataset FOR EACH ROW EXECUTE FUNCTION lv_before_dataset_delete_func();
            CREATE TRIGGER lv_before_delete BEFORE DELETE ON label FOR EACH ROW EXECUTE FUNCTION lv_before_label_delete_func();
            CREATE TRIGGER lv_before_update BEFORE UPDATE OF function ON label FOR EACH ROW EXECUTE FUNCTION lv_before_label_update_func();
            CREATE TRIGGER lv_before_delete BEFORE DELETE OR UPDATE ON label_extractors FOR EACH ROW EXECUTE FUNCTION lv_before_le_delete_func();
            CREATE TRIGGER lv_before_update BEFORE UPDATE ON label_extractors FOR EACH ROW EXECUTE FUNCTION lv_before_le_update_func();
            <!-- These triggers need to be deferred until the transaction completes because we likely execute update to both label extractor(s) and function -->
            CREATE CONSTRAINT TRIGGER lv_after_update AFTER INSERT OR UPDATE OF function ON label DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION lv_after_label_update_func();
            CREATE CONSTRAINT TRIGGER lv_after_update AFTER INSERT OR UPDATE ON label_extractors DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION lv_after_le_update_func();
            CREATE CONSTRAINT TRIGGER lv_after_delete AFTER DELETE ON label_extractors DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION lv_after_le_delete_func();
            CREATE CONSTRAINT TRIGGER recalc_labels AFTER INSERT ON label_recalc_queue DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION recalc_label_values();

            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE label_values, dataset_schemas, label_recalc_queue TO "${quarkus.datasource.username}";
            ALTER TABLE label_values ENABLE ROW LEVEL SECURITY;
            ALTER TABLE dataset_schemas ENABLE ROW LEVEL SECURITY;
            CREATE POLICY ds_select ON dataset_schemas FOR SELECT
                USING (exists(SELECT 1 FROM dataset WHERE id = dataset_id AND can_view2(access, owner)));
            CREATE POLICY ds_insert ON dataset_schemas FOR INSERT
                WITH CHECK (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND (has_role2(owner, 'uploader') OR has_role2(owner, 'tester'))));
            CREATE POLICY ds_delete ON dataset_schemas FOR DELETE
                USING (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND has_role2(owner, 'tester')));
            CREATE POLICY lv_select ON label_values FOR SELECT
                USING (exists(SELECT 1 FROM dataset WHERE dataset.id = dataset_id AND can_view2(access, owner)));
            CREATE POLICY lv_insert ON label_values FOR INSERT WITH CHECK (has_role('horreum.system'));
            CREATE POLICY lv_delete ON label_values FOR DELETE
                USING (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE dataset.id = dataset_id AND has_role2(owner, 'tester')));
            ALTER POLICY dataset_delete ON dataset
                USING (has_role('horreum.system') OR has_role2(owner, 'tester'));
        </sql>
    </changeSet>

    <changeSet id="61" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            ALTER POLICY dataset_insert ON dataset
                WITH CHECK (has_role('horreum.system') OR (has_role2(owner, 'tester') AND
                    exists((SELECT 1 FROM run WHERE run.id = runid AND has_role2(run.owner, 'tester')))));
        </sql>
    </changeSet>

    <changeSet id="62" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            WITH vca AS (
                SELECT id, unnest(string_to_array(accessors, ';')) AS accessor, headername, render from viewcomponent
            ), vca2 AS (
                SELECT id, replace(vca.accessor, '[]', '') as accessor, vca.accessor like '%[]' as isarray, headername, render FROM vca
            ), le AS (
                SELECT row_number() OVER () AS label_id, vca2.headername, schema_id, vca2.render AS function,
                    array_agg(vca2.accessor) AS accessors, array_agg(isarray) AS isarrays, array_agg('$' || jsonpath) AS jsonpaths, owner, access FROM vca2
                JOIN schemaextractor se ON vca2.accessor = se.accessor
                JOIN schema ON schema.id = schema_id
                GROUP BY schema_id, vca2.id, vca2.headername, function, owner, access
            ), label_insert AS (
                INSERT INTO label (id, name, schema_id, function, filtering, metrics, owner, access)
                SELECT label_id, headername, schema_id, function, false, true, owner, access FROM le
            ) INSERT INTO label_extractors (label_id, name, jsonpath, isarray)
                SELECT label_id, unnest(accessors), unnest(jsonpaths), unnest(isarrays) FROM le;

            WITH var AS (
                SELECT id, unnest(string_to_array(accessors, ';')) AS accessor, name, calculation from variable
            ), var2 AS (
                SELECT id, replace(var.accessor, '[]', '') as accessor, var.accessor like '%[]' as isarray, name, calculation FROM var
            ), le AS (
                SELECT row_number() OVER () + (SELECT count(*) FROM label) AS label_id, var2.name, schema_id, var2.calculation AS function,
                    array_agg(var2.accessor) AS accessors, array_agg(isarray) AS isarrays, array_agg('$' || jsonpath) AS jsonpaths, owner, access FROM var2
                JOIN schemaextractor se ON var2.accessor = se.accessor
                JOIN schema ON schema.id = schema_id
                GROUP BY schema_id, var2.id, var2.name, function, owner, access
            ), label_insert AS (
                INSERT INTO label (id, name, schema_id, function, filtering, metrics, owner, access)
                SELECT label_id, name, schema_id, function, false, true, owner, access FROM le
            ) INSERT INTO label_extractors (label_id, name, jsonpath, isarray)
                SELECT label_id, unnest(accessors), unnest(jsonpaths), unnest(isarrays) FROM le;

            WITH rc AS (
                SELECT id, unnest(string_to_array(accessors, ';')) AS accessor, name, function from reportcomponent
            ), rc2 AS (
                SELECT id, replace(rc.accessor, '[]', '') as accessor, rc.accessor like '%[]' as isarray, name, function FROM rc
            ), le AS (
                SELECT row_number() OVER () + (SELECT count(*) FROM label) AS label_id, rc2.name, schema_id, rc2.function,
                    array_agg(rc2.accessor) AS accessors, array_agg(isarray) AS isarrays, array_agg('$' || jsonpath) AS jsonpaths, owner, access FROM rc2
                JOIN schemaextractor se ON rc2.accessor = se.accessor
                JOIN schema ON schema.id = schema_id
                GROUP BY schema_id, rc2.id, rc2.name, function, owner, access
            ), label_insert AS (
                INSERT INTO label (id, name, schema_id, function, filtering, metrics, owner, access)
                SELECT label_id, name, schema_id, function, false, true, owner, access FROM le
            ) INSERT INTO label_extractors (label_id, name, jsonpath, isarray)
                SELECT label_id, unnest(accessors), unnest(jsonpaths), unnest(isarrays) FROM le;

            WITH tt AS (
                SELECT id, unnest(string_to_array(tags, ';')) AS accessor, name, tagscalculation as function from test
            ), tt2 AS (
                SELECT id, replace(tt.accessor, '[]', '') as accessor, tt.accessor like '%[]' as isarray, name, function FROM tt
            ), le AS (
                SELECT row_number() OVER () + (SELECT count(*) FROM label) AS label_id, tt2.name || '_tags' AS name, schema_id, tt2.function,
                    array_agg(tt2.accessor) AS accessors, array_agg(isarray) AS isarrays, array_agg('$' || jsonpath) AS jsonpaths, owner, access FROM tt2
                JOIN schemaextractor se ON tt2.accessor = se.accessor
                JOIN schema ON schema.id = schema_id
                GROUP BY schema_id, tt2.id, tt2.name, function, owner, access
            ), label_insert AS (
                INSERT INTO label (id, name, schema_id, function, filtering, metrics, owner, access)
                SELECT label_id, name, schema_id, function, true, false, owner, access FROM le
            ) INSERT INTO label_extractors (label_id, name, jsonpath, isarray)
                SELECT label_id, unnest(accessors), unnest(jsonpaths), unnest(isarrays) FROM le;

            WITH acs AS (
                SELECT DISTINCT unnest(string_to_array(filteraccessors, ';') ||
                    string_to_array(categoryaccessors, ';') || string_to_array(labelaccessors, ';')) AS accessor FROM tablereportconfig
            ), le AS (
                SELECT row_number() over () + (SELECT count(*) FROM label) AS label_id, acs.accessor, array_agg('$' || jsonpath) as jsonpaths, schema_id, owner, access FROM acs
                JOIN schemaextractor se ON acs.accessor = se.accessor
                JOIN schema ON schema.id = schema_id
                GROUP BY schema_id, acs.accessor, owner, access
            ), label_insert AS (
                INSERT INTO label (id, name, schema_id, function, filtering, metrics, owner, access)
                SELECT label_id, accessor, schema_id, NULL, true, false, owner, access FROM le
            ) INSERT INTO label_extractors (label_id, name, jsonpath, isarray)
                SELECT label_id, accessor, unnest(jsonpaths), false FROM le;

            <!-- Remove duplicate labels -->
            WITH ids AS (
                SELECT l1.id FROM label l1 JOIN label l2 ON l1.name = l2.name AND l1.schema_id = l2.schema_id WHERE l1.id > l2.id
            ), drop_extractors AS (
                DELETE FROM label_extractors USING ids WHERE label_id = ids.id
            ) DELETE FROM label USING ids WHERE label.id = ids.id;
        </sql>
    </changeSet>

    <!-- We need a new transaction to alter the table due to triggers -->
    <changeSet id="63" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addUniqueConstraint tableName="label" columnNames="name,schema_id"/>
    </changeSet>

    <changeSet id="64" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="viewcomponent">
            <column name="labels" type="jsonb" />
        </addColumn>
        <sql>
            UPDATE viewcomponent SET labels = jsonb_build_array(headername);

            DROP TRIGGER vd_before_delete ON run_schemas;
            DROP TRIGGER vd_after_insert ON run_schemas;
            DROP TRIGGER vd_before_update ON schemaextractor;
            DROP TRIGGER vd_before_delete ON schemaextractor;
            DROP TRIGGER vd_after_update ON schemaextractor;
            DROP TRIGGER vd_before_delete ON viewcomponent;
            DROP TRIGGER vd_before_update ON viewcomponent;
            DROP TRIGGER vd_after_update ON viewcomponent;

            DROP FUNCTION vd_before_delete_run_func;
            DROP FUNCTION vd_after_insert_run_func;
            DROP FUNCTION vd_before_update_extractor_func;
            DROP FUNCTION vd_before_delete_extractor_func;
            DROP FUNCTION vd_after_update_extractor_func;
            DROP FUNCTION vd_before_delete_vc_func;
            DROP FUNCTION vd_before_update_vc_func;
            DROP FUNCTION vd_after_update_vc_func;
        </sql>
        <addNotNullConstraint tableName="viewcomponent" columnName="labels"/>
        <dropColumn tableName="viewcomponent" columnName="accessors"/>
        <dropTable tableName="view_data"/>
    </changeSet>

    <changeSet id="65" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="dataset_view">
            <column name="dataset_id" type="integer" />
            <column name="view_id" type="integer" />
            <column name="label_ids" type="int[]" />
            <column name="value" type="jsonb" />
        </createTable>
        <addPrimaryKey tableName="dataset_view" columnNames="dataset_id,view_id"/>
        <createTable tableName="view_recalc_queue">
            <column name="dataset_id" type="integer">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="view_id" type="integer" />
            <column name="roles" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE dataset_view TO "${quarkus.datasource.username}";
            ALTER TABLE dataset_view ENABLE ROW LEVEL SECURITY;
            CREATE POLICY dsv_select ON dataset_view FOR SELECT
                USING (exists(SELECT 1 FROM dataset WHERE id = dataset_id AND can_view2(access, owner)));
            CREATE POLICY dsv_insert ON dataset_view FOR INSERT
                WITH CHECK (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND has_role2(owner, 'tester')));
            CREATE POLICY dsv_delete ON dataset_view FOR DELETE
                USING (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND has_role2(owner, 'tester')));

            <!-- As the view is calculated in the deferred trigger the horreum.userroles is already unset;
                 we need to persist the permissions in the roles column but this must not be readable by others -->
            GRANT INSERT, DELETE ON TABLE view_recalc_queue TO "${quarkus.datasource.username}";
            GRANT SELECT (dataset_id, view_id) ON TABLE view_recalc_queue TO "${quarkus.datasource.username}";
        </sql>

        <createProcedure>
            CREATE OR REPLACE FUNCTION dsv_after_lv_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset_view WHERE dataset_id = OLD.dataset_id;
                INSERT INTO view_recalc_queue(dataset_id, roles) VALUES (OLD.dataset_id, current_setting('horreum.userroles', true)) ON CONFLICT DO NOTHING;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION dsv_after_lv_insert_func() RETURNS TRIGGER AS $$
            BEGIN
            INSERT INTO view_recalc_queue(dataset_id, roles) VALUES (NEW.dataset_id, current_setting('horreum.userroles', true)) ON CONFLICT DO NOTHING;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION dsv_after_vc_delete_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH ds AS (
                    DELETE FROM dataset_view WHERE view_id = OLD.view_id RETURNING dataset_id, view_id
                ) INSERT INTO view_recalc_queue(dataset_id, view_id, roles) SELECT dataset_id, view_id, current_setting('horreum.userroles', true) FROM ds ON CONFLICT DO NOTHING;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION dsv_after_vc_update_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH ds AS (
                    DELETE FROM dataset_view WHERE view_id = OLD.view_id OR view_id = NEW.view_id RETURNING dataset_id, view_id
                ) INSERT INTO view_recalc_queue(dataset_id, view_id, roles) SELECT dataset_id, view_id, current_setting('horreum.userroles', true) FROM ds ON CONFLICT DO NOTHING;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            <!-- This is invoked as deferrd trigger and horreum.userroles is already unset
                 - we can pass it to the trigger using a non-readable column -->
            CREATE OR REPLACE FUNCTION recalc_dataset_view() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM set_config('horreum.userroles', NEW.roles, true);
                WITH view_agg AS (
                    SELECT vc.view_id, vc.id as vcid, array_agg(DISTINCT label.id) as label_ids, jsonb_object_agg(label.name, lv.value) as value FROM dataset_schemas ds
                    JOIN label ON label.schema_id = ds.schema_id
                    JOIN viewcomponent vc ON vc.labels ? label.name
                    JOIN label_values lv ON lv.label_id = label.id
                    WHERE ds.dataset_id = NEW.dataset_id AND (NEW.view_id IS NULL OR NEW.view_id = vc.view_id)
                    GROUP BY vc.view_id, vcid
                ) INSERT INTO dataset_view (dataset_id, view_id, label_ids, value)
                    SELECT NEW.dataset_id, view_id, array_agg(DISTINCT label_id), jsonb_object_agg(vcid, value) FROM view_agg, unnest(label_ids) as label_id
                    GROUP BY view_id;
                DELETE FROM view_recalc_queue WHERE dataset_id = NEW.dataset_id AND (view_id IS NULL OR view_id = NEW.view_id);
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER dsv_after_delete AFTER DELETE ON label_values FOR EACH ROW EXECUTE FUNCTION dsv_after_lv_delete_func();
            CREATE TRIGGER dsv_after_insert AFTER INSERT ON label_values FOR EACH ROW EXECUTE FUNCTION dsv_after_lv_insert_func();
            CREATE TRIGGER dsv_after_delete AFTER DELETE ON viewcomponent FOR EACH ROW EXECUTE FUNCTION dsv_after_vc_delete_func();
            CREATE TRIGGER dsv_after_update AFTER INSERT OR UPDATE OF labels ON viewcomponent FOR EACH ROW EXECUTE FUNCTION dsv_after_vc_update_func();
            CREATE CONSTRAINT TRIGGER recalc_dataset_view AFTER INSERT ON view_recalc_queue DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION recalc_dataset_view();
        </sql>
    </changeSet>

    <changeSet id="66" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="test">
            <column name="fingerprint_labels" type="jsonb" />
        </addColumn>
        <addColumn tableName="test">
            <column name="fingerprint_filter" type="text" />
        </addColumn>

        <dropColumn tableName="variable" columnName="accessors"/>
        <addColumn tableName="variable">
            <column name="labels" type="jsonb" defaultValue="[]">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <sql>
            UPDATE variable SET labels = jsonb_build_array(name);
        </sql>
        <dropDefaultValue tableName="variable" columnName="labels"/>
    </changeSet>

    <changeSet id="67" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <!-- We will need to recompute them all anyway -->
        <sql>
            DELETE FROM datapoint;
            DELETE FROM change;
            DELETE FROM calculationlog;
            UPDATE test SET fingerprint_labels = jsonb_build_array(name || '_tags');
        </sql>
        <renameColumn tableName="datapoint" oldColumnName="runid" newColumnName="dataset_id"/>
        <renameColumn tableName="change" oldColumnName="runid" newColumnName="dataset_id"/>
        <renameColumn tableName="calculationlog" oldColumnName="runid" newColumnName="dataset_id"/>
        <renameTable oldTableName="calculationlog" newTableName="datasetlog" />
        <sql>
            ALTER POLICY cl_all ON datasetlog
                USING (has_role('horreum.system') OR (exists(
                    SELECT 1 FROM test WHERE test.id = testid AND has_role(test.owner)
                ) AND exists(
                    SELECT 1 FROM dataset WHERE id = dataset_id AND has_role(owner)
                )
            ));
            ALTER POLICY datapoint_select ON datapoint
                USING (has_role('horreum.system') OR exists(
                    SELECT 1 FROM dataset WHERE dataset.id = dataset_id AND can_view2(dataset.access, dataset.owner)
                ));
            ALTER POLICY datapoint_insert ON datapoint WITH CHECK (has_role('horreum.system'));
            ALTER POLICY datapoint_update ON datapoint USING (has_role('horreum.system'));
            ALTER POLICY datapoint_delete ON datapoint
                USING (has_role('horreum.system') OR has_role2((SELECT owner FROM dataset WHERE dataset.id = dataset_id), 'tester'));

            ALTER POLICY change_select ON change
                USING (has_role('horreum.system') OR exists(
                    SELECT 1 FROM dataset WHERE dataset.id = dataset_id AND can_view2(dataset.access, dataset.owner)
                ));
            ALTER POLICY change_insert ON change WITH CHECK (has_role('horreum.system'));
            ALTER POLICY change_update ON change USING (has_role2((SELECT owner FROM dataset WHERE dataset.id = dataset_id), 'tester'));
            ALTER POLICY change_delete ON change
                USING (has_role('horreum.system') OR has_role2((SELECT owner FROM dataset WHERE dataset.id = dataset_id), 'tester'));

            ALTER POLICY variable_access ON variable USING (has_role('horreum.system') OR has_role('horreum.alerting'));
            ALTER POLICY rd_access ON changedetection USING (has_role('horreum.system') OR has_role('horreum.alerting'));
            ALTER POLICY test_select ON test
                USING (can_view2(access, owner) OR has_read_token(id) OR has_role('horreum.system'));
            ALTER POLICY lv_select ON label_values
                USING (exists(SELECT 1 FROM dataset WHERE dataset.id = dataset_id AND can_view2(access, owner)) OR has_role('horreum.system'));

        </sql>

        <createTable tableName="fingerprint">
            <column name="dataset_id" type="integer">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="fingerprint" type="jsonb" />
        </createTable>
        <createTable tableName="fingerprint_recalc_queue">
            <column name="dataset_id" type="integer">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="roles" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <createProcedure>
            CREATE OR REPLACE FUNCTION fp_after_lv_delete_func() RETURNS TRIGGER AS $$
            DECLARE
                labels jsonb;
            BEGIN
                labels := (SELECT fingerprint_labels FROM test JOIN dataset ON test.id = dataset.testid WHERE dataset.id = OLD.dataset_id);
                IF labels ? (SELECT name FROM label WHERE id = OLD.label_id) THEN
                    DELETE FROM fingerprint WHERE dataset_id = OLD.dataset_id;
                    INSERT INTO fingerprint_recalc_queue(dataset_id, roles) VALUES (OLD.dataset_id, current_setting('horreum.userroles', true)) ON CONFLICT DO NOTHING;
                END IF;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION fp_after_lv_insert_func() RETURNS TRIGGER AS $$
            DECLARE
                labels jsonb;
            BEGIN
                labels := (SELECT fingerprint_labels FROM test JOIN dataset ON test.id = dataset.testid WHERE dataset.id = NEW.dataset_id);
                IF labels ? (SELECT name FROM label WHERE id = NEW.label_id) THEN
                    DELETE FROM fingerprint WHERE dataset_id = NEW.dataset_id;
                    INSERT INTO fingerprint_recalc_queue(dataset_id, roles) VALUES (NEW.dataset_id, current_setting('horreum.userroles', true)) ON CONFLICT DO NOTHING;
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION fp_after_test_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM fingerprint USING dataset WHERE dataset.testid = NEW.id AND dataset.id = dataset_id;
                WITH ds AS (
                    SELECT id FROM dataset WHERE testid = NEW.id
                ) INSERT INTO fingerprint_recalc_queue(dataset_id, roles)
                    SELECT id, current_setting('horreum.userroles', true) FROM ds ON CONFLICT DO NOTHING;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            <!-- This is invoked as deferrd trigger and horreum.userroles is already unset
                 - we can pass it to the trigger using a non-readable column -->
            CREATE OR REPLACE FUNCTION recalc_fingerprint() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM set_config('horreum.userroles', NEW.roles, true);
                WITH fps AS (
                    SELECT NEW.dataset_id, jsonb_object_agg(label.name, lv.value) AS fingerprint, count(DISTINCT label.name) > 1 AS multi FROM test
                    LEFT JOIN label ON test.fingerprint_labels ? label.name
                    LEFT JOIN label_values lv ON label.id = lv.label_id
                    WHERE test.id = (SELECT testid FROM dataset WHERE id = NEW.dataset_id) AND lv.dataset_id = NEW.dataset_id
                ) INSERT INTO fingerprint(dataset_id, fingerprint)
                    SELECT dataset_id, (CASE WHEN fps.multi THEN fingerprint ELSE (SELECT value FROM jsonb_each(fingerprint)) END)
                        FROM fps WHERE fingerprint IS NOT NULL;
                DELETE FROM fingerprint_recalc_queue WHERE dataset_id = NEW.dataset_id;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE fingerprint TO "${quarkus.datasource.username}";
            ALTER TABLE fingerprint ENABLE ROW LEVEL SECURITY;
            CREATE POLICY fp_select ON fingerprint FOR SELECT
                USING (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND can_view2(access, owner)));
            CREATE POLICY fp_insert ON fingerprint FOR INSERT
                WITH CHECK (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND has_role2(owner, 'tester')));
            CREATE POLICY fp_delete ON fingerprint FOR DELETE
                USING (has_role('horreum.system') OR exists(SELECT 1 FROM dataset WHERE id = dataset_id AND has_role2(owner, 'tester')));

            <!-- As the view is calculated in the deferred trigger the horreum.userroles is already unset;
                 we need to persist the permissions in the roles column but this must not be readable by others -->
            GRANT INSERT, DELETE ON TABLE fingerprint_recalc_queue TO "${quarkus.datasource.username}";
            GRANT SELECT (dataset_id) ON TABLE fingerprint_recalc_queue TO "${quarkus.datasource.username}";

            CREATE TRIGGER fp_after_delete AFTER DELETE ON label_values FOR EACH ROW EXECUTE FUNCTION fp_after_lv_delete_func();
            CREATE TRIGGER fp_after_insert AFTER INSERT ON label_values FOR EACH ROW EXECUTE FUNCTION fp_after_lv_insert_func();
            CREATE TRIGGER fp_after_update AFTER UPDATE OF fingerprint_labels ON test FOR EACH ROW EXECUTE FUNCTION fp_after_test_update_func();
            CREATE CONSTRAINT TRIGGER recalc_fingerprint AFTER INSERT ON fingerprint_recalc_queue DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION recalc_fingerprint();
        </sql>
        <!-- Because the ? operator won't be parsed for native query correctly-->
        <createProcedure>
            CREATE OR REPLACE FUNCTION json_contains(container jsonb, element text) RETURNS boolean AS $$
            BEGIN
                RETURN container ? element;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="68" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="tablereportconfig">
            <column name="filterlabels" type="jsonb" />
            <column name="categorylabels" type="jsonb" />
            <column name="serieslabels" type="jsonb" />
            <column name="scalelabels" type="jsonb" />
        </addColumn>
        <sql>
            UPDATE tablereportconfig SET
                filterlabels = to_jsonb(string_to_array(filteraccessors, ';')),
                categorylabels = to_jsonb(string_to_array(categoryaccessors, ';')),
                serieslabels = to_jsonb(string_to_array(seriesaccessors, ';')),
                scalelabels = to_jsonb(string_to_array(labelaccessors, ';'));
        </sql>
        <addNotNullConstraint tableName="tablereportconfig" columnName="serieslabels" />
        <dropColumn tableName="tablereportconfig" columnName="filteraccessors"/>
        <dropColumn tableName="tablereportconfig" columnName="categoryaccessors"/>
        <dropColumn tableName="tablereportconfig" columnName="seriesaccessors"/>
        <dropColumn tableName="tablereportconfig" columnName="labelaccessors"/>
        <renameColumn tableName="tablereportconfig" oldColumnName="labelfunction" newColumnName="scalefunction"/>
        <renameColumn tableName="tablereportconfig" oldColumnName="labelformatter" newColumnName="scaleformatter"/>
        <renameColumn tableName="tablereportconfig" oldColumnName="labeldescription" newColumnName="scaledescription"/>

        <addColumn tableName="reportcomponent">
            <column name="labels" type="jsonb"/>
        </addColumn>
        <sql>
            UPDATE reportcomponent SET labels = jsonb_build_array(name);
        </sql>
        <dropColumn tableName="reportcomponent" columnName="accessors"/>
        <addNotNullConstraint tableName="reportcomponent" columnName="labels"/>

        <renameTable oldTableName="tablereport_rundata" newTableName="tablereport_data" />
        <addColumn tableName="tablereport_data">
            <column name="dataset_id" type="integer" />
            <column name="ordinal" type="integer" />
        </addColumn>
        <renameColumn tableName="tablereport_data" oldColumnName="label" newColumnName="scale" />
        <sql>
            UPDATE tablereport_data trd SET dataset_id = (SELECT id FROM dataset WHERE dataset.runid = trd.runid LIMIT 1), ordinal = 0;
        </sql>
        <dropUniqueConstraint tableName="tablereport_data" constraintName="tablereport_rundata_report_id_runid_key"/>
        <addNotNullConstraint tableName="tablereport_data" columnName="dataset_id"/>
        <addNotNullConstraint tableName="tablereport_data" columnName="ordinal"/>
        <addUniqueConstraint tableName="tablereport_data" columnNames="report_id,dataset_id,ordinal" />
    </changeSet>

    <changeSet id="69" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION ds_after_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH ds AS (
                    SELECT id, jsonb_array_elements(data)->>'$schema' as uri FROM dataset
                    WHERE jsonb_path_query_first(data, '$.type() == "array"')::boolean
                ),  indexed AS (
                    SELECT id, uri, row_number() over (PARTITION BY id) - 1 AS index FROM ds
                ) INSERT INTO dataset_schemas(dataset_id, uri, index, schema_id)
                    SELECT indexed.id, NEW.uri, indexed.index, NEW.id FROM indexed WHERE indexed.uri = NEW.uri;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE schemaid = OLD.id;
                DELETE FROM dataset_schemas WHERE schema_id = OLD.id;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER ds_after_schema_update AFTER INSERT OR UPDATE OF uri ON schema FOR EACH ROW EXECUTE FUNCTION ds_after_schema_update_func();
        </sql>
    </changeSet>

    <changeSet id="70" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <!-- We'll keep the old timestamp around to resolve any mess that's created by this change -->
        <renameColumn tableName="run" oldColumnName="start" newColumnName="old_start" />
        <addColumn tableName="run">
            <column name="start" type="timestamptz"/>
        </addColumn>
        <sql>
            UPDATE run SET start = old_start;
        </sql>
        <addNotNullConstraint tableName="run" columnName="start"/>
        <dropNotNullConstraint tableName="run" columnName="old_start"/>

        <modifyDataType tableName="run" columnName="stop" newDataType="timestamptz"/>
        <modifyDataType tableName="dataset" columnName="start" newDataType="timestamptz"/>
        <modifyDataType tableName="dataset" columnName="stop" newDataType="timestamptz"/>
        <modifyDataType tableName="datasetlog" columnName="timestamp" newDataType="timestamptz"/>
    </changeSet>

    <changeSet id="71" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropTable tableName="lastmissingrunnotification"/>
        <renameTable oldTableName="test_stalenesssettings" newTableName="missingdata_rule"/>
        <addColumn tableName="missingdata_rule">
            <column name="id" type="integer"/>
            <column name="name" type="text"/>
            <column name="condition" type="text" />
            <column name="last_notification" type="timestamptz" />
        </addColumn>
        <renameColumn tableName="missingdata_rule" oldColumnName="tags" newColumnName="labels" />
        <dropForeignKeyConstraint baseTableName="missingdata_rule" constraintName="fk_ss_test_id" />
        <sql>
            UPDATE missingdata_rule SET id = nextval('hibernate_sequence');
        </sql>
        <addPrimaryKey tableName="missingdata_rule" columnNames="id"/>
        <addNotNullConstraint tableName="missingdata_rule" columnName="id" />

        <createTable tableName="missingdata_ruleresult">
            <column name="rule_id" type="integer">
                <constraints nullable="false" referencedTableName="missingdata_rule" referencedColumnNames="id" foreignKeyName="rr_rule_id"/>
            </column>
            <column name="dataset_id" type="integer">
                <!-- No constraint, we'll remove through triggers -->
                <constraints nullable="false"/>
            </column>
            <column name="timestamp" type="timestamptz">
                <constraints nullable="false"/>
            </column>
        </createTable>

        <createProcedure>
            CREATE OR REPLACE FUNCTION mdr_on_test_delete() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM missingdata_rule WHERE test_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION mdr_on_rule_delete() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM missingdata_ruleresult WHERE rule_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION mdr_on_dataset_delete() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM missingdata_ruleresult WHERE dataset_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE POLICY ss_all ON missingdata_rule FOR ALL USING (has_role('horreum.system'));

            GRANT select, insert, delete, update ON TABLE missingdata_ruleresult TO "${quarkus.datasource.username}";
            ALTER TABLE missingdata_ruleresult ENABLE ROW LEVEL SECURITY;
            CREATE POLICY mdr_all ON missingdata_ruleresult FOR ALL USING (has_role('horreum.system'));
            <!-- Let tester delete the result along with the dataset when trashing the run -->
            CREATE POLICY mdr_select ON missingdata_ruleresult FOR SELECT USING (exists(SELECT 1 FROM dataset WHERE id = dataset_id AND can_view2(access, owner)));
            CREATE POLICY mdr_delete ON missingdata_ruleresult FOR DELETE USING (has_role2((SELECT owner FROM dataset WHERE id = dataset_id), 'tester'));

            CREATE TRIGGER mdr_before_delete BEFORE DELETE ON test FOR EACH ROW EXECUTE FUNCTION mdr_on_test_delete();
            CREATE TRIGGER mdr_before_delete BEFORE DELETE ON missingdata_rule FOR EACH ROW EXECUTE FUNCTION mdr_on_rule_delete();
            CREATE TRIGGER mdr_before_delete BEFORE DELETE ON dataset FOR EACH ROW EXECUTE FUNCTION mdr_on_dataset_delete();
        </sql>
    </changeSet>

    <changeSet id="72" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropColumn tableName="run_expectation" columnName="tags"/>
        <dropTable tableName="run_tags"/>
        <sql>
            DROP TRIGGER rt_after_insert ON test;
            DROP TRIGGER rt_before_update ON test;
            DROP TRIGGER rt_before_delete ON test;

            DROP TRIGGER rt_before_delete ON run;

            DROP TRIGGER rt_after_insert ON run_schemas;
            DROP TRIGGER rt_before_delete ON run_schemas;

            DROP TRIGGER rt_before_insert ON schemaextractor;
            DROP TRIGGER rt_before_update ON schemaextractor;
            DROP TRIGGER rt_after_update ON schemaextractor;
            DROP TRIGGER rt_before_delete ON schemaextractor;

            DROP FUNCTION rt_after_insert_test_func;
            DROP FUNCTION rt_before_update_test_func;
            DROP FUNCTION rt_before_delete_test_func;

            DROP FUNCTION rt_before_delete_run_func;

            DROP FUNCTION rt_after_insert_run_schemas_func;
            DROP FUNCTION rt_before_delete_run_schemas_func;

            DROP FUNCTION rt_before_insert_extractor_func;
            DROP FUNCTION rt_before_update_extractor_func;
            DROP FUNCTION rt_after_update_extractor_func;
            DROP FUNCTION rt_before_delete_extractor_func;
        </sql>
    </changeSet>

    <changeSet id="73" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropTable tableName="schemaextractor"/>
    </changeSet>

    <changeSet id="74" author="jwhiting">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="transformationlog">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="level" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="timestamp" type="timestamp without time zone">
                <constraints nullable="false" />
            </column>
            <column name="testid" type="integer" />
            <column name="runid" type="integer" />
            <column name="message" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <sql>
            GRANT select, insert, delete ON TABLE transformationlog TO "${quarkus.datasource.username}";
            ALTER TABLE transformationlog ENABLE ROW LEVEL SECURITY;
            CREATE POLICY cl_all ON transformationlog FOR ALL
                USING ((exists(
                    SELECT 1 FROM test
                    WHERE test.id = testid AND has_role(test.owner)
                ) AND exists(
                    SELECT 1 FROM run
                    WHERE run.id = runid AND has_role(run.owner)
                ))
                OR has_role('horreum.system'));
            DROP POLICY dataset_insert ON dataset;
            CREATE POLICY dataset_insert ON dataset FOR INSERT
                WITH CHECK (has_role2(owner, 'uploader') OR has_upload_token(testid) OR has_role('horreum.system'));
        </sql>
    </changeSet>

    <changeSet id="75" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <!-- This operation can take a long time - we don't want to execute in sync with schema update -->
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                IF OLD.uri IS NULL OR OLD.uri != NEW.uri THEN
                    PERFORM pg_notify('new_or_updated_schema', NEW.id::text);
                END IF;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <renameColumn tableName="run_schemas" oldColumnName="prefix" newColumnName="key"/>
        <addColumn tableName="run_schemas">
            <column name="type" type="integer" />
        </addColumn>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_func() RETURNS TRIGGER AS $$
            BEGIN
                WITH rs AS (
                    SELECT 0 AS type, NULL AS key, NEW.data->>'$schema' AS uri
                    UNION SELECT 1 AS type, values.key, values.value->>'$schema' FROM jsonb_each(NEW.data) as values WHERE jsonb_typeof(NEW.data) = 'object'
                    UNION SELECT 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri FROM jsonb_array_elements(NEW.data) WHERE jsonb_typeof(NEW.data) = 'array'
                ) INSERT INTO run_schemas(runid, testid, type, key, uri, schemaid)
                    SELECT NEW.id, NEW.testid, rs.type, rs.key, rs.uri, schema.id FROM rs
                    JOIN schema ON schema.uri = rs.uri;
                DELETE FROM dataset WHERE runid = OLD.id;
                PERFORM pg_notify('calculate_datasets', NEW.id::text);
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE FUNCTION update_run_schemas(runid INTEGER) RETURNS void AS $$
            BEGIN
                WITH rs AS (
                    SELECT id, testid, 0 AS type, NULL AS key, data->>'$schema' AS uri FROM run WHERE id = runid
                    UNION SELECT id, testid, 1 AS type, values.key, values.value->>'$schema' FROM run, jsonb_each(run.data) as values WHERE id = runid AND jsonb_typeof(data) = 'object'
                    UNION SELECT id, testid, 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri FROM run, jsonb_array_elements(data) WHERE id = runid AND jsonb_typeof(data) = 'array'
                ) INSERT INTO run_schemas(runid, testid, type, key, uri, schemaid)
                    SELECT rs.id, rs.testid, rs.type, rs.key, rs.uri, schema.id FROM rs
                    JOIN schema ON schema.uri = rs.uri;
                END;
            $$ LANGUAGE plpgsql VOLATILE;
        </createProcedure>
        <sql>
            DROP TRIGGER ds_after_schema_update ON schema;
            DROP FUNCTION ds_after_schema_update_func;
            CREATE POLICY schema_select_system ON schema FOR SELECT USING (has_role('horreum.system'));
            CREATE POLICY rs_system ON run_schemas USING (has_role('horreum.system'));

            <!-- We won't do a big update data = data to run the trigger because that would also
                 delete the datasets; recalculation can't be triggered from migration (app not ready yet), though -->
            DELETE FROM run_schemas;
            SELECT update_run_schemas(id) FROM run;
        </sql>
        <addNotNullConstraint tableName="run_schemas" columnName="runid"/>
        <addNotNullConstraint tableName="run_schemas" columnName="testid"/>
        <addNotNullConstraint tableName="run_schemas" columnName="schemaid"/>
        <addNotNullConstraint tableName="run_schemas" columnName="uri"/>
        <addNotNullConstraint tableName="run_schemas" columnName="type"/>
    </changeSet>

    <changeSet id="76" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <modifyDataType tableName="transformationlog" columnName="timestamp" newDataType="timestamptz"/>
    </changeSet>

    <changeSet id="77" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <!-- We have accidentally duplicated ViewComponent.render and Label.function - let's remove that from label
             as in some instances we expect to get 3 parameters to the ViewComponent.render -->
        <sql>
            SELECT set_config('horreum.userroles', '', false);
            UPDATE label SET function = NULL WHERE id IN (
                SELECT label.id FROM viewcomponent
                    JOIN label ON render = function and json_contains(viewcomponent.labels, label.name)
            );
            SELECT set_config('horreum.userroles', NULL, false);
        </sql>
        <!-- Let's split run_schemas update and notifications into independent functions for easier maintenance -->
        <createProcedure>
            CREATE OR REPLACE FUNCTION rs_after_run_update() RETURNS TRIGGER AS $$
            BEGIN
                WITH rs AS (
                    SELECT 0 AS type, NULL AS key, NEW.data->>'$schema' AS uri
                    UNION SELECT 1 AS type, values.key, values.value->>'$schema' FROM jsonb_each(NEW.data) as values WHERE jsonb_typeof(NEW.data) = 'object'
                    UNION SELECT 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri FROM jsonb_array_elements(NEW.data) WHERE jsonb_typeof(NEW.data) = 'array'
                ) INSERT INTO run_schemas(runid, testid, type, key, uri, schemaid)
                    SELECT NEW.id, NEW.testid, rs.type, rs.key, rs.uri, schema.id FROM rs
                        JOIN schema ON schema.uri = rs.uri;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION after_run_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset WHERE runid = OLD.id;
                PERFORM pg_notify('calculate_datasets', NEW.id::text || ';' || (OLD.id IS NOT NULL)::text);
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER rs_after_run_update AFTER INSERT OR UPDATE OF data ON run FOR EACH ROW EXECUTE FUNCTION rs_after_run_update();
        </sql>
        <!-- These indexes prevent sequential scan on dataset_schemas which causes conflicts with many concurrent operations -->
        <createIndex tableName="dataset_schemas" indexName="ds_datasets">
            <column name="dataset_id" />
        </createIndex>
        <createIndex tableName="dataset_schemas" indexName="ds_schemas">
            <column name="schema_id" />
        </createIndex>
    </changeSet>

    <changeSet id="78" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            <!-- This is invoked as deferred trigger and horreum.userroles is already unset
                 - we can pass it to the trigger using a non-readable column -->
            CREATE OR REPLACE FUNCTION recalc_fingerprint() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM set_config('horreum.userroles', NEW.roles, true);
                WITH fps AS (
                    SELECT jsonb_object_agg(label.name, lv.value) FILTER (WHERE label.name IS NOT NULL) AS fingerprint FROM test
                    LEFT JOIN label ON test.fingerprint_labels ? label.name
                    LEFT JOIN label_values lv ON label.id = lv.label_id
                    WHERE test.id = (SELECT testid FROM dataset WHERE id = NEW.dataset_id) AND lv.dataset_id = NEW.dataset_id
                ) INSERT INTO fingerprint(dataset_id, fingerprint)
                    SELECT NEW.dataset_id, fingerprint FROM fps WHERE fingerprint IS NOT NULL;
                DELETE FROM fingerprint_recalc_queue WHERE dataset_id = NEW.dataset_id;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="79" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <!-- Variable and label functions are duplicated -->
        <sql>
            UPDATE variable SET calculation = NULL WHERE id IN (
                SELECT variable.id FROM variable JOIN label
                    ON variable.name = label.name
                    AND variable.calculation = label.function
                    AND (SELECT value#>>'{}' FROM jsonb_array_elements(labels) LIMIT 1)::text = label.name
            );
        </sql>
    </changeSet>

    <changeSet id="80" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            CREATE POLICY test_select_system ON test FOR SELECT USING (has_role('horreum.system'));
            CREATE POLICY test_token_select_system ON test_token FOR SELECT USING (has_role('horreum.system'));
            CREATE POLICY view_select_system ON view FOR SELECT USING (has_role('horreum.system'));
        </sql>
    </changeSet>

    <changeSet id="81" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            SELECT setval('label_id_seq', (SELECT MAX(id) FROM label));
        </sql>
        <!-- Forgot to remove this earlier -->
        <dropColumn tableName="test" columnName="tagscalculation" />
    </changeSet>

    <changeSet id="82" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="reportlog">
            <column name="id" type="int8">
                <constraints primaryKey="true"/>
            </column>
            <column name="report_id" type="integer">
                <constraints nullable="false" foreignKeyName="tablereport_id" referencedTableName="tablereport" referencedColumnNames="id"/>
            </column>
            <column name="timestamp" type="timestamptz">
                <constraints nullable="false"/>
            </column>
            <column name="level" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="message" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <sql>
            GRANT select, insert, delete, update ON TABLE reportlog TO "${quarkus.datasource.username}";
            CREATE POLICY rl_select ON reportlog FOR SELECT
                USING (exists(SELECT 1 FROM tablereport tr
                    JOIN tablereportconfig trc ON tr.config_id = trc.id
                    JOIN test ON test.id = trc.testid
                    WHERE trc.id = report_id AND can_view2(test.access, test.owner)));
            CREATE POLICY rl_insert ON reportlog FOR INSERT
                WITH CHECK (has_role2((SELECT test.owner FROM tablereport tr
                    JOIN tablereportconfig trc ON tr.config_id = trc.id
                    JOIN test ON test.id = trc.testid
                    WHERE trc.id = report_id), 'tester'));
            CREATE POLICY rl_delete ON reportlog FOR DELETE
                USING (has_role2((SELECT test.owner FROM tablereport tr
                    JOIN tablereportconfig trc ON tr.config_id = trc.id
                    JOIN test ON test.id = testid
                    WHERE trc.id = report_id), 'tester'));
        </sql>
    </changeSet>

    <changeSet id="83" author="rvansa">
        <sql>
            CREATE TYPE extractor AS (name text, jsonpath jsonpath, isarray boolean);
        </sql>
    </changeSet>

    <changeSet id="84" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION recalc_dataset_view() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM set_config('horreum.userroles', NEW.roles, true);
                <!-- Make sure we won't conflict with existing view -->
                DELETE FROM dataset_view WHERE dataset_id = NEW.dataset_id AND (NEW.view_id IS NULL OR NEW.view_id = view_id);
                WITH view_agg AS (
                    SELECT vc.view_id, vc.id as vcid, array_agg(DISTINCT label.id) as label_ids, jsonb_object_agg(label.name, lv.value) as value FROM dataset_schemas ds
                    JOIN label ON label.schema_id = ds.schema_id
                    JOIN viewcomponent vc ON vc.labels ? label.name
                    JOIN label_values lv ON lv.label_id = label.id
                    WHERE ds.dataset_id = NEW.dataset_id
                        AND (NEW.view_id IS NULL OR NEW.view_id = vc.view_id)
                        AND vc.view_id IN (SELECT view.id FROM view JOIN dataset ON view.test_id = dataset.testid WHERE dataset.id = NEW.dataset_id)
                    GROUP BY vc.view_id, vcid
                ) INSERT INTO dataset_view (dataset_id, view_id, label_ids, value)
                    SELECT NEW.dataset_id, view_id, array_agg(DISTINCT label_id), jsonb_object_agg(vcid, value) FROM view_agg, unnest(label_ids) as label_id
                    GROUP BY view_id;
                DELETE FROM view_recalc_queue WHERE dataset_id = NEW.dataset_id AND (view_id IS NULL OR view_id = NEW.view_id);
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <!-- Delete those values for non-existent view/dataset combinations -->
        <sql>
            DELETE FROM dataset_view USING (
                SELECT dv.dataset_id, dv.view_id FROM dataset_view dv
                JOIN view ON dv.view_id = view.id
                JOIN dataset ON dataset.id = dv.dataset_id
                WHERE dataset.testid != view.test_id
            ) AS extra WHERE dataset_view.dataset_id = extra.dataset_id AND dataset_view.view_id = extra.view_id;
        </sql>
    </changeSet>

    <changeSet id="85" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION dsv_after_vc_update_func() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset_view WHERE view_id = OLD.view_id OR view_id = NEW.view_id;
                INSERT INTO view_recalc_queue(dataset_id, view_id, roles)
                    SELECT dataset.id, NEW.view_id, current_setting('horreum.userroles', true) FROM dataset
                    WHERE testid = (
                        SELECT test_id FROM view WHERE view.id = NEW.view_id
                    ) ON CONFLICT DO NOTHING;
            RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>

    <changeSet id="86" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="run_validationerrors">
            <column name="run_id" type="integer">
                <constraints nullable="false" foreignKeyName="validationerrors_runid" referencedTableName="run" referencedColumnNames="id" />
            </column>
            <column name="schema_id" type="integer">
                <constraints nullable="false" foreignKeyName="validationerrors_schemaid" referencedTableName="schema" referencedColumnNames="id" />
            </column>
            <column name="error" type="jsonb">
                <constraints nullable="false" />
            </column>
        </createTable>
        <createIndex tableName="run_validationerrors" indexName="validationerrors_by_run_id">
            <column name="run_id" />
        </createIndex>
        <createTable tableName="dataset_validationerrors">
            <column name="dataset_id" type="integer">
                <constraints nullable="false" foreignKeyName="validationerrors_datasetid" referencedTableName="dataset" referencedColumnNames="id" />
            </column>
            <column name="schema_id" type="integer">
                <constraints nullable="false" foreignKeyName="validationerrors_schemaid" referencedTableName="schema" referencedColumnNames="id" />
            </column>
            <column name="error" type="jsonb">
                <constraints nullable="false" />
            </column>
        </createTable>
        <createIndex tableName="dataset_validationerrors" indexName="validationerrors_by_dataset_id">
            <column name="dataset_id" />
        </createIndex>
        <createProcedure>
            CREATE OR REPLACE FUNCTION validate_run_data() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_validationerrors WHERE run_id = NEW.id;
                PERFORM pg_notify('validate_run_data', NEW.id::text );
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION validate_dataset_data() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset_validationerrors WHERE dataset_id = NEW.id;
                PERFORM pg_notify('validate_dataset_data', NEW.id::text );
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION revalidate_all() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset_validationerrors WHERE schema_id = NEW.id;
                DELETE FROM run_validationerrors WHERE schema_id = NEW.id;
                PERFORM pg_notify('revalidate_all', NEW.id::text );
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION delete_run_validations() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_validationerrors WHERE run_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION delete_dataset_validations() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM dataset_validationerrors WHERE dataset_id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_schema_update_func() RETURNS TRIGGER AS $$
            BEGIN
                IF OLD.uri != NEW.uri THEN
                    DELETE FROM run_schemas WHERE schemaid = OLD.id;
                    DELETE FROM dataset_schemas WHERE schema_id = OLD.id;
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            GRANT select, insert, delete ON TABLE run_validationerrors, dataset_validationerrors TO "${quarkus.datasource.username}";
            CREATE POLICY rve_select ON run_validationerrors FOR SELECT
                USING (exists(SELECT 1 FROM run WHERE id = run_id AND can_view(access, owner, token)));
            CREATE POLICY rve_insert ON run_validationerrors FOR INSERT
                WITH CHECK (has_role('horreum.system'));
            CREATE POLICY rve_delete ON run_validationerrors FOR DELETE
                USING (has_role2((SELECT owner FROM run WHERE run.id = run_id), 'tester'));
            CREATE POLICY dve_select ON dataset_validationerrors FOR SELECT
                USING (exists(SELECT 1 FROM dataset WHERE id = dataset_id AND can_view2(access, owner)));
            CREATE POLICY dve_insert ON dataset_validationerrors FOR INSERT
                WITH CHECK (has_role('horreum.system'));
            CREATE POLICY dve_delete ON dataset_validationerrors FOR DELETE
                USING (has_role2((SELECT owner FROM dataset WHERE dataset.id = dataset_id), 'tester'));

            CREATE TRIGGER validate_run_data AFTER INSERT OR UPDATE OF data ON run FOR EACH ROW EXECUTE FUNCTION validate_run_data();
            CREATE TRIGGER validate_dataset_data AFTER INSERT OR UPDATE OF data ON dataset FOR EACH ROW EXECUTE FUNCTION validate_dataset_data();
            CREATE TRIGGER delete_run_validations BEFORE DELETE ON run FOR EACH ROW EXECUTE FUNCTION delete_run_validations();
            CREATE TRIGGER delete_dataset_validations BEFORE DELETE ON dataset FOR EACH ROW EXECUTE FUNCTION delete_dataset_validations();

            CREATE TRIGGER revalidate_all AFTER INSERT OR UPDATE OF schema ON schema FOR EACH ROW EXECUTE FUNCTION revalidate_all();
        </sql>
    </changeSet>

    <changeSet id="87" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION recalc_dataset_view() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM set_config('horreum.userroles', NEW.roles, true);
                DELETE FROM dataset_view WHERE dataset_id = NEW.dataset_id AND (NEW.view_id IS NULL OR NEW.view_id = view_id);
                WITH view_agg AS (
                    SELECT vc.view_id, vc.id as vcid, array_agg(DISTINCT label.id) as label_ids, jsonb_object_agg(label.name, lv.value) as value FROM dataset_schemas ds
                    JOIN label ON label.schema_id = ds.schema_id
                    JOIN viewcomponent vc ON vc.labels ? label.name
                    JOIN label_values lv ON lv.label_id = label.id AND lv.dataset_id = ds.dataset_id
                    WHERE ds.dataset_id = NEW.dataset_id
                    AND (NEW.view_id IS NULL OR NEW.view_id = vc.view_id)
                    AND vc.view_id IN (SELECT view.id FROM view JOIN dataset ON view.test_id = dataset.testid WHERE dataset.id = NEW.dataset_id)
                    GROUP BY vc.view_id, vcid
                ) INSERT INTO dataset_view (dataset_id, view_id, label_ids, value)
                    SELECT NEW.dataset_id, view_id, array_agg(DISTINCT label_id), jsonb_object_agg(vcid, value) FROM view_agg, unnest(label_ids) as label_id
                    GROUP BY view_id;
                DELETE FROM view_recalc_queue WHERE dataset_id = NEW.dataset_id AND (view_id IS NULL OR view_id = NEW.view_id);
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
    </changeSet>
    
    <changeSet id="88" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="experiment_profile">
            <column name="id" type="integer">
                <constraints primaryKey="true"/>
            </column>
            <column name="name" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="test_id" type="integer">
                <constraints nullable="false" foreignKeyName="experiment_profile_test"
                             referencedTableName="test" referencedColumnNames="id"/>
            </column>
            <column name="selector_labels" type="jsonb">
                <constraints nullable="false"/>
            </column>
            <column name="selector_filter" type="text" />
            <column name="baseline_labels" type="jsonb">
                <constraints nullable="false"/>
            </column>
            <column name="baseline_filter" type="text" />
            <column name="extra_labels" type="jsonb" />
        </createTable>
        <createTable tableName="experiment_comparisons">
            <column name="profile_id" type="integer">
                <constraints nullable="false" foreignKeyName="experiment_comparison_profile"
                             referencedTableName="experiment_profile" referencedColumnNames="id"/>
            </column>
            <column name="variable_id" type="integer">
                <constraints nullable="false" foreignKeyName="experiment_comparison_variable"
                             referencedTableName="variable" referencedColumnNames="id" />
            </column>
            <column name="model" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="config" type="jsonb">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE experiment_profile, experiment_comparisons TO "${quarkus.datasource.username}";
            ALTER TABLE experiment_profile ENABLE ROW LEVEL SECURITY;
            ALTER TABLE experiment_comparisons ENABLE ROW LEVEL SECURITY;

            CREATE POLICY ep_select ON experiment_profile FOR SELECT USING(exists(
                SELECT 1 FROM test WHERE test.id = test_id AND (can_view2(access, owner) OR has_read_token(test.id))
            ) OR has_role('horreum.system'));
            CREATE POLICY ep_insert ON experiment_profile FOR INSERT WITH CHECK (exists(
                SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester') OR has_modify_token(test.id))
            ));
            CREATE POLICY ep_update ON experiment_profile FOR UPDATE USING (exists(
                SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester') OR has_modify_token(test.id))
            ));
            CREATE POLICY ep_delete ON experiment_profile FOR DELETE USING (exists(
                SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester') OR has_modify_token(test.id))
            ));

            CREATE POLICY ec_select ON experiment_comparisons FOR SELECT USING(exists(
                SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (can_view2(access, owner) OR has_read_token(test.id))
            )  OR has_role('horreum.system'));
            CREATE POLICY ec_insert ON experiment_comparisons FOR INSERT WITH CHECK (exists(
                SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (has_role2(owner, 'tester') OR has_modify_token(test.id))
            ));
            CREATE POLICY ec_update ON experiment_comparisons FOR UPDATE USING (exists(
                SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (has_role2(owner, 'tester') OR has_modify_token(test.id))
            ));
            CREATE POLICY ec_delete ON experiment_comparisons FOR DELETE USING (exists(
                SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (has_role2(owner, 'tester') OR has_modify_token(test.id))
            ));
        </sql>
    </changeSet>

    <changeSet id="89" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <renameTable oldTableName="allowedhookprefix" newTableName="allowedsite"/>
        <renameTable oldTableName="hook" newTableName="action"/>
        <renameColumn tableName="action" oldColumnName="type" newColumnName="event"/>
        <renameColumn tableName="action" oldColumnName="target" newColumnName="test_id"/>
        <addColumn tableName="action">
            <column name="type" type="text" defaultValue="http">
                <constraints nullable="false"/>
            </column>
            <column name="config" type="jsonb" defaultValue="{}">
                <constraints nullable="false"/>
            </column>
            <column name="secrets" type="jsonb" defaultValue="{}">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <dropColumn tableName="action" columnName="active"/>
        <sql>
            UPDATE action SET config = jsonb_build_object('url', url);

            DROP POLICY hook_policy ON action;
            CREATE POLICY action_policy ON action USING (
                has_role('horreum.system') OR
                has_role('admin') OR
                has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester') OR
                has_modify_token(test_id)
            );
            DROP POLICY hook_write_check ON action;
            CREATE POLICY action_write_check ON action
                WITH CHECK (config->'url' IS NULL OR exists(SELECT 1 FROM allowedsite site WHERE left(config->>'url', length(site.prefix)) = site.prefix));
        </sql>
        <dropUniqueConstraint tableName="action" constraintName="hook_url_type_target_key"/>
        <dropColumn tableName="action" columnName="url"/>
        <dropDefaultValue tableName="action" columnName="type"/>
        <dropDefaultValue tableName="action" columnName="config"/>
        <dropDefaultValue tableName="action" columnName="secrets"/>
        <renameSequence oldSequenceName="hook_id_seq" newSequenceName="action_id_seq" />
    </changeSet>

    <changeSet id="90" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="actionlog">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="level" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="timestamp" type="timestamptz">
                <constraints nullable="false" />
            </column>
            <column name="testid" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="event" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="type" type="text" />
            <column name="message" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>
        <sql>
            GRANT select, insert, delete ON TABLE actionlog TO "${quarkus.datasource.username}";
            ALTER TABLE actionlog ENABLE ROW LEVEL SECURITY;
            CREATE POLICY al_all ON actionlog FOR ALL
            USING (has_role('horreum.system') OR has_role('admin') OR exists(
                SELECT 1 FROM test
                WHERE test.id = testid AND has_role(test.owner)
            ));
        </sql>
    </changeSet>

    <changeSet id="91" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropColumn tableName="datapoint" columnName="timestamp"/>
        <addColumn tableName="datapoint">
            <column name="timestamp" type="timestamptz" defaultValue="1970-01-01T00:00:00Z">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <sql>
            UPDATE datapoint SET timestamp = start FROM dataset WHERE dataset.id = dataset_id;
        </sql>
        <dropDefaultValue tableName="datapoint" columnName="timestamp"/>
    </changeSet>

    <changeSet id="92" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropNotNullConstraint tableName="dataset_validationerrors" columnName="schema_id"/>
    </changeSet>

    <changeSet id="93" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="action">
            <column name="run_always" type="boolean" defaultValue="false">
                <constraints nullable="false"/>
            </column>
        </addColumn>
    </changeSet>

    <changeSet id="94" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="test">
            <column name="timeline_labels" type="jsonb" />
            <column name="timeline_function" type="text" />
        </addColumn>
    </changeSet>

    <changeSet id="95" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION run_exists(runid INTEGER, test_id INTEGER) RETURNS boolean AS $$
            BEGIN
                RETURN exists(SELECT 1 FROM run WHERE run.id = runid AND run.testid = test_id);
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION is_uploader_for_run(runid INTEGER) RETURNS boolean AS $$
            BEGIN
                RETURN exists(SELECT 1 FROM run WHERE run.id = runid AND has_role2(owner, 'uploader'));
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <sql>
            ALTER POLICY rs_insert_validate ON run_schemas WITH CHECK (run_exists(runid, testid));
            ALTER POLICY rs_insert ON run_schemas
                WITH CHECK (
                    is_uploader_for_run(runid) OR
                    has_upload_token(testid) OR
                    has_role2((SELECT owner FROM schema WHERE schema.id = schemaid), 'tester')
                );
            <!-- Uploader cannot normally read schemas but we need to give him access in order to create run_schemas -->
            ALTER POLICY schema_select ON schema USING (can_view(access, owner, token) OR has_role2(owner, 'uploader'));
            ALTER POLICY ds_select ON dataset_schemas USING (has_role('horreum.system') OR (exists(
                SELECT 1 FROM dataset WHERE dataset.id = dataset_id AND can_view2(access, owner)
            )));
        </sql>
    </changeSet>

    <changeSet id="96" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="messagebus_subscriptions">
            <column name="channel" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="index" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="component" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addUniqueConstraint tableName="messagebus_subscriptions" columnNames="channel,index"/>
        <addUniqueConstraint tableName="messagebus_subscriptions" columnNames="channel,component"/>
        <createTable tableName="messagebus">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="timestamp" type="timestamptz">
                <constraints nullable="false"/>
            </column>
            <column name="channel" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="message" type="jsonb"/>
            <column name="flags" type="int">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <createSequence sequenceName="messagebus_seq"/>
        <createProcedure>
            CREATE OR REPLACE FUNCTION messagebus_delete() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM messagebus WHERE id = OLD.id;
                RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            GRANT select, insert, update, delete ON TABLE messagebus, messagebus_subscriptions TO "${quarkus.datasource.username}";
            GRANT ALL ON SEQUENCE messagebus_seq TO "${quarkus.datasource.username}";

            CREATE POLICY messagebus_insert ON messagebus FOR INSERT WITH CHECK (has_role('horreum.messagebus'));
            CREATE POLICY messagebus_all ON messagebus FOR ALL USING (has_role('horreum.system'));

            CREATE POLICY mbs_all ON messagebus_subscriptions FOR ALL USING (has_role('horreum.system'));

            CREATE TRIGGER messagebus_delete AFTER UPDATE OF flags ON messagebus FOR EACH ROW WHEN (NEW.flags = 0) EXECUTE FUNCTION messagebus_delete();
        </sql>
    </changeSet>

    <changeSet id="97" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createIndex tableName="datapoint" indexName="datapoint_datasets">
            <column name="dataset_id" />
        </createIndex>
        <createIndex tableName="datapoint" indexName="datapoint_timestamps">
            <column name="timestamp" />
        </createIndex>
        <createIndex tableName="change" indexName="change_datasets">
            <column name="dataset_id" />
        </createIndex>
    </changeSet>

    <changeSet id="98" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            ALTER POLICY cl_all_alerting ON datasetlog USING (has_role('horreum.system'));
            ALTER POLICY notificationsettings_policies ON notificationsettings USING ((has_role('horreum.system'::text) OR has_role(name)));
            ALTER POLICY run_expectation_delete ON run_expectation USING (has_role('horreum.system'));
            ALTER POLICY run_expectation_select ON run_expectation USING (has_role('horreum.system'));
            ALTER POLICY userinfo_teams_read ON userinfo_teams USING (has_role('horreum.system'));
            DROP POLICY userinfo_read ON userinfo_teams;
            CREATE POLICY userinfo_read ON userinfo FOR SELECT USING (has_role('horreum.system'));
            ALTER POLICY watch_delete ON watch USING (has_role('horreum.system'));
            ALTER POLICY watch_update ON watch USING (has_role('horreum.system'));
            ALTER POLICY watch_teams_policies ON watch_teams USING ((has_role('horreum.system') OR has_role(teams)));
            ALTER POLICY watch_users_policies ON watch_users USING ((has_role('horreum.system') OR has_role(users)));
            ALTER POLICY watch_optout_policies ON watch_optout USING ((has_role('horreum.system') OR has_role(optout)));
            ALTER POLICY variable_access ON variable USING (has_role('horreum.system'));
            ALTER POLICY rd_access ON changedetection USING (has_role('horreum.system'));
        </sql>
        <createProcedure>
            CREATE OR REPLACE FUNCTION can_view(access INTEGER, owner TEXT, token TEXT) RETURNS boolean AS $$
            BEGIN
                RETURN (
                    access = 0
                    OR (access = 1 AND has_role('viewer'))
                    OR (access = 2 AND has_role(owner) AND has_role('viewer'))
                    OR token = current_setting('horreum.token', true)
                    OR has_role('horreum.system')
                );
            END;
            $$ LANGUAGE plpgsql STABLE;
        </createProcedure>
    </changeSet>

    <changeSet id="99" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION recalc_label_values() RETURNS TRIGGER AS $$
            BEGIN
                PERFORM pg_notify('calculate_labels', (SELECT testid FROM dataset WHERE id = dataset_id)::text || ';' || dataset_id::text || ';' || NEW.label_id) FROM dataset_schemas
                WHERE dataset_schemas.schema_id = NEW.schema_id;
                DELETE FROM label_recalc_queue WHERE label_id = NEW.label_id;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <addColumn tableName="messagebus">
            <column name="testid" type="integer" defaultValue="-1">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <dropDefaultValue tableName="messagebus" columnName="testid"/>
    </changeSet>

    <changeSet id="100" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rs_after_run_trash() RETURNS TRIGGER AS $$
            BEGIN
                DELETE FROM run_schemas WHERE runid = OLD.id;
                RETURN NULL;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER rs_after_run_trash AFTER UPDATE OF trashed ON run FOR EACH ROW WHEN (NEW.trashed) EXECUTE FUNCTION rs_after_run_trash();
            CREATE TRIGGER rs_after_run_untrash AFTER UPDATE OF trashed ON run FOR EACH ROW WHEN (NOT NEW.trashed) EXECUTE FUNCTION rs_after_run_update();
            ALTER POLICY run_update ON run USING (has_role2(owner, 'tester') OR has_role('horreum.system'));
        </sql>
    </changeSet>

    <changeSet id="101" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="run">
            <column name="metadata" type="jsonb">
                <constraints checkConstraint="metadata IS NULL OR jsonb_typeof(metadata) = 'array'" />
            </column>
        </addColumn>
        <addColumn tableName="run_schemas">
            <column name="source" type="integer" defaultValue="0">
                <!-- 0 means run.data, 1 is run.metadata -->
                <constraints nullable="false" checkConstraint="source BETWEEN 0 AND 1"/>
            </column>
        </addColumn>
        <dropDefaultValue tableName="run_schemas" columnName="source"/>
        <createProcedure>
            CREATE OR REPLACE FUNCTION rs_after_run_update() RETURNS TRIGGER AS $$
            BEGIN
                WITH rs AS (
                    SELECT 0 AS type, NULL AS key, NEW.data->>'$schema' AS uri, 0 AS source
                    UNION SELECT 1 AS type, values.key, values.value->>'$schema' AS uri, 0 AS source FROM jsonb_each(NEW.data) as values WHERE jsonb_typeof(NEW.data) = 'object'
                    UNION SELECT 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri, 0 AS source FROM jsonb_array_elements(NEW.data) WHERE jsonb_typeof(NEW.data) = 'array'
                    UNION SELECT 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri, 1 AS source FROM jsonb_array_elements(NEW.metadata) WHERE NEW.metadata IS NOT NULL
                ) INSERT INTO run_schemas(runid, testid, source, type, key, uri, schemaid)
                    SELECT NEW.id, NEW.testid, rs.source, rs.type, rs.key, rs.uri, schema.id FROM rs
                    JOIN schema ON schema.uri = rs.uri;
                    RETURN NULL;
                END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <createProcedure>
            CREATE OR REPLACE FUNCTION update_run_schemas(runid INTEGER) RETURNS void AS $$
            BEGIN
                WITH rs AS (
                    SELECT id, testid, 0 AS type, NULL AS key, data->>'$schema' AS uri, 0 AS source FROM run WHERE id = runid
                    UNION SELECT id, testid, 1 AS type, values.key, values.value->>'$schema' AS uri, 0 AS source FROM run, jsonb_each(run.data) as values WHERE id = runid AND jsonb_typeof(data) = 'object'
                    UNION SELECT id, testid, 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri, 0 AS source FROM run, jsonb_array_elements(data) WHERE id = runid AND jsonb_typeof(data) = 'array'
                    UNION SELECT id, testid, 2 AS type, (row_number() OVER () - 1)::text AS key, value->>'$schema' as uri, 1 AS source FROM run, jsonb_array_elements(metadata) WHERE id = runid AND metadata IS NOT NULL
                ) INSERT INTO run_schemas(runid, testid, source, type, key, uri, schemaid)
                    SELECT rs.id, rs.testid, rs.source, rs.type, rs.key, rs.uri, schema.id FROM rs
                    JOIN schema ON schema.uri = rs.uri;
                END;
            $$ LANGUAGE plpgsql VOLATILE;
        </createProcedure>
        <sql>
            DROP TRIGGER rs_after_run_update ON run;
            CREATE TRIGGER rs_after_run_update AFTER INSERT OR UPDATE OF data, metadata ON run FOR EACH ROW EXECUTE FUNCTION rs_after_run_update();
        </sql>
    </changeSet>

    <changeSet id="102" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DELETE FROM dataset_validationerrors WHERE error->>'type' LIKE '% not defined';
            UPDATE schema SET schema = NULL WHERE jsonb_typeof(schema) != 'object';
        </sql>
    </changeSet>

    <changeSet id="103" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DELETE FROM changedetection WHERE variable_id IN (SELECT variable.id FROM variable LEFT JOIN test ON test.id = testid WHERE test.id IS NULL);
            DELETE FROM variable WHERE id IN (SELECT variable.id FROM variable LEFT JOIN test ON test.id = testid WHERE test.id IS NULL);
        </sql>
    </changeSet>

    <changeSet id="104" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropColumn tableName="test" columnName="defaultview_id" />
    </changeSet>

    <changeSet id="105" author="rvansa">
        <validCheckSum>ANY</validCheckSum>
        <dropForeignKeyConstraint baseTableName="experiment_profile" constraintName="experiment_profile_test" />
        <sql>
            <!-- Cleanup after test delete is executed using system role -->
            ALTER POLICY ep_delete ON experiment_profile USING (
                has_role('horreum.system') OR
                exists(
                    SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester') OR has_modify_token(test.id)))
            );
            ALTER POLICY ec_delete ON experiment_comparisons USING (
                has_role('horreum.system') OR
                exists(
                    SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id
                        WHERE profile_id = ep.id AND (has_role2(owner, 'tester') OR has_modify_token(test.id)))
            );
        </sql>
    </changeSet>
    <changeSet id="106" author="jwhiting">
        <validCheckSum>ANY</validCheckSum>
        <createProcedure>
            CREATE OR REPLACE FUNCTION before_variable_delete_func() RETURNS TRIGGER AS $$
            BEGIN
            DELETE FROM change WHERE variable_id = OLD.id;
            DELETE FROM datapoint WHERE variable_id = OLD.id;
            DELETE FROM experiment_comparisons WHERE variable_id = OLD.id;
            RETURN OLD;
            END;
            $$ LANGUAGE plpgsql;
        </createProcedure>
        <sql>
            CREATE TRIGGER before_variable_delete BEFORE DELETE ON variable FOR EACH ROW EXECUTE FUNCTION before_variable_delete_func();
        </sql>
    </changeSet>
    <changeSet id="107" author="stalep">
        <validCheckSum>ANY</validCheckSum>
        <createSequence sequenceName="actionlog_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="allowedsite_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="banner_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="change_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="datapoint_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="datasetlog_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="notificationsettings_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="reportcomment_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="reportcomponent_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="reportlog_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="run_expectation_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="tablereport_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="tablereportconfig_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="transformationlog_seq" startValue="1" incrementBy="50" cacheSize="1" />
        <createSequence sequenceName="changedetectionidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="experimentprofileidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="mdridgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="subscriptionidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="tokenidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="transformeridgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="variableidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="viewcomponentidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="viewidgenerator" startValue="1" incrementBy="1" cacheSize="1" />
        <sql>
            GRANT ALL ON SEQUENCE ActionLog_SEQ, AllowedSite_SEQ, Banner_SEQ, Change_SEQ, DataPoint_SEQ, DatasetLog_SEQ, NotificationSettings_SEQ, ReportComment_SEQ, ReportComponent_SEQ, ReportLog_SEQ, Run_Expectation_SEQ, TableReport_SEQ, TableReportConfig_SEQ, TransformationLog_SEQ, changeDetectionIdGenerator, experimentProfileIdGenerator, mdrIdGenerator, subscriptionIdGenerator, tokenIdGenerator, transformerIdGenerator, variableIdGenerator, viewComponentIdGenerator, viewIdGenerator TO "${quarkus.datasource.username}";
        </sql>
    </changeSet>

    <changeSet id="108" author="jwhiting">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            alter table dataset drop constraint fk_dataset_run_id;
        </sql>
        <addForeignKeyConstraint constraintName="fk_datapoint_dataset_id"
                                 baseTableName="datapoint" baseColumnNames="dataset_id"
                                 referencedTableName="dataset" referencedColumnNames="id" />
    </changeSet>
    <changeSet id="109" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <createSequence sequenceName="actionlog_id_generator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="datasetlog_id_generator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="reportlog_id_generator" startValue="1" incrementBy="1" cacheSize="1" />
        <createSequence sequenceName="transformationlog_id_generator" startValue="1" incrementBy="1" cacheSize="1" />
        <sql>
            select setval('actionlog_id_generator', (select max(id)+1 from actionlog), false);
            select setval('datasetlog_id_generator', (select max(id)+1 from datasetlog), false);
            select setval('reportlog_id_generator', (select max(id)+1 from reportlog), false);
            select setval('transformationlog_id_generator', (select max(id)+1 from transformationlog), false);
            GRANT ALL ON SEQUENCE actionlog_id_generator, datasetlog_id_generator, reportlog_id_generator, transformationlog_id_generator TO "${quarkus.datasource.username}";
        </sql>
    </changeSet>

    <changeSet id="110" author="stalep">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER IF EXISTS before_run_delete ON run;
            DROP FUNCTION IF EXISTS before_run_delete_func();
            DROP TRIGGER IF EXISTS before_schema_delete ON schema;
            DROP FUNCTION IF EXISTS before_schema_delete_func();
            DROP TRIGGER IF EXISTS mdr_before_delete ON test;
            DROP FUNCTION IF EXISTS mdr_on_test_delete();
            DROP TRIGGER IF EXISTS mdr_before_delete ON missingdata_rule;
            DROP FUNCTION IF EXISTS mdr_on_rule_delete();
            DROP TRIGGER IF EXISTS mdr_before_delete ON dataset;
            DROP FUNCTION IF EXISTS mdr_on_dataset_delete();
            DROP TRIGGER IF EXISTS rs_after_run_trash ON run;
            DROP FUNCTION IF EXISTS rs_after_run_trash();
            DROP TRIGGER IF EXISTS after_run_delete ON run;
            DROP FUNCTION IF EXISTS after_run_delete_func();
            DROP TRIGGER IF EXISTS lv_before_delete ON dataset;
            DROP FUNCTION IF EXISTS lv_before_dataset_delete_func();
            DROP TRIGGER IF EXISTS lv_before_delete ON label;
            DROP FUNCTION IF EXISTS lv_before_label_delete_func();
            DROP TRIGGER IF EXISTS lv_before_delete ON label_extractors;
            DROP FUNCTION IF EXISTS lv_before_le_delete_func();
            DROP TRIGGER IF EXISTS lv_after_delete ON label_extractors;
            DROP FUNCTION IF EXISTS lv_after_le_delete_func();
            DROP TRIGGER IF EXISTS after_run_update on run;
            DROP FUNCTION IF EXISTS after_run_update_func();
            DROP TRIGGER IF EXISTS fp_after_delete ON label_values;
            DROP FUNCTION IF EXISTS fp_after_lv_delete_func();
            DROP TRIGGER IF EXISTS fp_after_insert ON label_values;
            DROP FUNCTION IF EXISTS fp_after_lv_insert_func();
            DROP TRIGGER IF EXISTS recalc_fingerprint ON fingerprint_recalc_queue;
            DROP TRIGGER IF EXISTS fp_after_update ON test;
            DROP FUNCTION IF EXISTS fp_after_test_update_func();
            DROP TRIGGER IF EXISTS after_schema_update ON schema;
            DROP FUNCTION IF EXISTS after_schema_update_func();
            DROP TRIGGER IF EXISTS validate_run_data ON run;
            DROP FUNCTION IF EXISTS validate_run_data();
            DROP TRIGGER IF EXISTS validate_dataset_data ON dataset;
            DROP FUNCTION IF EXISTS validate_dataset_data();
            DROP TRIGGER IF EXISTS revalidate_all ON schema;
            DROP FUNCTION IF EXISTS revalidate_all();
        </sql>
    </changeSet>
    <changeSet id="111" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER IF EXISTS messagebus_delete ON messagebus;
            DROP FUNCTION IF EXISTS messagebus_delete();
            DROP POLICY if EXISTS messagebus_insert ON messagebus;
            DROP POLICY if EXISTS messagebus_all ON messagebus;
            DROP POLICY if EXISTS mbs_all ON messagebus_subscriptions;
            DROP TABLE IF EXISTS messagebus_subscriptions;
            DROP TABLE IF EXISTS messagebus;
        </sql>
    </changeSet>
    <changeSet id="112" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER IF EXISTS lv_after_update ON label_extractors;
            DROP FUNCTION IF EXISTS lv_after_le_update_func();
            DROP TRIGGER IF EXISTS lv_before_update ON label_extractors;
            DROP FUNCTION IF EXISTS lv_before_le_update_func();
            DROP TRIGGER IF EXISTS dsv_after_delete ON label_values;
            DROP FUNCTION IF EXISTS dsv_after_lv_delete_func();

--              DROP TRIGGER IF EXISTS recalc_dataset_view ON view_recalc_queue;
--              DROP FUNCTION IF EXISTS recalc_dataset_view();

            -- still need db procedure until https://hibernate.atlassian.net/browse/HHH-17314 is fixed in quarkus
            CREATE OR REPLACE PROCEDURE calc_dataset_view(datasetId bigint) AS $$
            BEGIN
            WITH view_agg AS (
                SELECT
                        vc.view_id, vc.id as vcid, array_agg(DISTINCT label.id) as label_ids, jsonb_object_agg(label.name, lv.value) as value FROM dataset_schemas ds
                JOIN label ON label.schema_id = ds.schema_id
                JOIN viewcomponent vc ON vc.labels ? label.name
                JOIN label_values lv ON lv.label_id = label.id
                WHERE ds.dataset_id = datasetId
                GROUP BY vc.view_id, vcid
            )
            INSERT INTO dataset_view (dataset_id, view_id, label_ids, value)
            SELECT datasetId, view_id, array_agg(DISTINCT label_id), jsonb_object_agg(vcid, value) FROM view_agg, unnest(label_ids) as label_id
            GROUP BY view_id;
            END
            $$ LANGUAGE plpgsql;
        </sql>
    </changeSet>
    <changeSet id="113" author="jpederse">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            CREATE INDEX label_values_label_id ON label_values (label_id);
        </sql>
    </changeSet>
    <changeSet id="114" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <addColumn tableName="fingerprint">
            <column name="fp_hash"
                    type="integer"/>
        </addColumn>
        <sql>
            CREATE INDEX fingerprint_fp_hash ON fingerprint (fp_hash);
        </sql>
    </changeSet>
    <changeSet id="115" author="barreiro">
        <validCheckSum>ANY</validCheckSum>
        <!-- remove the encryption in `has_role` in the db procedure, and we can remove `horreum.db.secret` property and the corresponding database table -->
        <dropTable tableName="dbsecret"/>
        <createProcedure>
            <!-- redefine `has_role()` to remove signing -->
            CREATE OR REPLACE FUNCTION has_role(owner TEXT) RETURNS boolean AS $$
            DECLARE
                v_userroles TEXT;
                v_role TEXT;
            BEGIN
                v_userroles := current_setting('horreum.userroles', true);

                IF v_userroles = '' OR v_userroles IS NULL THEN
                    RETURN 0;
                END IF;

                FOREACH v_role IN ARRAY regexp_split_to_array(v_userroles, ',')
                LOOP
                    IF v_role = owner THEN
                        RETURN 1;
                    END IF;
                END LOOP;
                RETURN 0;
            END;
            $$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
        </createProcedure>
        <sql>
            DROP EXTENSION pgcrypto;
        </sql>
    </changeSet>
    <changeSet id="116" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <createSequence sequenceName="backend_id_seq" startValue="10" incrementBy="1" cacheSize="1" />
        <createTable tableName="backendconfig">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="name" type="text">
                <constraints nullable="false" />
            </column>
            <column name="configuration" type="jsonb">
                <constraints nullable="false" />
            </column>
            <column name="type" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="owner" type="text">
                <constraints nullable="false" />
            </column>
            <column name="access" type="integer">
                <constraints nullable="false" />
            </column>
        </createTable>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE backendconfig TO "${quarkus.datasource.username}";
            GRANT ALL ON SEQUENCE backend_id_seq TO "${quarkus.datasource.username}";
        </sql>
        <insert tableName="backendconfig">
            <column name="id" value="1" />
            <column name="name" value="Postgres - Default" />
            <column name="owner" value="horreum.system" />
            <column name="type" value="0" />
            <column name="access" value="0" />
            <column name="configuration" value='{ "builtIn": true }' />
        </insert>
        <addColumn tableName="test">
            <column name="backendconfig_id" type="integer" />
        </addColumn>
        <sql>
            update test set backendconfig_id = 1 WHERE backendconfig_id IS NULL;
        </sql>
    </changeSet>
    <changeSet id="117" author="stalep">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            DROP TRIGGER IF EXISTS before_run_update ON run;
            DROP FUNCTION IF EXISTS before_run_update_func();
        </sql>
    </changeSet>
    <changeSet id="118" author="barreiro">
        <validCheckSum>ANY</validCheckSum>
        <!-- Extend userinfo for support of internal security -->
        <addColumn tableName="userinfo">
            <column name="password" type="text"/>
            <column name="email" type="text"/>
            <column name="first_name" type="text"/>
            <column name="last_name" type="text"/>
        </addColumn>
        <createSequence sequenceName="team_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="team">
            <column name="id" type="integer">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="team_name" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <delete tableName="userinfo_teams"/>
        <dropColumn tableName="userinfo_teams" columnName="team"/>
        <addColumn tableName="userinfo_teams">
            <column name="team_id" type="integer">
                <constraints nullable="false"/>
            </column>
            <column name="team_role" type="text">
                <constraints nullable="false"/>
            </column>
        </addColumn>
        <addForeignKeyConstraint constraintName="fk_userinfo_teams"
                                 baseTableName="userinfo_teams" baseColumnNames="team_id"
                                 referencedTableName="team" referencedColumnNames="id"/>
        <addUniqueConstraint tableName="userinfo_teams" columnNames="username,team_id,team_role"/>
        <createTable tableName="userinfo_roles">
            <column name="username" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="role" type="text">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_userinfo_roles"
                                 baseTableName="userinfo_roles" baseColumnNames="username"
                                 referencedTableName="userinfo" referencedColumnNames="username"/>
        <addUniqueConstraint tableName="userinfo_roles" columnNames="username,role"/>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE team, userinfo_roles TO "${quarkus.datasource.username}";
            GRANT ALL ON SEQUENCE team_id_seq TO "${quarkus.datasource.username}";
            ALTER TABLE userinfo_roles ENABLE ROW LEVEL SECURITY;
            CREATE POLICY userinfo_roles_read ON userinfo_roles USING (has_role('horreum.system'));
            CREATE POLICY userinfo_roles_rw ON userinfo_roles FOR ALL USING (has_role(username));
        </sql>
    </changeSet>
    <changeSet id="119" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            UPDATE changedetection
            SET config=subquery.newConfig
                FROM (select id as newID,  jsonb_set(config, '{model}', to_jsonb(changedetection.model::text), true) as newConfig from changedetection) AS subquery
            WHERE changedetection.id=subquery.newID;
        </sql>
    </changeSet>

    <changeSet id="120" author="johara">
        <validCheckSum>ANY</validCheckSum>
        <createTable tableName="changedetectionlog">
            <column name="id" type="bigint">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="level" type="integer">
                <constraints nullable="false" />
            </column>
            <column name="timestamp" type="timestamptz">
                <constraints nullable="false" />
            </column>
            <column name="variableid" type="integer" />
            <column name="fingerprint" type="jsonb" />
            <column name="message" type="text">
                <constraints nullable="false" />
            </column>
        </createTable>

        <sql>
            GRANT select, insert, delete, update ON TABLE changedetectionlog TO "${quarkus.datasource.username}";
            ALTER TABLE changedetectionlog ENABLE ROW LEVEL SECURITY;
            CREATE POLICY cdl_all ON changedetectionlog FOR ALL
                USING (has_role('horreum.system'));
        </sql>

        <createSequence sequenceName="changedetectionlog_id_generator" startValue="1" incrementBy="1" cacheSize="1" />
        <sql>
            select setval('changedetectionlog_id_generator', (select max(id)+1 from changedetectionlog), false);
            GRANT ALL ON SEQUENCE changedetectionlog_id_generator TO "${quarkus.datasource.username}";
        </sql>
    </changeSet>
    <changeSet id="121" author="lampajr">
        <validCheckSum>ANY</validCheckSum>
        <!-- we are using actionlog_id_generator as sequence for actionlog table -->
        <dropSequence sequenceName="actionlog_seq" />
        <sql>
            <!-- reset sequences that have not been properly reset in changeset 107 -->
            select setval('allowedsite_seq', (select max(id)+50 from allowedsite), false);
            select setval('banner_seq', (select max(id)+50 from banner), false);
            select setval('change_seq', (select max(id)+50 from change), false);
            select setval('datapoint_seq', (select max(id)+50 from datapoint), false);
            select setval('datasetlog_seq', (select max(id)+50 from datasetlog), false);
            select setval('notificationsettings_seq', (select max(id)+50 from notificationsettings), false);
            select setval('reportcomment_seq', (select max(id)+50 from reportcomment), false);
            select setval('reportcomponent_seq', (select max(id)+50 from reportcomponent), false);
            select setval('reportlog_seq', (select max(id)+50 from reportlog), false);
            select setval('run_expectation_seq', (select max(id)+50 from run_expectation), false);
            select setval('tablereport_seq', (select max(id)+50 from tablereport), false);
            select setval('tablereportconfig_seq', (select max(id)+50 from tablereportconfig), false);
            select setval('transformationlog_seq', (select max(id)+50 from transformationlog), false);
            select setval('changedetectionidgenerator', (select max(id)+1 from changedetection), false);
            select setval('experimentprofileidgenerator', (select max(id)+1 from experiment_profile), false);
            select setval('mdridgenerator', (select max(id)+1 from missingdata_rule), false);
            select setval('subscriptionidgenerator', (select max(id)+1 from watch), false);
            select setval('tokenidgenerator', (select max(id)+1 from test_token), false);
            select setval('transformeridgenerator', (select max(id)+1 from transformer), false);
            select setval('variableidgenerator', (select max(id)+1 from variable), false);
            select setval('viewcomponentidgenerator', (select max(id)+1 from viewcomponent), false);
            select setval('viewidgenerator', (select max(id)+1 from view), false);
        </sql>
    </changeSet>
    <changeSet id="122" author="lampajr">
        <validCheckSum>ANY</validCheckSum>
        <createIndex tableName="dataset" indexName="dataset_testid_key">
            <column name="testid" />
        </createIndex>
        <sql>
            CREATE INDEX idx_label_filtering_partial ON label (id) WHERE filtering = TRUE;
            CREATE INDEX idx_label_metrics_partial ON label (id) WHERE metrics = TRUE;
        </sql>
    </changeSet>
    <changeSet id="123" author="barreiro">
        <validCheckSum>ANY</validCheckSum>
        <createSequence sequenceName="userinfo_apikey_id_seq" startValue="1" incrementBy="1" cacheSize="1" />
        <createTable tableName="userinfo_apikey">
            <column name="id" type="bigint"/>
            <column name="hash" type="text">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="username" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="name" type="text"/>
            <column name="type" type="smallint"/>
            <column name="revoked" type="bool"/>
            <column name="creation" type="timestamptz"/>
            <column name="access" type="timestamptz"/>
            <column name="active" type="bigint"/>
        </createTable>
        <createIndex tableName="userinfo_apikey" indexName="userinfo_apikey_hash">
            <column name="hash" />
        </createIndex>
        <addForeignKeyConstraint constraintName="fk_userinfo_apikey"
                                 baseTableName="userinfo_apikey" baseColumnNames="username"
                                 referencedTableName="userinfo" referencedColumnNames="username"/>
        <sql>
            GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE userinfo_apikey TO "${quarkus.datasource.username}";
            GRANT ALL ON SEQUENCE userinfo_apikey_id_seq TO "${quarkus.datasource.username}";
            ALTER TABLE userinfo_apikey ENABLE ROW LEVEL SECURITY;
            CREATE POLICY userinfo_apikey_read ON userinfo_apikey USING (has_role('horreum.system'));
            CREATE POLICY userinfo_apikey_rw ON userinfo_apikey FOR ALL USING (has_role(username));
        </sql>
    </changeSet>
    <changeSet id="124" author="barreiro">
        <validCheckSum>ANY</validCheckSum>
        <!-- remove test token feature -->
        <sql>
            ALTER POLICY run_insert ON run WITH CHECK (has_role2(owner, 'uploader'));
            ALTER POLICY run_select ON run USING (can_view2(access, owner) OR has_role('horreum.system'));

            ALTER POLICY rs_insert ON run_schemas WITH CHECK (is_uploader_for_run(runid) OR has_role2((SELECT owner FROM schema WHERE id = schemaid), 'tester'));
            ALTER POLICY rs_select ON run_schemas USING (exists(SELECT 1 FROM run WHERE run.id = runid AND can_view2(access, owner)));

            ALTER POLICY rve_select ON run_validationerrors USING (exists(SELECT 1 FROM run WHERE id = run_id AND can_view2(access, owner)));

            ALTER POLICY schema_select ON schema USING (can_view2(access, owner) OR has_role2(owner, 'uploader'));

            ALTER POLICY test_delete ON test USING (has_role2(owner, 'tester'));
            ALTER POLICY test_select ON test USING (can_view2(access, owner));
            ALTER POLICY test_update ON test USING (has_role2(owner, 'tester'));

            ALTER POLICY variable_delete ON variable USING (has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester'));
            ALTER POLICY variable_insert ON variable WITH CHECK (has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester'));
            ALTER POLICY variable_select ON variable USING (exists(SELECT 1 FROM test WHERE test.id = testid AND can_view2(test.access, test.owner)));
            ALTER POLICY variable_update ON variable USING (has_role2((SELECT owner FROM test WHERE test.id = testid), 'tester'));

            ALTER POLICY view_delete ON view USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));
            ALTER POLICY view_insert ON view WITH CHECK (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));
            ALTER POLICY view_select ON view USING (exists(SELECT 1 FROM test WHERE test.id = test_id AND can_view2(test.access, test.owner)));
            ALTER POLICY view_update ON view USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));

            ALTER POLICY vc_delete ON viewcomponent USING (exists(SELECT 1 FROM test JOIN view ON view.test_id = test.id WHERE view.id = view_id AND has_role2(test.owner, 'tester')));
            ALTER POLICY vc_insert ON viewcomponent WITH CHECK (exists(SELECT 1 FROM test JOIN view ON view.test_id = test.id WHERE view.id = view_id AND has_role2(test.owner, 'tester')));
            ALTER POLICY vc_select ON viewcomponent USING (exists(SELECT 1 FROM test JOIN view ON view.test_id = test.id WHERE view.id = view_id AND can_view2(test.access, test.owner)));
            ALTER POLICY vc_update ON viewcomponent USING (exists(SELECT 1 FROM test JOIN view ON view.test_id = test.id WHERE view.id = view_id AND has_role2(test.owner, 'tester')));

            ALTER POLICY ss_delete ON missingdata_rule USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));
            ALTER POLICY ss_insert ON missingdata_rule WITH CHECK (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));
            ALTER POLICY ss_select ON missingdata_rule USING (exists(SELECT 1 FROM test WHERE test.id = test_id AND can_view2(test.access, test.owner)));
            ALTER POLICY ss_update ON missingdata_rule USING (has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));

            ALTER POLICY rd_delete ON changedetection USING (exists(SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id AND (has_role2(test.owner, 'tester'))));
            ALTER POLICY rd_insert ON changedetection WITH CHECK (exists(SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id AND (has_role2(test.owner, 'tester'))));
            ALTER POLICY rd_select ON changedetection USING (exists(SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id AND (can_view2(test.access, test.owner))));
            ALTER POLICY rd_update ON changedetection USING (exists(SELECT 1 FROM test JOIN variable ON test.id = variable.testid WHERE variable.id = variable_id AND (has_role2(test.owner, 'tester'))));

            ALTER POLICY ep_delete ON experiment_profile USING (exists(SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester'))));
            ALTER POLICY ep_insert ON experiment_profile WITH CHECK (exists(SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester'))));
            ALTER POLICY ep_select ON experiment_profile USING(exists(SELECT 1 FROM test WHERE test.id = test_id AND (can_view2(access, owner)) OR has_role('horreum.system')));
            ALTER POLICY ep_update ON experiment_profile USING (exists(SELECT 1 FROM test WHERE test.id = test_id AND (has_role2(owner, 'tester'))));

            ALTER POLICY ec_delete ON experiment_comparisons USING (exists(SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (has_role2(owner, 'tester'))));
            ALTER POLICY ec_insert ON experiment_comparisons WITH CHECK (exists(SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (has_role2(owner, 'tester'))));
            ALTER POLICY ec_select ON experiment_comparisons USING (exists(SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (can_view2(access, owner)) OR has_role('horreum.system')));
            ALTER POLICY ec_update ON experiment_comparisons USING (exists(SELECT 1 FROM experiment_profile ep JOIN test ON test.id = test_id WHERE profile_id = ep.id AND (has_role2(owner, 'tester'))));

            ALTER POLICY action_policy ON action USING (has_role('horreum.system') OR has_role('admin') OR has_role2((SELECT owner FROM test WHERE test.id = test_id), 'tester'));

            ALTER POLICY dataset_insert ON dataset WITH CHECK (has_role2(owner, 'uploader') OR has_role('horreum.system'));

            DROP FUNCTION can_view; <!-- calls to this function are replaced with existing `can_view2` -->
            DROP FUNCTION has_read_token;
            DROP FUNCTION has_modify_token;
            DROP FUNCTION has_upload_token;
            DROP FUNCTION auth_suffix;
        </sql>
        <dropColumn tableName="run" columnName="token" />
        <dropColumn tableName="schema" columnName="token" />
        <dropTable tableName="test_token" />
    </changeSet>
    <changeSet id="125" author="lampajr">
        <validCheckSum>ANY</validCheckSum>
        <sql>
            -- still need db procedure until https://hibernate.atlassian.net/browse/HHH-17314 is fixed in quarkus
            CREATE OR REPLACE PROCEDURE calc_dataset_view(datasetId bigint) AS $$
            BEGIN
            WITH view_agg AS (
                SELECT
                    vc.view_id, vc.id as vcid, array_agg(DISTINCT label.id) as label_ids, jsonb_object_agg(label.name, lv.value) as value FROM dataset_schemas ds
                JOIN label ON label.schema_id = ds.schema_id
                JOIN viewcomponent vc ON vc.labels ? label.name
                JOIN label_values lv ON lv.label_id = label.id AND lv.dataset_id = ds.dataset_id
                WHERE ds.dataset_id = datasetId
                    AND vc.view_id IN (SELECT view.id FROM view JOIN dataset ON view.test_id = dataset.testid WHERE dataset.id = datasetId)
                GROUP BY vc.view_id, vcid
            )
            INSERT INTO dataset_view (dataset_id, view_id, label_ids, value)
            SELECT datasetId, view_id, array_agg(DISTINCT label_id), jsonb_object_agg(vcid, value) FROM view_agg, unnest(label_ids) as label_id
            GROUP BY view_id;
            END
            $$ LANGUAGE plpgsql;
        </sql>
    </changeSet>
</databaseChangeLog>




© 2015 - 2025 Weber Informatics LLC | Privacy Policy