Skip to content

Commit 13f3f2a

Browse files
authored
Make annotation layer name check deferrable, fixing false positives (#8896)
- removes unused route editAnnotationLayer - change to transactionally update annotation layers in postgres, after it was reported by tracingstore side - make the name unique check deferrable, so it is only checked on the transaction commit. - This fixes the following error scenario: The user renames layer 1 from A to B and layer 2 from C to A. This is legitimate, as no duplicate names exist if the order is correct. However, since the postgres updates are not guaranteed to come in the same order, this could previously lead to false positive duplicate checks. - Note that running this evolution on large annotation_layers tables may be slow, as the constraint is completely rebuilt. ### Steps to test: - Create annotation with multiple volume annotation layers - rename multiple ones of them such that the name that was previously taken by one layer is then immediately taken by another. Should not fail. Save and reload should work and show the correct names. ### Issues: - fixes #8647 ------ - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md)
1 parent abcdb07 commit 13f3f2a

File tree

8 files changed

+46
-26
lines changed

8 files changed

+46
-26
lines changed

app/controllers/AnnotationController.scala

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -276,17 +276,6 @@ class AnnotationController @Inject()(
276276
} yield JsonOk(Messages("annotation.edit.success"))
277277
}
278278

279-
def editAnnotationLayer(typ: String, id: ObjectId, tracingId: String): Action[JsValue] =
280-
sil.SecuredAction.async(parse.json) { implicit request =>
281-
for {
282-
annotation <- provider.provideAnnotation(typ, id, request.identity) ~> NOT_FOUND
283-
restrictions <- provider.restrictionsFor(typ, id) ?~> "restrictions.notFound" ~> NOT_FOUND
284-
_ <- restrictions.allowUpdate(request.identity) ?~> "notAllowed" ~> FORBIDDEN
285-
newLayerName = (request.body \ "name").as[String]
286-
_ <- annotationLayerDAO.updateName(annotation._id, tracingId, newLayerName) ?~> "annotation.edit.failed"
287-
} yield JsonOk(Messages("annotation.edit.success"))
288-
}
289-
290279
def annotationsForTask(taskId: ObjectId): Action[AnyContent] =
291280
sil.SecuredAction.async { implicit request =>
292281
for {

app/controllers/WKRemoteTracingStoreController.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ class WKRemoteTracingStoreController @Inject()(tracingStoreService: TracingStore
6161
layerIdsToUpdate = existingLayerIds.intersect(newLayerIds)
6262
layerIdsToInsert = newLayerIds.diff(existingLayerIds)
6363
_ <- Fox.serialCombined(layerIdsToDelete.toList)(annotationLayerDAO.deleteOneByTracingId(annotationId, _))
64-
_ <- Fox.serialCombined(newLayersProto.filter(l => layerIdsToInsert.contains(l.tracingId))) { layerProto =>
65-
annotationLayerDAO.insertOne(annotationId, AnnotationLayer.fromProto(layerProto))
66-
}
67-
_ <- Fox.serialCombined(newLayersProto.filter(l => layerIdsToUpdate.contains(l.tracingId)))(l =>
68-
annotationLayerDAO.updateName(annotationId, l.tracingId, l.name))
64+
_ <- annotationLayerDAO.updateLayers(
65+
annotationId,
66+
newLayersProto.filter(l => layerIdsToInsert.contains(l.tracingId)).map(AnnotationLayer.fromProto),
67+
newLayersProto.filter(l => layerIdsToUpdate.contains(l.tracingId)).map(AnnotationLayer.fromProto)
68+
)
6969
// Layer stats are ignored here, they are sent eagerly when saving updates
7070
_ <- annotationDAO.updateDescription(annotationId, request.body.description)
7171
} yield Ok

app/models/annotation/Annotation.scala

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import slick.jdbc.TransactionIsolation.Serializable
1616
import slick.lifted.Rep
1717
import slick.sql.SqlAction
1818
import com.scalableminds.util.objectid.ObjectId
19+
import slick.dbio.DBIO
1920
import utils.sql.{SQLDAO, SimpleSQLDAO, SqlClient, SqlToken}
2021

2122
import javax.inject.Inject
@@ -131,8 +132,7 @@ class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionC
131132
_ <- run(insertOneQuery(annotationId, annotationLayer))
132133
} yield ()
133134

134-
def insertLayerQueries(annotationId: ObjectId,
135-
layers: List[AnnotationLayer]): List[SqlAction[Int, NoStream, Effect]] =
135+
def insertLayerQueries(annotationId: ObjectId, layers: Seq[AnnotationLayer]): Seq[SqlAction[Int, NoStream, Effect]] =
136136
layers.map { annotationLayer =>
137137
insertOneQuery(annotationId, annotationLayer)
138138
}
@@ -161,13 +161,20 @@ class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionC
161161
head <- rList.headOption.toFox
162162
} yield head
163163

164-
def updateName(annotationId: ObjectId, tracingId: String, newName: String): Fox[Unit] =
164+
def updateLayers(annotationId: ObjectId,
165+
newLayers: Seq[AnnotationLayer],
166+
updatedLayers: Seq[AnnotationLayer]): Fox[Unit] = {
167+
val insertQueries = insertLayerQueries(annotationId, newLayers)
168+
val updateQueries = updatedLayers.map { updatedLayer =>
169+
q"""UPDATE webknossos.annotation_layers
170+
SET name = ${updatedLayer.name}
171+
WHERE _annotation = $annotationId
172+
AND tracingId = ${updatedLayer.tracingId}""".asUpdate
173+
}
165174
for {
166-
_ <- run(q"""UPDATE webknossos.annotation_layers
167-
SET name = $newName
168-
WHERE _annotation = $annotationId
169-
AND tracingId = $tracingId""".asUpdate)
175+
_ <- run(DBIO.sequence(insertQueries ++ updateQueries).transactionally)
170176
} yield ()
177+
}
171178

172179
def deleteAllForAnnotationQuery(annotationId: ObjectId): SqlAction[Int, NoStream, Effect] =
173180
q"DELETE FROM webknossos.annotation_layers WHERE _annotation = $annotationId".asUpdate
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
START TRANSACTION;
2+
3+
do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 139, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql;
4+
5+
ALTER TABLE webknossos.annotation_layers DROP CONSTRAINT annotation_layers_name__annotation_key;
6+
ALTER TABLE webknossos.annotation_layers ADD CONSTRAINT annotation_layers_name__annotation_key UNIQUE (name, _annotation) DEFERRABLE INITIALLY DEFERRED;
7+
8+
UPDATE webknossos.releaseInformation SET schemaVersion = 140;
9+
10+
COMMIT TRANSACTION;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
START TRANSACTION;
2+
3+
do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 140, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql;
4+
5+
ALTER TABLE webknossos.annotation_layers DROP CONSTRAINT annotation_layers_name__annotation_key;
6+
ALTER TABLE webknossos.annotation_layers ADD CONSTRAINT annotation_layers_name__annotation_key UNIQUE (name, _annotation);
7+
8+
UPDATE webknossos.releaseInformation SET schemaVersion = 139;
9+
10+
COMMIT TRANSACTION;

conf/webknossos.latest.routes

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ POST /userToken/generate
156156
POST /annotations/upload controllers.AnnotationIOController.upload()
157157
POST /annotations/:typ/:id/duplicate controllers.AnnotationController.duplicate(typ: String, id: ObjectId)
158158
PATCH /annotations/:typ/:id/edit controllers.AnnotationController.editAnnotation(typ: String, id: ObjectId)
159-
PATCH /annotations/:typ/:id/editLayer/:tracingId controllers.AnnotationController.editAnnotationLayer(typ: String, id: ObjectId, tracingId: String)
160159

161160
PATCH /annotations/:typ/:id/finish controllers.AnnotationController.finish(typ: String, id: ObjectId, timestamp: Long)
162161
PATCH /annotations/:typ/finish controllers.AnnotationController.finishAll(typ: String, timestamp: Long)

tools/postgres/schema.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ CREATE TABLE webknossos.releaseInformation (
2121
schemaVersion BIGINT NOT NULL
2222
);
2323

24-
INSERT INTO webknossos.releaseInformation(schemaVersion) values(139);
24+
INSERT INTO webknossos.releaseInformation(schemaVersion) values(140);
2525
COMMIT TRANSACTION;
2626

2727

@@ -59,7 +59,7 @@ CREATE TABLE webknossos.annotation_layers(
5959
typ webknossos.ANNOTATION_LAYER_TYPE NOT NULL,
6060
name TEXT NOT NULL CHECK (name ~* '^[A-Za-z0-9\-_\.\$]+$'),
6161
statistics JSONB NOT NULL,
62-
UNIQUE (name, _annotation),
62+
UNIQUE (name, _annotation) DEFERRABLE INITIALLY DEFERRED,
6363
PRIMARY KEY (_annotation, tracingId),
6464
CONSTRAINT statisticsIsJsonObject CHECK(jsonb_typeof(statistics) = 'object')
6565
);

unreleased_changes/8896.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Fixed
2+
- Fixed a bug where annotation updates would fail if multiple annotation layers are renamed such that a previously-taken name is then used by another layer.
3+
4+
### Postgres Evolutions
5+
- [140-annotation-layer-name-check-deferrable.sql](conf/evolutions/140-annotation-layer-name-check-deferrable.sql)

0 commit comments

Comments
 (0)