Repository: n8n-io/n8n
File: packages/@n8n/db/src/repositories/execution.repository.ts (TypeScript)

Code before:
				this.logger.error('Failed to delete an execution due to insufficient permissions', {
					executionIds: deleteConditions.ids,
				});
			}
			return [];
		}

		const refs = executions.map(({ id, workflowId, storedAt }) => ({
			executionId: id,
			workflowId,
			storedAt,
		}));

		const toDelete = [...refs];
		do {
			// Delete in batches to avoid "SQLITE_ERROR: Expression tree is too large (maximum depth 1000)" error
			const batch = toDelete.splice(0, this.hardDeletionBatchSize);
			await Promise.all([
				this.delete(batch.map(({ executionId }) => executionId)),
				this.binaryDataService.deleteMany(batch.map((b) => ({ type: 'execution' as const, ...b }))),
			]);
		} while (toDelete.length > 0);

		return refs;
	}

	async getIdsSince(date: Date) {
		return await this.find({
			select: ['id'],
			where: {
				startedAt: MoreThanOrEqual(DateUtils.mixedDateToUtcDatetimeString(date)),
			},
		}).then((executions) => executions.map(({ id }) => id));
	}

	async softDeletePrunableExecutions() {
		const { pruneDataMaxAge, pruneDataMaxCount } = this.globalConfig.executions;

		// Sub-query to exclude executions having annotations
		const annotatedExecutionsSubQuery = this.manager
			.createQueryBuilder()
			.subQuery()
			.select('annotation.executionId')
			.from(ExecutionAnnotation, 'annotation');

		// Find ids of all executions that were stopped longer that pruneDataMaxAge ago
		const date = new Date();
		date.setHours(date.getHours() - pruneDataMaxAge);

		const toPrune: Array<FindOptionsWhere<ExecutionEntity>> = [
			// date reformatting needed - see https://github.com/typeorm/typeorm/issues/2286

Review comment (bug):
This is a bit confusing what the responsibilities here are. In some cases `ExecutionPersistence` deletes the binary data, but here in this case `ExecutionRepository` is responsible for that. Which one should own that?

Code after:
		}

		const executions = await query.getMany();

		if (!executions.length) {
			if (deleteConditions.ids) {
				this.logger.error('Failed to delete an execution due to insufficient permissions', {
					executionIds: deleteConditions.ids,
				});
			}
			return [];
		}

		const refs = executions.map(({ id, workflowId, storedAt }) => ({
			executionId: id,
			workflowId,
			storedAt,
		}));

		const toDelete = [...refs];
		do {
			// Delete in batches to avoid "SQLITE_ERROR: Expression tree is too large (maximum depth 1000)" error
			const batch = toDelete.splice(0, this.hardDeletionBatchSize);
			await Promise.all([
				this.delete(batch.map(({ executionId }) => executionId)),
				this.binaryDataService.deleteMany(batch.map((b) => ({ type: 'execution' as const, ...b }))),
			]);
		} while (toDelete.length > 0);

		return refs;
	}

	async getIdsSince(date: Date) {
		return await this.find({
			select: ['id'],
			where: {
				startedAt: MoreThanOrEqual(DateUtils.mixedDateToUtcDatetimeString(date)),
			},
		}).then((executions) => executions.map(({ id }) => id));
	}

	async softDeletePrunableExecutions() {
		const { pruneDataMaxAge, pruneDataMaxCount } = this.globalConfig.executions;

		// Sub-query to exclude executions having annotations
		const annotatedExecutionsSubQuery = this.manager
			.createQueryBuilder()
			.subQuery()
			.select('annotation.executionId')
			.from(ExecutionAnnotation, 'annotation');