diff options
author | Guillaume Jambet <guillaume.jambet@sonarsource.com> | 2018-02-13 15:42:38 +0100 |
---|---|---|
committer | Guillaume Jambet <guillaume.jambet@gmail.com> | 2018-03-01 15:21:05 +0100 |
commit | 8edd835fae1987fcd61b045a6c79b6e8ed8cf197 (patch) | |
tree | 1c5bd01d7029f07d0a0ceb04334bba361cb21d39 /server/sonar-db-migration | |
parent | cc4b808264013326716a0ce7ef67eb1bd4452a6f (diff) | |
download | sonarqube-8edd835fae1987fcd61b045a6c79b6e8ed8cf197.tar.gz sonarqube-8edd835fae1987fcd61b045a6c79b6e8ed8cf197.zip |
SONAR-10345 Migrate webhooks from PROPERTIES table to WEBHOOKS table.
Diffstat (limited to 'server/sonar-db-migration')
5 files changed, 524 insertions, 1 deletions
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71.java index d786fe7f4af..abe4af30f23 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71.java @@ -41,6 +41,7 @@ public class DbVersion71 implements DbVersion { .add(2011, "Drop table PROJECT_LINKS", DropTableProjectLinks.class) .add(2012, "Rename table PROJECT_LINKS2 to PROJECT_LINKS", RenameTableProjectLinks2ToProjectLinks.class) .add(2013, "Create WEBHOOKS Table", CreateWebhooksTable.class) + .add(2014, "Migrate webhooks from SETTINGS table to WEBHOOKS table", MigrateWebhooksToWebhooksTable.class) ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java new file mode 100644 index 00000000000..5e133925daa --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java @@ -0,0 +1,241 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v71; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuidProvider; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +public class MigrateWebhooksToWebhooksTable extends DataChange { + + private static final Logger LOGGER = Loggers.get(MigrateWebhooksToWebhooksTable.class); + + private DefaultOrganizationUuidProvider defaultOrganizationUuidProvider; + private UuidFactory uuidFactory; + + public MigrateWebhooksToWebhooksTable(Database db, DefaultOrganizationUuidProvider defaultOrganizationUuidProvider, UuidFactory uuidFactory) { + super(db); + this.defaultOrganizationUuidProvider = defaultOrganizationUuidProvider; + this.uuidFactory = uuidFactory; + } + + @Override + public void execute(Context context) throws SQLException { + Map<String, PropertyRow> rows = context + .prepareSelect("select id, prop_key, resource_id, text_value, created_at from properties where prop_key like 'sonar.webhooks%'") + .list(row -> new PropertyRow( + row.getLong(1), + row.getString(2), + row.getLong(3), + row.getString(4), + row.getLong(5))) + .stream() + .collect(toMap(PropertyRow::key, Function.identity())); + + if (!rows.isEmpty()) { + migrateGlobalWebhooks(context, rows); + migrateProjectsWebhooks(context, rows); + context + .prepareUpsert("delete from properties where prop_key like 'sonar.webhooks.global%' or prop_key like 'sonar.webhooks.project%'") + .execute() + .commit(); + } + } + + private void migrateProjectsWebhooks(Context context, Map<String, PropertyRow> properties) throws SQLException { + PropertyRow index = properties.get("sonar.webhooks.project"); + if (index != null) { + // can't lambda due to checked exception. + for (Webhook webhook : extractProjectWebhooksFrom(context, properties, index.value().split(","))) { + insert(context, webhook); + } + } + } + + private void migrateGlobalWebhooks(Context context, Map<String, PropertyRow> properties) throws SQLException { + PropertyRow index = properties.get("sonar.webhooks.global"); + if (index != null) { + // can't lambda due to checked exception. + for (Webhook webhook : extractGlobalWebhooksFrom(context, properties, index.value().split(","))) { + insert(context, webhook); + } + } + } + + private void insert(Context context, Webhook webhook) throws SQLException { + if (webhook.isValid()) { + context.prepareUpsert("insert into webhooks (uuid, name, url, organization_uuid, project_uuid, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)") + .setString(1, uuidFactory.create()) + .setString(2, webhook.name()) + .setString(3, webhook.url()) + .setString(4, webhook.organisationUuid()) + .setString(5, webhook.projectUuid()) + .setLong(6, webhook.createdAt()) + .setLong(7, webhook.createdAt()) + .execute() + .commit(); + } else { + LOGGER.info("Unable to migrate inconsistent webhook (entry deleted from PROPERTIES) : " + webhook); + } + } + + private List<Webhook> extractGlobalWebhooksFrom(Context context, Map<String, PropertyRow> properties, String[] values) throws SQLException { + String defaultOrganizationUuid = defaultOrganizationUuidProvider.get(context); + return Arrays.stream(values) + .map(value -> new Webhook( + properties.get("sonar.webhooks.global." + value + ".name"), + properties.get("sonar.webhooks.global." + value + ".url"), + defaultOrganizationUuid, null)) + .collect(toList()); + } + + private static List<Webhook> extractProjectWebhooksFrom(Context context, Map<String, PropertyRow> properties, String[] values) throws SQLException { + List<Webhook> webhooks = new ArrayList<>(); + for (String value : values) { + PropertyRow name = properties.get("sonar.webhooks.project." + value + ".name"); + PropertyRow url = properties.get("sonar.webhooks.project." + value + ".url"); + webhooks.add(new Webhook(name, url, null, projectUuidOf(context, name))); + } + return webhooks; + } + + private static String projectUuidOf(Context context, PropertyRow row) throws SQLException { + return context + .prepareSelect("select uuid from projects where id = ?") + .setLong(1, row.resourceId()) + .list(row1 -> row1.getString(1)).stream().findFirst().orElse(null); + } + + private static class PropertyRow { + + private final Long id; + private final String key; + private final Long resourceId; + private final String value; + private final Long createdAt; + + public PropertyRow(long id, String key, Long resourceId, String value, Long createdAt) { + this.id = id; + this.key = key; + this.resourceId = resourceId; + this.value = value; + this.createdAt = createdAt; + } + + public Long id() { + return id; + } + + public String key() { + return key; + } + + public Long resourceId() { + return resourceId; + } + + public String value() { + return value; + } + + public Long createdAt() { + return createdAt; + } + + @Override + public String toString() { + return "{" + + "id=" + id + + ", key='" + key + '\'' + + ", resourceId=" + resourceId + + ", value='" + value + '\'' + + ", createdAt=" + createdAt + + '}'; + } + } + + private static class Webhook { + + private final PropertyRow name; + private final PropertyRow url; + private String organisationUuid; + private String projectUuid; + + public Webhook(@Nullable PropertyRow name, @Nullable PropertyRow url, @Nullable String organisationUuid, @Nullable String projectUuid) { + this.name = name; + this.url = url; + this.organisationUuid = organisationUuid; + this.projectUuid = projectUuid; + } + + public String name() { + return name.value(); + } + + public String url() { + return url.value(); + } + + public String organisationUuid() { + return organisationUuid; + } + + public String projectUuid() { + return projectUuid; + } + + public Long createdAt() { + return name.createdAt(); + } + + public boolean isValid() { + return name != null && url != null && name() != null && url() != null && (organisationUuid() != null || projectUuid() != null) && createdAt() != null; + } + + @Override + public String toString() { + final StringBuilder s = new StringBuilder().append("Webhook{").append("name=").append(name); + if (name != null) { + s.append(name.toString()); + } + s.append(", url=").append(url); + if (url != null) { + s.append(url.toString()); + } + s.append(", organisationUuid='").append(organisationUuid).append('\'') + .append(", projectUuid='").append(projectUuid).append('\'').append('}'); + return s.toString(); + } + } + +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71Test.java index bc893e7bd4c..70a599f9f6e 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71Test.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71Test.java @@ -36,7 +36,7 @@ public class DbVersion71Test { @Test public void verify_migration_count() { - verifyMigrationCount(underTest, 14); + verifyMigrationCount(underTest, 15); } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java new file mode 100644 index 00000000000..6145fa74e0f --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java @@ -0,0 +1,205 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v71; + +import java.sql.SQLException; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.core.util.UuidFactory; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuidProviderImpl; + +import static java.lang.Long.parseLong; +import static java.lang.String.valueOf; +import static org.apache.commons.lang.RandomStringUtils.randomNumeric; +import static org.assertj.core.api.Assertions.assertThat; + +public class MigrateWebhooksToWebhooksTableTest { + + @Rule + public final CoreDbTester dbTester = CoreDbTester.createForSchema(MigrateWebhooksToWebhooksTableTest.class, "migrate_webhooks.sql"); + private static final long NOW = 1_500_000_000_000L; + private System2 system2 = new TestSystem2().setNow(NOW); + private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); + + private MigrateWebhooksToWebhooksTable underTest = new MigrateWebhooksToWebhooksTable(dbTester.database(), new DefaultOrganizationUuidProviderImpl(), uuidFactory); + + @Test + public void should_do_nothing_if_no_webhooks() throws SQLException { + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0); + } + + @Test + public void should_migrate_one_global_webhook() throws SQLException { + String uuid = insertDefaultOrganization(); + insertProperty("sonar.webhooks.global", "1", null, system2.now()); + insertProperty("sonar.webhooks.global.1.name", "a webhook", null, system2.now()); + insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, system2.now()); + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1); + + Map<String, Object> migrated = dbTester.selectFirst("select * from webhooks"); + assertThat(migrated.get("UUID")).isNotNull(); + assertThat(migrated.get("NAME")).isEqualTo("a webhook"); + assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); + assertThat(migrated.get("PROJECT_UUID")).isNull(); + assertThat(migrated.get("ORGANIZATION_UUID")).isEqualTo(uuid); + assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); + assertThat(migrated.get("CREATED_AT")).isEqualTo(system2.now()); + assertThat(migrated.get("UPDATED_AT")).isEqualTo(system2.now()); + } + + @Test + public void should_migrate_one_project_webhook() throws SQLException { + String organization = insertDefaultOrganization(); + String projectId = "156"; + String projectUuid = UuidFactoryFast.getInstance().create(); + ; + insertProject(organization, projectId, projectUuid); + + insertProperty("sonar.webhooks.project", "1", projectId, system2.now()); + insertProperty("sonar.webhooks.project.1.name", "a webhook", projectId, system2.now()); + insertProperty("sonar.webhooks.project.1.url", "http://webhook.com", projectId, system2.now()); + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1); + + Map<String, Object> migrated = dbTester.selectFirst("select * from webhooks"); + assertThat(migrated.get("UUID")).isNotNull(); + assertThat(migrated.get("NAME")).isEqualTo("a webhook"); + assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); + assertThat(migrated.get("PROJECT_UUID")).isEqualTo(projectUuid); + assertThat(migrated.get("ORGANIZATION_UUID")).isNull(); + assertThat(migrated.get("URL")).isEqualTo("http://webhook.com"); + assertThat(migrated.get("CREATED_AT")).isEqualTo(system2.now()); + assertThat(migrated.get("UPDATED_AT")).isEqualTo(system2.now()); + } + + @Test + public void should_migrate_global_webhooks() throws SQLException { + insertDefaultOrganization(); + insertProperty("sonar.webhooks.global", "1,2", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.1.name", "a webhook", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.2.url", "http://webhook.com", null, parseLong(randomNumeric(7))); + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2); + } + + @Test + public void should_migrate_only_valid_webhooks() throws SQLException { + insertDefaultOrganization(); + insertProperty("sonar.webhooks.global", "1,2,3,4", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.3.name", "a webhook", null, parseLong(randomNumeric(7))); + insertProperty("sonar.webhooks.global.3.url", "http://webhook.com", null, parseLong(randomNumeric(7))); + // nothing for 4 + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1); + } + + @Test + public void should_migrate_project_webhooks() throws SQLException { + String organization = insertDefaultOrganization(); + String projectId = "156"; + String projectUuid = UuidFactoryFast.getInstance().create(); + ; + insertProject(organization, projectId, projectUuid); + + insertProperty("sonar.webhooks.project", "1,2", projectId, system2.now()); + insertProperty("sonar.webhooks.project.1.name", "a webhook", projectId, system2.now()); + insertProperty("sonar.webhooks.project.1.url", "http://webhook.com", projectId, system2.now()); + insertProperty("sonar.webhooks.project.2.name", "another webhook", projectId, system2.now()); + insertProperty("sonar.webhooks.project.2.url", "http://webhookhookhook.com", projectId, system2.now()); + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2); + } + + @Test + public void should_not_migrate_more_than_10_webhooks_per_project() throws SQLException { + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0); + } + + @Test + public void should_not_migrate_more_than_10_global_webhooks() throws SQLException { + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0); + } + + private void insertProperty(String key, @Nullable String value, @Nullable String resourceId, Long date) { + dbTester.executeInsert("PROPERTIES", + "id", randomNumeric(7), + "prop_key", valueOf(key), + "text_value", value, + "is_empty", value.isEmpty() ? true : false, + "resource_id", resourceId == null ? null : valueOf(resourceId), + "created_at", valueOf(date)); + } + + private String insertDefaultOrganization() { + String uuid = UuidFactoryFast.getInstance().create(); + dbTester.executeInsert( + "INTERNAL_PROPERTIES", + "KEE", "organization.default", + "IS_EMPTY", "false", + "TEXT_VALUE", uuid); + return uuid; + } + + private void insertProject(String organizationUuid, String projectId, String projectUuid) { + dbTester.executeInsert( + "PROJECTS", + "ID", projectId, + "ORGANIZATION_UUID", organizationUuid, + "UUID", projectUuid); + } +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql new file mode 100644 index 00000000000..d551e6c37ea --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql @@ -0,0 +1,76 @@ +CREATE TABLE "INTERNAL_PROPERTIES" ( + "KEE" VARCHAR(20) NOT NULL PRIMARY KEY, + "IS_EMPTY" BOOLEAN NOT NULL, + "TEXT_VALUE" VARCHAR(4000), + "CLOB_VALUE" CLOB, + "CREATED_AT" BIGINT +); + + +CREATE TABLE "PROJECTS" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "ORGANIZATION_UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(400), + "UUID" VARCHAR(50) NOT NULL, +-- "UUID_PATH" VARCHAR(1500) NOT NULL, +-- "ROOT_UUID" VARCHAR(50) NOT NULL, +-- "PROJECT_UUID" VARCHAR(50) NOT NULL, + "MODULE_UUID" VARCHAR(50), + "MODULE_UUID_PATH" VARCHAR(1500), + "MAIN_BRANCH_PROJECT_UUID" VARCHAR(50), + "NAME" VARCHAR(2000), + "DESCRIPTION" VARCHAR(2000), +-- "PRIVATE" BOOLEAN NOT NULL, + "TAGS" VARCHAR(500), +-- "ENABLED" BOOLEAN NOT NULL DEFAULT TRUE, + "SCOPE" VARCHAR(3), + "QUALIFIER" VARCHAR(10), + "DEPRECATED_KEE" VARCHAR(400), + "PATH" VARCHAR(2000), + "LANGUAGE" VARCHAR(20), + "COPY_COMPONENT_UUID" VARCHAR(50), + "LONG_NAME" VARCHAR(2000), + "DEVELOPER_UUID" VARCHAR(50), + "CREATED_AT" TIMESTAMP, + "AUTHORIZATION_UPDATED_AT" BIGINT, + "B_CHANGED" BOOLEAN, + "B_COPY_COMPONENT_UUID" VARCHAR(50), + "B_DESCRIPTION" VARCHAR(2000), + "B_ENABLED" BOOLEAN, + "B_UUID_PATH" VARCHAR(1500), + "B_LANGUAGE" VARCHAR(20), + "B_LONG_NAME" VARCHAR(500), + "B_MODULE_UUID" VARCHAR(50), + "B_MODULE_UUID_PATH" VARCHAR(1500), + "B_NAME" VARCHAR(500), + "B_PATH" VARCHAR(2000), + "B_QUALIFIER" VARCHAR(10) +); + +CREATE TABLE "PROPERTIES" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "PROP_KEY" VARCHAR(512) NOT NULL, + "RESOURCE_ID" INTEGER, + "USER_ID" INTEGER, + "IS_EMPTY" BOOLEAN NOT NULL, + "TEXT_VALUE" VARCHAR(4000), + "CLOB_VALUE" CLOB, + "CREATED_AT" BIGINT +); +CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES" ("PROP_KEY"); + + +CREATE TABLE "WEBHOOKS" ( + "UUID" VARCHAR(40) NOT NULL PRIMARY KEY, + "NAME" VARCHAR(100) NOT NULL, + "URL" VARCHAR(2000) NOT NULL, + "ORGANIZATION_UUID" VARCHAR(40), + "PROJECT_UUID" VARCHAR(40), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT +); +CREATE UNIQUE INDEX "PK_WEBHOOKS" ON "WEBHOOKS" ("UUID"); +CREATE INDEX "ORGANIZATION_WEBHOOK" ON "WEBHOOKS" ("ORGANIZATION_UUID"); +CREATE INDEX "PROJECT_WEBHOOK" ON "WEBHOOKS" ("PROJECT_UUID"); + + |