Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ec1f1b4
feat: update edit page
anna-parker Nov 6, 2025
a03431b
run prettier
anna-parker Nov 6, 2025
c550acc
fix integration tests
anna-parker Nov 6, 2025
bb6e95c
stupid error
anna-parker Nov 6, 2025
eb2a102
add test for multipath case
anna-parker Nov 6, 2025
7d81617
formatting
anna-parker Nov 6, 2025
a9b5574
try again
anna-parker Nov 6, 2025
98b64d5
Update integration-tests/tests/specs/features/revise-sequence.spec.ts
anna-parker Nov 6, 2025
e74f175
Update integration-tests/tests/specs/features/revise-sequence.spec.ts
anna-parker Nov 6, 2025
27034cc
feat!(backend): use column fastaId in metadata to group multi-segment…
anna-parker Nov 7, 2025
956680c
feat(prepro): have prepro assign segment - parse fastaHeader for back…
anna-parker Nov 7, 2025
6850dcc
feat(ingest): update multi-segment submission to new format
anna-parker Nov 7, 2025
acbc3c7
feat(website): improve website edit page
anna-parker Nov 7, 2025
3c6286c
feat(backend): add test for multi path case
anna-parker Nov 7, 2025
78c4d9a
feat(website): dont remove _ and - from header
anna-parker Nov 7, 2025
a60e638
feat(integration): fix integration tests
anna-parker Nov 7, 2025
d08e3b6
feat(website): correct single sequence submission for multi segment case
anna-parker Nov 7, 2025
8a305fb
feat(backend): fix merge conflicts
anna-parker Nov 7, 2025
e25d412
fix test data
anna-parker Nov 7, 2025
7b854c4
apply codex suggestions, e.g. fix join for no sequences case
anna-parker Nov 9, 2025
6344608
Update schema documentation based on migration changes
actions-user Nov 9, 2025
e812bec
fix
anna-parker Nov 9, 2025
852f76c
ok lets try this out
anna-parker Nov 9, 2025
0da55be
another try
anna-parker Nov 9, 2025
e7dfcfd
only replace whitespace
anna-parker Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions backend/docs/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,8 @@ CREATE TABLE public.metadata_upload_aux_table (
group_id integer,
uploaded_at timestamp without time zone NOT NULL,
metadata jsonb NOT NULL,
files jsonb
files jsonb,
fasta_ids jsonb DEFAULT '[]'::jsonb
);


Expand Down Expand Up @@ -537,9 +538,8 @@ ALTER VIEW public.sequence_entries_view OWNER TO postgres;

CREATE TABLE public.sequence_upload_aux_table (
upload_id text NOT NULL,
submission_id text NOT NULL,
segment_name text NOT NULL,
compressed_sequence_data text NOT NULL
compressed_sequence_data text NOT NULL,
fasta_id text NOT NULL
);


Expand Down Expand Up @@ -752,7 +752,7 @@ ALTER TABLE ONLY public.sequence_entries_preprocessed_data
--

ALTER TABLE ONLY public.sequence_upload_aux_table
ADD CONSTRAINT sequence_upload_aux_table_pkey PRIMARY KEY (upload_id, submission_id, segment_name);
ADD CONSTRAINT sequence_upload_aux_table_pkey PRIMARY KEY (upload_id, fasta_id);


--
Expand Down Expand Up @@ -793,6 +793,13 @@ CREATE INDEX data_use_terms_table_accession_idx ON public.data_use_terms_table U
CREATE INDEX flyway_schema_history_s_idx ON public.flyway_schema_history USING btree (success);


--
-- Name: metadata_upload_aux_table_fasta_ids_idx; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX metadata_upload_aux_table_fasta_ids_idx ON public.metadata_upload_aux_table USING gin (fasta_ids jsonb_path_ops);


--
-- Name: sequence_entries_organism_idx; Type: INDEX; Schema: public; Owner: postgres
--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import io.swagger.v3.oas.annotations.media.Schema
import org.loculus.backend.model.FastaId
import org.loculus.backend.model.SubmissionId
import org.loculus.backend.service.files.FileId
import org.loculus.backend.utils.Accession
Expand Down Expand Up @@ -300,9 +301,9 @@ data class OriginalDataInternal<SequenceType, FilesType>(
val metadata: Map<String, String>,
@Schema(
example = "{\"segment1\": \"ACTG\", \"segment2\": \"GTCA\"}",
description = "The key is the segment name, the value is the nucleotide sequence",
description = "The key is the fastaID, the value is the nucleotide sequence",
)
val unalignedNucleotideSequences: Map<SegmentName, SequenceType?>,
val unalignedNucleotideSequences: Map<FastaId, SequenceType?>,
@Schema(
example = """{"raw_reads": [{"fileId": "f1le-uuId-asdf", "name": "myfile.fastaq"]}""",
description = "A map from file categories, to lists of files. The files can also have URLs.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.loculus.backend.controller

import org.loculus.backend.model.HEADER_TO_CONNECT_METADATA_AND_SEQUENCES
import org.loculus.backend.model.METADATA_ID_HEADER

const val SUBMIT_RESPONSE_DESCRIPTION = """
Returns a list of accession, version and submissionId of the submitted sequence entries.
The submissionId is the (locally unique) '$HEADER_TO_CONNECT_METADATA_AND_SEQUENCES' provided by the submitter in the metadata file.
The submissionId is the (locally unique) '$METADATA_ID_HEADER' provided by the submitter in the metadata file.
The version will be 1 for every sequence.
The accession is the (globally unique) id that the system assigned to the sequence entry.
You can use this response to associate the user provided $HEADER_TO_CONNECT_METADATA_AND_SEQUENCES with the system assigned accession.
You can use this response to associate the user provided $METADATA_ID_HEADER with the system assigned accession.
"""

const val SUBMIT_ERROR_RESPONSE = """
Expand All @@ -18,16 +18,18 @@ const val METADATA_FILE_DESCRIPTION = """
A TSV (tab separated values) file containing the metadata of the submitted sequence entries.
The file may be compressed with zstd, xz, zip, gzip, lzma, bzip2 (with common extensions).
It must contain the column names.
The field '$HEADER_TO_CONNECT_METADATA_AND_SEQUENCES' is required and must be unique within the provided dataset.
The field '$METADATA_ID_HEADER' is required and must be unique within the provided dataset.
It is used to associate metadata to the sequences in the sequences fasta file.
"""

// TODO: update description
const val SEQUENCE_FILE_DESCRIPTION = """
A fasta file containing the unaligned nucleotide sequences of the submitted sequences.
The file may be compressed with zstd, xz, zip, gzip, lzma, bzip2 (with common extensions).
If the underlying organism has a single segment,
the headers of the fasta file must match the '$HEADER_TO_CONNECT_METADATA_AND_SEQUENCES' field in the metadata file.
the headers of the fasta file must match the '$METADATA_ID_HEADER' field in the metadata file.
If the underlying organism has multiple segments,
the headers of the fasta file must be of the form '>[$HEADER_TO_CONNECT_METADATA_AND_SEQUENCES]_[segmentName]'.
the headers of the fasta file must be of the form '>[$METADATA_ID_HEADER]_[segmentName]'.
"""

const val FILE_MAPPING_DESCRIPTION = """
Expand Down Expand Up @@ -114,7 +116,7 @@ The version will increase by one in respect to the original accession version.

const val REVISED_METADATA_FILE_DESCRIPTION = """
A TSV (tab separated values) file containing the metadata of the revised data.
The first row must contain the column names. The column '$HEADER_TO_CONNECT_METADATA_AND_SEQUENCES' is required and must be unique within the
The first row must contain the column names. The column '$METADATA_ID_HEADER' is required and must be unique within the
provided dataset. It is used to associate metadata to the sequences in the sequences fasta file.
Additionally, the column 'accession' is required and must match the accession of the original sequence entry.
"""
Expand Down
68 changes: 39 additions & 29 deletions backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ import java.io.BufferedInputStream
import java.io.File
import java.io.InputStream

const val HEADER_TO_CONNECT_METADATA_AND_SEQUENCES = "id"
const val HEADER_TO_CONNECT_METADATA_AND_SEQUENCES_ALTERNATE_FOR_BACKCOMPAT = "submissionId"
const val METADATA_ID_HEADER = "id"
const val METADATA_ID_HEADER_ALTERNATE_FOR_BACKCOMPAT = "submissionId"
const val FASTA_ID_HEADER = "fastaId"

const val ACCESSION_HEADER = "accession"
private val log = KotlinLogging.logger { }

typealias SubmissionId = String
typealias FastaId = String
typealias SegmentName = String

const val UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"
Expand Down Expand Up @@ -128,8 +130,13 @@ class SubmitModel(
val metadataSubmissionIds = uploadDatabaseService.getMetadataUploadSubmissionIds(uploadId).toSet()
if (requiresConsensusSequenceFile(submissionParams.organism)) {
log.debug { "Validating submission with uploadId $uploadId" }
val sequenceSubmissionIds = uploadDatabaseService.getSequenceUploadSubmissionIds(uploadId).toSet()
validateSubmissionIdSetsForConsensusSequences(metadataSubmissionIds, sequenceSubmissionIds)
val metadataFastaIds = uploadDatabaseService.getFastaIdsForMetadata(uploadId)
val metadataFastaIdsSet = metadataFastaIds.flatten().toSet()
if (metadataFastaIdsSet.size < metadataFastaIds.flatten().size) {
throw UnprocessableEntityException("Metadata file contains duplicate fastaIds.")
Comment thread
anna-parker marked this conversation as resolved.
}
val sequenceFastaIds = uploadDatabaseService.getSequenceUploadSubmissionIds(uploadId).toSet()
validateSubmissionIdSetsForConsensusSequences(metadataFastaIdsSet, sequenceFastaIds)
}

if (submissionParams is SubmissionParams.RevisionSubmissionParams) {
Expand Down Expand Up @@ -177,38 +184,39 @@ class SubmitModel(
metadataFileTypes,
metadataTempFileToDelete,
)
val addFastaId = requiresConsensusSequenceFile(submissionParams.organism)
try {
uploadMetadata(uploadId, submissionParams, metadataStream, batchSize)
uploadMetadata(uploadId, submissionParams, metadataStream, batchSize, addFastaId = addFastaId)
} finally {
metadataTempFileToDelete.delete()
}

val sequenceFile = submissionParams.sequenceFile
if (sequenceFile == null) {
if (requiresConsensusSequenceFile(submissionParams.organism)) {
if (addFastaId) {
throw BadRequestException(
"Submissions for organism ${submissionParams.organism.name} require a sequence file.",
)
}
} else {
if (!requiresConsensusSequenceFile(submissionParams.organism)) {
throw BadRequestException(
"Sequence uploads are not allowed for organism ${submissionParams.organism.name}.",
)
}
return
}
if (!addFastaId) {
throw BadRequestException(
"Sequence uploads are not allowed for organism ${submissionParams.organism.name}.",
)
}

val sequenceTempFileToDelete = MaybeFile()
try {
val sequenceStream = getStreamFromFile(
sequenceFile,
uploadId,
sequenceFileTypes,
sequenceTempFileToDelete,
)
uploadSequences(uploadId, sequenceStream, batchSize, submissionParams.organism)
} finally {
sequenceTempFileToDelete.delete()
}
val sequenceTempFileToDelete = MaybeFile()
try {
val sequenceStream = getStreamFromFile(
sequenceFile,
uploadId,
sequenceFileTypes,
sequenceTempFileToDelete,
)
uploadSequences(uploadId, sequenceStream, batchSize, submissionParams.organism)
} finally {
sequenceTempFileToDelete.delete()
}
}

Expand Down Expand Up @@ -254,6 +262,7 @@ class SubmitModel(
submissionParams: SubmissionParams,
metadataStream: InputStream,
batchSize: Int,
addFastaId: Boolean,
) {
log.debug {
"intermediate storing uploaded metadata of type ${submissionParams.uploadType.name} " +
Expand All @@ -263,7 +272,7 @@ class SubmitModel(
try {
when (submissionParams) {
is SubmissionParams.OriginalSubmissionParams -> {
metadataEntryStreamAsSequence(metadataStream)
metadataEntryStreamAsSequence(metadataStream, addFastaId)
.chunked(batchSize)
.forEach { batch ->
uploadDatabaseService.batchInsertMetadataInAuxTable(
Expand All @@ -279,7 +288,7 @@ class SubmitModel(
}

is SubmissionParams.RevisionSubmissionParams -> {
revisionEntryStreamAsSequence(metadataStream)
revisionEntryStreamAsSequence(metadataStream, addFastaId)
.chunked(batchSize)
.forEach { batch ->
uploadDatabaseService.batchInsertRevisedMetadataInAuxTable(
Expand Down Expand Up @@ -354,14 +363,15 @@ class SubmitModel(

if (metadataKeysNotInSequences.isNotEmpty() || sequenceKeysNotInMetadata.isNotEmpty()) {
val metadataNotPresentErrorText = if (metadataKeysNotInSequences.isNotEmpty()) {
"Metadata file contains ${metadataKeysNotInSequences.size} ids that are not present " +
"Metadata file contains ${metadataKeysNotInSequences.size} Fasta ids that are not present " +
"in the sequence file: " + metadataKeysNotInSequences.toList().joinToString(limit = 10) + "; "
} else {
""
}
val sequenceNotPresentErrorText = if (sequenceKeysNotInMetadata.isNotEmpty()) {
"Sequence file contains ${sequenceKeysNotInMetadata.size} ids that are not present " +
"in the metadata file: " + sequenceKeysNotInMetadata.toList().joinToString(limit = 10)
"Sequence file contains ${sequenceKeysNotInMetadata.size} Fasta ids that are not present " +
"in the metadata file: " +
sequenceKeysNotInMetadata.toList().joinToString(limit = 10)
} else {
""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,7 @@ class SubmissionDatabaseService(
.fetchSize(streamBatchSize)
.asSequence()
.map {
// Revoked sequences have no original metdadata, hence null can happen
// Revoked sequences have no original metadata, hence null can happen
@Suppress("USELESS_ELVIS")
val metadata = it[originalMetadata] ?: null
val selectedMetadata = fields?.associateWith { field -> metadata?.get(field) }
Expand Down
Loading
Loading