From 56a24678da451256185a08681cfefe53b8b3ff7d Mon Sep 17 00:00:00 2001 From: krestar Date: Tue, 11 Aug 2026 14:04:24 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20PostgreSQL=20=EB=B3=B4=ED=98=B8=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20RLS=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #125의 V40/V41 다음 V42 migration으로 tenant 보호 테이블 38개의 RLS를 활성화 - migration의 lock timeout과 statement timeout을 설정 - 정책 테이블, RLS 활성 테이블, FORCE 미적용 상태를 schema 테스트로 검증 - RLS 환경에서 runtime timeout 테스트가 실제 tenant context로 UPDATE를 수행하도록 보정 --- .../V42__enable_postgresql_rls.sql | 45 +++++++++++ .../server/PostgreSqlMigrationTests.java | 74 ++++++++++++++++++- ...RuntimeTimeoutBehaviorIntegrationTest.java | 39 ++++++---- ...reSqlRuntimeTimeoutIntegrationSupport.java | 5 ++ 4 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 src/main/resources/db/migration-postgresql/V42__enable_postgresql_rls.sql diff --git a/src/main/resources/db/migration-postgresql/V42__enable_postgresql_rls.sql b/src/main/resources/db/migration-postgresql/V42__enable_postgresql_rls.sql new file mode 100644 index 00000000..e55ac5cd --- /dev/null +++ b/src/main/resources/db/migration-postgresql/V42__enable_postgresql_rls.sql @@ -0,0 +1,45 @@ +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '30s'; + +ALTER TABLE public.company ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.company_settings ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.user_account ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.refresh_token ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.user_agreement_consent ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.password_reset_token ENABLE ROW LEVEL SECURITY; + +ALTER TABLE public.worker ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_document ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.stored_file ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_checklist_item ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_transition_history ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.document_request_draft ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.document_request_draft_type ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.approval_request ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.external_submission ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_evidence ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.audit_event ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.workflow_case ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.document_ocr_run ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.notification ENABLE ROW LEVEL SECURITY; + +ALTER TABLE public.worker_link ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_response ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_response_upload ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_document_upload_idempotency ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_import_job ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_import_row ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_import_commit_idempotency ENABLE ROW LEVEL SECURITY; + +ALTER TABLE public.ai_run ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_attempt ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_question ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_candidate ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_candidate_decision_batch ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_candidate_decision ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_candidate_decision_task ENABLE ROW LEVEL SECURITY; + +ALTER TABLE public.event_publication ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.event_consumption ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.outbox_manual_retry ENABLE ROW LEVEL SECURITY; diff --git a/src/test/java/com/fowoco/server/PostgreSqlMigrationTests.java b/src/test/java/com/fowoco/server/PostgreSqlMigrationTests.java index 9a0cd9af..fb66787a 100644 --- a/src/test/java/com/fowoco/server/PostgreSqlMigrationTests.java +++ b/src/test/java/com/fowoco/server/PostgreSqlMigrationTests.java @@ -21,6 +21,47 @@ @EnabledIfEnvironmentVariable(named = "POSTGRES_TEST_ENABLED", matches = "true") class PostgreSqlMigrationTests { + private static final Set RLS_TABLES = Set.of( + "company", + "company_settings", + "user_account", + "refresh_token", + "user_agreement_consent", + "password_reset_token", + "worker", + "worker_document", + "stored_file", + "task", + "task_checklist_item", + "task_transition_history", + "document_request_draft", + "document_request_draft_type", + "approval_request", + "external_submission", + "task_evidence", + "audit_event", + "workflow_case", + "document_ocr_run", + "notification", + "worker_link", + "worker_response", + "worker_response_upload", + "worker_document_upload_idempotency", + "worker_import_job", + "worker_import_row", + "worker_import_commit_idempotency", + "ai_run", + "ai_attempt", + "ai_question", + "ai_candidate", + "ai_candidate_decision_batch", + "ai_candidate_decision", + "ai_candidate_decision_task", + "event_publication", + "event_consumption", + "outbox_manual_retry" + ); + private static final String COMPANY_A = "10000000-0000-0000-0000-000000000001"; private static final String COMPANY_B = "20000000-0000-0000-0000-000000000002"; private static final String USER_A = "11000000-0000-0000-0000-000000000001"; @@ -109,7 +150,8 @@ private void assertSchemaContract(Connection connection) throws SQLException { "worker_import_job", "worker_import_row", "worker_import_commit_idempotency", - "document_ocr_run" + "document_ocr_run", + "notification" ); assertThat(columnSpecs(connection, "company")) @@ -528,7 +570,11 @@ private void assertSchemaContract(Connection connection) throws SQLException { "pl_document_ocr_run_tenant_isolation", "pl_notification_tenant_isolation" ); - assertThat(rlsEnabledTables(connection)).isEmpty(); + assertThat(policyTableNames(connection)) + .containsExactlyInAnyOrderElementsOf(RLS_TABLES); + assertThat(rlsEnabledTables(connection)) + .containsExactlyInAnyOrderElementsOf(RLS_TABLES); + assertThat(rlsForcedTables(connection)).isEmpty(); assertThat(securityDefinerFunctionNames(connection)) .containsExactlyInAnyOrder( "bootstrap_company_id_by_normalized_email", @@ -1046,6 +1092,17 @@ private Set policyNames(Connection connection) throws SQLException { ); } + private Set policyTableNames(Connection connection) throws SQLException { + return queryStrings( + connection, + """ + SELECT DISTINCT tablename + FROM pg_catalog.pg_policies + WHERE schemaname = 'public' + """ + ); + } + private Set rlsEnabledTables(Connection connection) throws SQLException { return queryStrings( connection, @@ -1059,6 +1116,19 @@ private Set rlsEnabledTables(Connection connection) throws SQLException ); } + private Set rlsForcedTables(Connection connection) throws SQLException { + return queryStrings( + connection, + """ + SELECT relname + FROM pg_catalog.pg_class + WHERE relnamespace = 'public'::regnamespace + AND relkind = 'r' + AND relforcerowsecurity + """ + ); + } + private Set securityDefinerFunctionNames(Connection connection) throws SQLException { return queryStrings( connection, diff --git a/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutBehaviorIntegrationTest.java b/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutBehaviorIntegrationTest.java index aa1208f7..4a70d616 100644 --- a/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutBehaviorIntegrationTest.java +++ b/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutBehaviorIntegrationTest.java @@ -39,11 +39,14 @@ void resetFixture() { void statementTimeoutRollsBackTransactionAndPoolServesNextQuery() { Throwable failure = catchThrowable(() -> transactionTemplate.executeWithoutResult( status -> { - runtimeJdbc.update( + tenantDatabaseContext.setCompanyIdForCurrentTransaction( + FIXTURE_COMPANY_ID + ); + assertThat(runtimeJdbc.update( "UPDATE company SET name = ? WHERE company_id = ?", "must roll back", FIXTURE_COMPANY_ID - ); + )).isEqualTo(1); runtimeJdbc.execute("SELECT pg_catalog.pg_sleep(1.0)"); } )); @@ -72,14 +75,17 @@ void lockTimeoutDoesNotAffectLockOwnerAndPoolRecovers() throws Exception { for (int attempt = 0; attempt < 3; attempt++) { Throwable failure = catchThrowable(() -> - transactionTemplate.executeWithoutResult(status -> - runtimeJdbc.update( - "UPDATE company SET name = ? " - + "WHERE company_id = ?", - "blocked update", - FIXTURE_COMPANY_ID - ) - ) + transactionTemplate.executeWithoutResult(status -> { + tenantDatabaseContext.setCompanyIdForCurrentTransaction( + FIXTURE_COMPANY_ID + ); + runtimeJdbc.update( + "UPDATE company SET name = ? " + + "WHERE company_id = ?", + "blocked update", + FIXTURE_COMPANY_ID + ); + }) ); PostgreSqlTimeoutClassification classification = classifier.classify(failure); @@ -95,11 +101,14 @@ void lockTimeoutDoesNotAffectLockOwnerAndPoolRecovers() throws Exception { } } - transactionTemplate.executeWithoutResult(status -> runtimeJdbc.update( - "UPDATE company SET name = ? WHERE company_id = ?", - "after lock release", - FIXTURE_COMPANY_ID - )); + transactionTemplate.executeWithoutResult(status -> { + tenantDatabaseContext.setCompanyIdForCurrentTransaction(FIXTURE_COMPANY_ID); + assertThat(runtimeJdbc.update( + "UPDATE company SET name = ? WHERE company_id = ?", + "after lock release", + FIXTURE_COMPANY_ID + )).isEqualTo(1); + }); assertThat(migrationJdbc.queryForObject( "SELECT name FROM company WHERE company_id = ?", String.class, diff --git a/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutIntegrationSupport.java b/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutIntegrationSupport.java index 4c966891..c936c10d 100644 --- a/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutIntegrationSupport.java +++ b/src/test/java/com/fowoco/server/common/database/PostgreSqlRuntimeTimeoutIntegrationSupport.java @@ -2,6 +2,7 @@ import com.fowoco.server.ServerApplication; import com.fowoco.server.common.security.PostgreSqlRlsTestLock; +import com.fowoco.server.common.security.TenantDatabaseContext; import com.zaxxer.hikari.HikariDataSource; import java.sql.Connection; import java.sql.DriverManager; @@ -41,6 +42,7 @@ abstract class PostgreSqlRuntimeTimeoutIntegrationSupport { protected JdbcTemplate runtimeJdbc; protected HikariDataSource runtimeDataSource; protected TransactionTemplate transactionTemplate; + protected TenantDatabaseContext tenantDatabaseContext; private PostgreSqlRlsTestLock rlsTestLock; protected abstract String statementTimeout(); @@ -122,6 +124,9 @@ ON CONFLICT (company_id) DO UPDATE SET transactionTemplate = new TransactionTemplate( applicationContext.getBean(PlatformTransactionManager.class) ); + tenantDatabaseContext = applicationContext.getBean( + TenantDatabaseContext.class + ); } catch (Throwable setupFailure) { try { tearDownRuntimeTimeoutFixture(); From d6b65c0136755725e429fde44703605a6cc07e6b Mon Sep 17 00:00:00 2001 From: krestar Date: Thu, 13 Aug 2026 11:33:21 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20V42=20->=20V43=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20Migration=20=EB=B2=88=ED=98=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main 브랜치 기준으로 충돌 방지를 위해 수정합니다. --- ...__enable_postgresql_rls.sql => V43__enable_postgresql_rls.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration-postgresql/{V42__enable_postgresql_rls.sql => V43__enable_postgresql_rls.sql} (100%) diff --git a/src/main/resources/db/migration-postgresql/V42__enable_postgresql_rls.sql b/src/main/resources/db/migration-postgresql/V43__enable_postgresql_rls.sql similarity index 100% rename from src/main/resources/db/migration-postgresql/V42__enable_postgresql_rls.sql rename to src/main/resources/db/migration-postgresql/V43__enable_postgresql_rls.sql From a17c61bbf079b39607798a6c597603a66c3701d4 Mon Sep 17 00:00:00 2001 From: krestar Date: Mon, 17 Aug 2026 16:09:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(db):=20=EC=B5=9C=EC=8B=A0=20=ED=85=8C?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=20RLS=20?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RLS 활성화 migration을 최신 main 다음 버전인 V58로 재번호화 - stay_verification_case와 worker_archive를 RLS 활성화 대상에 추가 - 제한 runtime role 격리 테스트에 두 테이블의 fixture, 조회 격리, 쓰기 차단, cleanup 검증 추가 --- ...rls.sql => V58__enable_postgresql_rls.sql} | 2 + .../security/PostgreSqlRlsIsolationTest.java | 103 +++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) rename src/main/resources/db/migration-postgresql/{V43__enable_postgresql_rls.sql => V58__enable_postgresql_rls.sql} (95%) diff --git a/src/main/resources/db/migration-postgresql/V43__enable_postgresql_rls.sql b/src/main/resources/db/migration-postgresql/V58__enable_postgresql_rls.sql similarity index 95% rename from src/main/resources/db/migration-postgresql/V43__enable_postgresql_rls.sql rename to src/main/resources/db/migration-postgresql/V58__enable_postgresql_rls.sql index e55ac5cd..05caa7ba 100644 --- a/src/main/resources/db/migration-postgresql/V43__enable_postgresql_rls.sql +++ b/src/main/resources/db/migration-postgresql/V58__enable_postgresql_rls.sql @@ -23,6 +23,8 @@ ALTER TABLE public.audit_event ENABLE ROW LEVEL SECURITY; ALTER TABLE public.workflow_case ENABLE ROW LEVEL SECURITY; ALTER TABLE public.document_ocr_run ENABLE ROW LEVEL SECURITY; ALTER TABLE public.notification ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.stay_verification_case ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.worker_archive ENABLE ROW LEVEL SECURITY; ALTER TABLE public.worker_link ENABLE ROW LEVEL SECURITY; ALTER TABLE public.worker_response ENABLE ROW LEVEL SECURITY; diff --git a/src/test/java/com/fowoco/server/common/security/PostgreSqlRlsIsolationTest.java b/src/test/java/com/fowoco/server/common/security/PostgreSqlRlsIsolationTest.java index 34a0a29f..aa16e53d 100644 --- a/src/test/java/com/fowoco/server/common/security/PostgreSqlRlsIsolationTest.java +++ b/src/test/java/com/fowoco/server/common/security/PostgreSqlRlsIsolationTest.java @@ -86,6 +86,10 @@ class PostgreSqlRlsIsolationTest { UUID.fromString("ad000000-0000-0000-0000-000000000001"); private static final UUID OCR_RUN_B = UUID.fromString("bd000000-0000-0000-0000-000000000002"); + private static final UUID STAY_VERIFICATION_A = + UUID.fromString("ae000000-0000-0000-0000-000000000001"); + private static final UUID STAY_VERIFICATION_B = + UUID.fromString("be000000-0000-0000-0000-000000000002"); private static final List RLS_TABLES = List.of( "company", "user_account", @@ -102,7 +106,9 @@ class PostgreSqlRlsIsolationTest { "outbox_manual_retry", "user_agreement_consent", "password_reset_token", - "document_ocr_run" + "document_ocr_run", + "stay_verification_case", + "worker_archive" ); @Test @@ -196,6 +202,8 @@ private void prepareFixture( + "public.worker_document_upload_idempotency, " + "public.outbox_manual_retry, " + "public.document_ocr_run, " + + "public.stay_verification_case, " + + "public.worker_archive, " + "public.user_agreement_consent, " + "public.password_reset_token TO " + quotedRole @@ -225,6 +233,31 @@ INSERT INTO user_account ( ('%s', '%s', 'rls-b@example.com', 'rls-b@example.com', 'test-password-hash-b', 'ADMIN', 'ACTIVE') """.formatted(USER_A, COMPANY_A, USER_B, COMPANY_B)); + statement.execute(""" + INSERT INTO stay_verification_case ( + stay_verification_id, company_id, worker_id, + source_stay_expiry_date, verification_status, + created_at, updated_at + ) VALUES + ('%s', '%s', '%s', DATE '2026-08-01', 'UNKNOWN', + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('%s', '%s', '%s', DATE '2026-08-02', 'UNKNOWN', + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """.formatted( + STAY_VERIFICATION_A, COMPANY_A, WORKER_A, + STAY_VERIFICATION_B, COMPANY_B, WORKER_B + )); + statement.execute(""" + INSERT INTO worker_archive ( + worker_id, company_id, archived_at, archived_by, + archive_reason, worker_version + ) VALUES + ('%s', '%s', CURRENT_TIMESTAMP, '%s', 'RLS archive A', 1), + ('%s', '%s', CURRENT_TIMESTAMP, '%s', 'RLS archive B', 1) + """.formatted( + WORKER_A, COMPANY_A, USER_A, + WORKER_B, COMPANY_B, USER_B + )); statement.execute(""" INSERT INTO user_agreement_consent ( consent_id, company_id, user_id, agreement_type, @@ -448,6 +481,8 @@ private void assertMissingAndInvalidContextFailClosed(Connection connection) assertThat(tableCount(connection, "user_agreement_consent")).isZero(); assertThat(tableCount(connection, "password_reset_token")).isZero(); assertThat(tableCount(connection, "document_ocr_run")).isZero(); + assertThat(tableCount(connection, "stay_verification_case")).isZero(); + assertThat(tableCount(connection, "worker_archive")).isZero(); setTenantContext(connection, ""); assertThat(workerCount(connection)).isZero(); @@ -524,6 +559,15 @@ private void assertTenantCrudIsolation(Connection connection) throws SQLExceptio connection, "SELECT ocr_run_id FROM public.document_ocr_run ORDER BY ocr_run_id" )).containsExactly(OCR_RUN_A); + assertThat(uuidValues( + connection, + "SELECT stay_verification_id FROM public.stay_verification_case " + + "ORDER BY stay_verification_id" + )).containsExactly(STAY_VERIFICATION_A); + assertThat(uuidValues( + connection, + "SELECT worker_id FROM public.worker_archive ORDER BY worker_id" + )).containsExactly(WORKER_A); assertThat(executeUpdate( connection, @@ -698,6 +742,33 @@ INSERT INTO outbox_manual_retry ( ) """.formatted(COMPANY_B, EVENT_B, USER_B) ); + assertSqlState( + connection, + "42501", + """ + INSERT INTO stay_verification_case ( + stay_verification_id, company_id, worker_id, + source_stay_expiry_date, verification_status, + created_at, updated_at + ) VALUES ( + 'be000000-0000-0000-0000-000000000099', '%s', '%s', + DATE '2026-08-03', 'UNKNOWN', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + ) + """.formatted(COMPANY_B, WORKER_B) + ); + assertSqlState( + connection, + "42501", + """ + INSERT INTO worker_archive ( + worker_id, company_id, archived_at, archived_by, + archive_reason, worker_version + ) VALUES ( + '%s', '%s', + CURRENT_TIMESTAMP, '%s', 'Forbidden archive', 1 + ) + """.formatted(WORKER_B, COMPANY_B, USER_B) + ); assertThat(executeUpdate( connection, @@ -714,6 +785,16 @@ INSERT INTO outbox_manual_retry ( "DELETE FROM document_ocr_run WHERE ocr_run_id = ?", OCR_RUN_B )).isZero(); + assertThat(executeUpdate( + connection, + "DELETE FROM stay_verification_case WHERE stay_verification_id = ?", + STAY_VERIFICATION_B + )).isZero(); + assertThat(executeUpdate( + connection, + "DELETE FROM worker_archive WHERE worker_id = ?", + WORKER_B + )).isZero(); assertThat(executeUpdate( connection, "DELETE FROM workflow_case WHERE case_id = ?", @@ -760,6 +841,8 @@ private void assertCommittedContextDoesNotLeak(Connection connection) throws SQL assertThat(tableCount(connection, "workflow_case")).isZero(); assertThat(tableCount(connection, "outbox_manual_retry")).isZero(); assertThat(tableCount(connection, "document_ocr_run")).isZero(); + assertThat(tableCount(connection, "stay_verification_case")).isZero(); + assertThat(tableCount(connection, "worker_archive")).isZero(); setTenantContext(connection, COMPANY_B.toString()); assertThat(workerIds(connection)).containsExactly(WORKER_B); assertThat(uuidValues( @@ -774,6 +857,15 @@ private void assertCommittedContextDoesNotLeak(Connection connection) throws SQL connection, "SELECT ocr_run_id FROM public.document_ocr_run ORDER BY ocr_run_id" )).containsExactly(OCR_RUN_B); + assertThat(uuidValues( + connection, + "SELECT stay_verification_id FROM public.stay_verification_case " + + "ORDER BY stay_verification_id" + )).containsExactly(STAY_VERIFICATION_B); + assertThat(uuidValues( + connection, + "SELECT worker_id FROM public.worker_archive ORDER BY worker_id" + )).containsExactly(WORKER_B); connection.rollback(); } @@ -816,6 +908,15 @@ private SQLException runCleanupStep(SQLException failure, SqlCleanupStep step) { } private void deleteFixtureRows(Statement statement) throws SQLException { + statement.execute(""" + DELETE FROM stay_verification_case + WHERE stay_verification_id IN ('%s', '%s') + OR stay_verification_id = 'be000000-0000-0000-0000-000000000099' + """.formatted(STAY_VERIFICATION_A, STAY_VERIFICATION_B)); + statement.execute(""" + DELETE FROM worker_archive + WHERE worker_id IN ('%s', '%s') + """.formatted(WORKER_A, WORKER_B)); statement.execute(""" DELETE FROM document_ocr_run WHERE ocr_run_id IN ('%s', '%s')