Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
2 changes: 2 additions & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).
- Example command for the migration: `PG_PASSWORD=myPassword python main.py --src localhost:7500 --dst localhost:7155 --num_threads 20 --postgres webknossos@localhost:5430/webknossos`

### Postgres Evolutions:

- [126-mag-real-paths.sql](conf/evolutions/126-mag-real-paths.sql)
18 changes: 18 additions & 0 deletions app/controllers/DatasetController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ object SegmentAnythingMaskParameters {
implicit val jsonFormat: Format[SegmentAnythingMaskParameters] = Json.format[SegmentAnythingMaskParameters]
}

case class MagLinkInfo(mag: DatasetMagInfo, linkedMags: Seq[DatasetMagInfo])

object MagLinkInfo {
implicit val jsonFormat: Format[MagLinkInfo] = Json.format[MagLinkInfo]
}

class DatasetController @Inject()(userService: UserService,
userDAO: UserDAO,
datasetService: DatasetService,
Expand Down Expand Up @@ -126,6 +132,18 @@ class DatasetController @Inject()(userService: UserService,
}
}

def getLinkedMags(datasetId: ObjectId, dataLayerName: String): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
_ <- datasetDAO.findOne(datasetId) ?~> notFoundMessage(datasetId) ~> NOT_FOUND
_ <- Fox.bool2Fox(request.identity.isAdmin) ?~> "notAllowed" ~> FORBIDDEN
magsAndLinkedMags <- datasetService.getPathsForDatalayer(datasetId, dataLayerName)
returnValues = magsAndLinkedMags.map {
case (mag, linkedMags) => MagLinkInfo(mag, linkedMags)
}
} yield Ok(Json.toJson(returnValues))
}

def exploreRemoteDataset(): Action[List[WKExploreRemoteLayerParameters]] =
sil.SecuredAction.async(validateJson[List[WKExploreRemoteLayerParameters]]) { implicit request =>
for {
Expand Down
18 changes: 17 additions & 1 deletion app/controllers/WKRemoteDataStoreController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.scalableminds.webknossos.datastore.controllers.JobExportProperties
import com.scalableminds.webknossos.datastore.models.UnfinishedUpload
import com.scalableminds.webknossos.datastore.models.datasource.DataSourceId
import com.scalableminds.webknossos.datastore.models.datasource.inbox.{InboxDataSourceLike => InboxDataSource}
import com.scalableminds.webknossos.datastore.services.DataStoreStatus
import com.scalableminds.webknossos.datastore.services.{DataSourcePathInfo, DataStoreStatus}
import com.scalableminds.webknossos.datastore.services.uploading.{
LinkedLayerIdentifier,
ReserveAdditionalInformation,
Expand Down Expand Up @@ -221,6 +221,22 @@ class WKRemoteDataStoreController @Inject()(
}
}

def updatePaths(name: String, key: String): Action[JsValue] = Action.async(parse.json) { implicit request =>
dataStoreService.validateAccess(name, key) { _ =>
request.body.validate[List[DataSourcePathInfo]] match {
case JsSuccess(infos, _) =>
for {
_ <- datasetService.updateRealPaths(infos)(GlobalAccessContext)
} yield {
JsonOk
}
case e: JsError =>
logger.warn("Data store reported invalid json for data source paths.")
Fox.successful(JsonBadRequest(JsError.toJson(e)))
Comment on lines +231 to +233
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason to validate explicitly and log errors here, rather than using a playBodyParser to build an Action[List[DataSourcePathInfo]]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that was just copied from the function above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should this be changed for all methods in this controller?

Copy link
Member

@fm3 fm3 Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it for updateAll in #8402 – but I’d say it’s not super important to update this now. Both is valid. The older variant has the benefit of controller-side logging (but I don’t think I ever saw this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #8406 for that.

}
}
}

def deleteDataset(name: String, key: String): Action[JsValue] = Action.async(parse.json) { implicit request =>
dataStoreService.validateAccess(name, key) { _ =>
for {
Expand Down
84 changes: 80 additions & 4 deletions app/models/dataset/Dataset.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.scalableminds.webknossos.datastore.models.datasource.{
ThinPlateSplineCorrespondences,
DataLayerLike => DataLayer
}
import com.scalableminds.webknossos.datastore.services.MagPathInfo
import com.scalableminds.webknossos.schema.Tables._
import controllers.DatasetUpdateParameters

Expand All @@ -29,6 +30,7 @@ import models.organization.OrganizationDAO
import play.api.i18n.{Messages, MessagesProvider}
import play.api.libs.json._
import play.utils.UriEncoding
import slick.dbio.DBIO
import slick.jdbc.PostgresProfile.api._
import slick.jdbc.TransactionIsolation.Serializable
import slick.lifted.Rep
Expand Down Expand Up @@ -707,18 +709,45 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
}
}

class DatasetMagsDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) extends SimpleSQLDAO(sqlClient) {
private def parseRow(row: DatasetMagsRow): Fox[Vec3Int] =
case class DatasetMagInfo(datasetId: ObjectId,
dataLayerName: String,
mag: Vec3Int,
path: String,
realPath: String,
hasLocalData: Boolean)

object DatasetMagInfo {
implicit val jsonFormat: Format[DatasetMagInfo] = Json.format[DatasetMagInfo]
}
class DatasetMagsDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
extends SQLDAO[MagPathInfo, DatasetMagsRow, DatasetMags](sqlClient) {
protected val collection = DatasetMags

protected def idColumn(x: DatasetMags): Rep[String] = x._Dataset

protected def isDeletedColumn(x: DatasetMags): Rep[Boolean] = false

protected def parse(row: DatasetMagsRow): Fox[MagPathInfo] =
for {
mag <- Vec3Int.fromList(parseArrayLiteral(row.mag).map(_.toInt)) ?~> "could not parse mag"
} yield
MagPathInfo(row.datalayername,
mag,
row.path.getOrElse("uninitialized"),
row.realpath.getOrElse("uninitialized"),
hasLocalData = row.haslocaldata)

private def parseMag(magArrayLiteral: String): Fox[Vec3Int] =
for {
mag <- Vec3Int.fromList(parseArrayLiteral(magArrayLiteral).map(_.toInt)) ?~> "could not parse mag"
} yield mag

def findMagForLayer(datasetId: ObjectId, dataLayerName: String): Fox[List[Vec3Int]] =
for {
rows <- run(DatasetMags.filter(r => r._Dataset === datasetId.id && r.datalayername === dataLayerName).result)
.map(_.toList)
rowsParsed <- Fox.combined(rows.map(parseRow)) ?~> "could not parse mag row"
} yield rowsParsed
mags <- Fox.combined(rows.map(r => parseMag(r.mag))) ?~> "could not parse mag row"
} yield mags

def updateMags(datasetId: ObjectId, dataLayersOpt: Option[List[DataLayer]]): Fox[Unit] = {
val clearQuery = q"DELETE FROM webknossos.dataset_mags WHERE _dataset = $datasetId".asUpdate
Expand All @@ -733,6 +762,53 @@ class DatasetMagsDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionConte
replaceSequentiallyAsTransaction(clearQuery, insertQueries)
}

def updateMagPathsForDataset(datasetId: ObjectId, magPaths: List[MagPathInfo]): Fox[Unit] =
for {
_ <- Fox.successful(())
updateQueries = magPaths.map(pathInfo => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
updateQueries = magPaths.map(pathInfo => {
updateQueries = magPathInfos.map(magPathInfo => {

(Seems cleaner to me if the variable names match, and also match the type. Or, for the inner-most, use an obvious abbreviation like m. That way the reader doesn’t have to understand that these things are all the same)

val magLiteral = s"(${pathInfo.mag.x}, ${pathInfo.mag.y}, ${pathInfo.mag.z})"
q"""UPDATE webknossos.dataset_mags
SET path = ${pathInfo.path}, realPath = ${pathInfo.realPath}, haslocaldata = ${pathInfo.hasLocalData}
WHERE _dataset = $datasetId
AND datalayername = ${pathInfo.layerName}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please follow capitalization from schema.sql here, dataLayerName and hasLocalData

AND mag = CAST($magLiteral AS webknossos.vector3)""".asUpdate
})
composedQuery = DBIO.sequence(updateQueries)
_ <- run(
composedQuery.transactionally.withTransactionIsolation(Serializable),
retryCount = 50,
retryIfErrorContains = List(transactionSerializationError)
)
} yield ()

def findPathsForDatasetAndDatalayer(datasetId: ObjectId, dataLayerName: String): Fox[List[DatasetMagInfo]] =
for {
rows <- run(q"""SELECT $columns
FROM webknossos.dataset_mags
WHERE _dataset = $datasetId
AND datalayername = $dataLayerName""".as[DatasetMagsRow])
magInfos <- Fox.combined(rows.toList.map(parse))
datasetMagInfos = magInfos.map(magInfo =>
DatasetMagInfo(datasetId, magInfo.layerName, magInfo.mag, magInfo.path, magInfo.realPath, magInfo.hasLocalData))
} yield datasetMagInfos

def findAllByRealPath(realPath: String): Fox[List[DatasetMagInfo]] =
for {
rows <- run(q"""SELECT $columns
FROM webknossos.dataset_mags
WHERE realPath = $realPath""".as[DatasetMagsRow])
mags <- Fox.serialCombined(rows.toList)(r => parseMag(r.mag))
magInfos = rows.toList.zip(mags).map {
case (row, mag) =>
DatasetMagInfo(ObjectId(row._Dataset),
row.datalayername,
mag,
row.path.getOrElse("uninitialized"),
row.realpath.getOrElse("uninitialized"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use None for this case? I feel like having a custom string literal in the json could cause headaches down the line. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this to avoid having options in the DatasetMagInfo, since this is only a theoretical case either way. The path and realPath are set when the dataset is reported, so it would only be empty between the two report functions that are separate at the moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn’t call it a theoretical case, since there is a real time difference between the two report functions. That means that this value will, at least for short durations, occur in the database and downstream functions will have to be able to handle it. If a function lists all datasets that use the same layer, anad does string comparison, this would return all that have "uninitialized", which could become a nonsensical list. I’d rather have the Option if we cannot prevent that this value is ever inserted.

row.haslocaldata)
}
} yield magInfos

}

class DatasetLayerDAO @Inject()(
Expand Down
33 changes: 32 additions & 1 deletion app/models/dataset/DatasetService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import com.scalableminds.webknossos.datastore.models.datasource.{
DataLayerLike => DataLayer
}
import com.scalableminds.webknossos.datastore.rpc.RPC
import com.scalableminds.webknossos.datastore.services.DataSourcePathInfo
import com.typesafe.scalalogging.LazyLogging
import models.folder.FolderDAO
import models.organization.{Organization, OrganizationDAO}
import models.team._
import models.user.{User, UserService}
import net.liftweb.common.{Box, Full}
import net.liftweb.common.{Box, Empty, EmptyBox, Full}
import play.api.libs.json.{JsObject, Json}
import security.RandomIDGenerator
import utils.WkConf
Expand All @@ -33,6 +34,7 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
dataStoreDAO: DataStoreDAO,
datasetLastUsedTimesDAO: DatasetLastUsedTimesDAO,
datasetDataLayerDAO: DatasetLayerDAO,
datasetMagsDAO: DatasetMagsDAO,
teamDAO: TeamDAO,
folderDAO: FolderDAO,
dataStoreService: DataStoreService,
Expand Down Expand Up @@ -338,6 +340,35 @@ class DatasetService @Inject()(organizationDAO: OrganizationDAO,
_ <- datasetDAO.updateUploader(dataset._id, Some(_uploader)) ?~> "dataset.uploader.forbidden"
} yield ()

private def updateRealPath(pathInfo: DataSourcePathInfo)(implicit ctx: DBAccessContext): Fox[Unit] =
if (pathInfo.magPathInfos.isEmpty) {
Fox.successful(())
} else {
val dataset = datasetDAO.findOneByDataSourceId(pathInfo.dataSourceId).futureBox
dataset.flatMap {
case Full(dataset) => datasetMagsDAO.updateMagPathsForDataset(dataset._id, pathInfo.magPathInfos)
case Empty => // Dataset reported but ignored (non-existing/forbidden org)
Fox.successful(())
case e: EmptyBox =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, TIL about the EmptyBox trait. Looks like we should have used this in a lot of places where we match against Failure (but miss ParamFailure?). But I guess we instantiate so few ParamFailures that this hasn’t caused any real problems.

Fox.failure("dataset.notFound", e)
}
}

def updateRealPaths(pathInfos: List[DataSourcePathInfo])(implicit ctx: DBAccessContext): Fox[Unit] =
for {
_ <- Fox.serialCombined(pathInfos)(updateRealPath)
} yield ()

def getPathsForDatalayer(datasetId: ObjectId, layerName: String): Fox[List[(DatasetMagInfo, List[DatasetMagInfo])]] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use capital L in DataLayer/dataLayer for consistency. There are a couple of usages.

for {
magInfos <- datasetMagsDAO.findPathsForDatasetAndDatalayer(datasetId, layerName)
magInfosAndLinkedMags <- Fox.serialCombined(magInfos)(magInfo =>
for {
pathInfos <- datasetMagsDAO.findAllByRealPath(magInfo.realPath)
} yield (magInfo, pathInfos.filter(!_.equals(magInfo))))

} yield magInfosAndLinkedMags

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding input validation for layer name.

The method should validate the layer name before querying the database.

Apply this diff to add validation:

 def getPathsForDatalayer(datasetId: ObjectId, layerName: String): Fox[List[(DatasetMagInfo, List[DatasetMagInfo])]] =
   for {
+    _ <- assertValidLayerNameLax(layerName)
     magInfos <- datasetMagsDAO.findPathsForDatasetAndDatalayer(datasetId, layerName)
     magInfosAndLinkedMags <- Fox.serialCombined(magInfos)(magInfo =>
       for {
         pathInfos <- datasetMagsDAO.findAllByRealPath(magInfo.realPath)
       } yield (magInfo, pathInfos.filter(!_.equals(magInfo))))
   } yield magInfosAndLinkedMags

Committable suggestion skipped: line range outside the PR's diff.

def publicWrites(dataset: Dataset,
requestingUserOpt: Option[User],
organization: Option[Organization] = None,
Expand Down
12 changes: 12 additions & 0 deletions conf/evolutions/126-mag-real-paths.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
START TRANSACTION;

do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 125, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql;

ALTER TABLE webknossos.dataset_mags ADD COLUMN realPath TEXT;
ALTER TABLE webknossos.dataset_mags ADD COLUMN path TEXT;
ALTER TABLE webknossos.dataset_mags ADD COLUMN hasLocalData BOOLEAN NOT NULL DEFAULT false;

UPDATE webknossos.releaseInformation SET schemaVersion = 126;


COMMIT TRANSACTION;
11 changes: 11 additions & 0 deletions conf/evolutions/reversions/126-mag-real-paths.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
START TRANSACTION;

do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 126, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql;

ALTER TABLE webknossos.dataset_mags DROP COLUMN realPath;
ALTER TABLE webknossos.dataset_mags DROP COLUMN path;
ALTER TABLE webknossos.dataset_mags DROP COLUMN hasLocalData;

UPDATE webknossos.releaseInformation SET schemaVersion = 125;

COMMIT TRANSACTION;
2 changes: 2 additions & 0 deletions conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ DELETE /datasets/:datasetId/sharingToken
PATCH /datasets/:datasetId/teams controllers.DatasetController.updateTeams(datasetId: ObjectId)
GET /datasets/:datasetId/layers/:layer/thumbnail controllers.DatasetController.thumbnail(datasetId: ObjectId, layer: String, w: Option[Int], h: Option[Int], mappingName: Option[String], sharingToken: Option[String])
POST /datasets/:datasetId/layers/:layer/segmentAnythingMask controllers.DatasetController.segmentAnythingMask(datasetId: ObjectId, layer: String, intensityMin: Option[Float], intensityMax: Option[Float])
GET /datasets/:datasetId/layers/:layer/linkedMags controllers.DatasetController.getLinkedMags(datasetId: ObjectId, layer: String)
PUT /datasets/:datasetId/clearThumbnailCache controllers.DatasetController.removeFromThumbnailCache(datasetId: ObjectId)
GET /datasets/:datasetName/isValidNewName controllers.DatasetController.isValidNewName(datasetName: String)
GET /datasets/:datasetId controllers.DatasetController.read(datasetId: ObjectId, sharingToken: Option[String])
Expand All @@ -108,6 +109,7 @@ DELETE /folders/:id
GET /datastores controllers.DataStoreController.list()
PUT /datastores/:name/datasource controllers.WKRemoteDataStoreController.updateOne(name: String, key: String)
PUT /datastores/:name/datasources controllers.WKRemoteDataStoreController.updateAll(name: String, key: String)
PUT /datastores/:name/datasources/paths controllers.WKRemoteDataStoreController.updatePaths(name: String, key: String)
PATCH /datastores/:name/status controllers.WKRemoteDataStoreController.statusUpdate(name: String, key: String)
POST /datastores/:name/reserveUpload controllers.WKRemoteDataStoreController.reserveDatasetUpload(name: String, key: String, token: String)
GET /datastores/:name/getUnfinishedUploadsForUser controllers.WKRemoteDataStoreController.getUnfinishedUploadsForUser(name: String, key: String, token: String, organizationName: String)
Expand Down
52 changes: 26 additions & 26 deletions test/db/dataSet_mags.csv
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
_dataSet,dataLayerName,mag,scale
'59e9cfbdba632ac2ab8b23b3','color_1','(1,1,1)'
'59e9cfbdba632ac2ab8b23b3','color_1','(2,2,2)'
'59e9cfbdba632ac2ab8b23b3','color_1','(4,4,4)'
'59e9cfbdba632ac2ab8b23b3','color_1','(8,8,8)'
'59e9cfbdba632ac2ab8b23b3','color_1','(16,16,16)'
'59e9cfbdba632ac2ab8b23b3','color_2','(1,1,1)'
'59e9cfbdba632ac2ab8b23b3','color_2','(2,2,2)'
'59e9cfbdba632ac2ab8b23b3','color_2','(4,4,4)'
'59e9cfbdba632ac2ab8b23b3','color_2','(8,8,8)'
'59e9cfbdba632ac2ab8b23b3','color_2','(16,16,16)'
'59e9cfbdba632ac2ab8b23b3','color_3','(1,1,1)'
'59e9cfbdba632ac2ab8b23b3','color_3','(2,2,2)'
'59e9cfbdba632ac2ab8b23b3','color_3','(4,4,4)'
'59e9cfbdba632ac2ab8b23b3','color_3','(8,8,8)'
'59e9cfbdba632ac2ab8b23b3','color_3','(16,16,16)'
'59e9cfbdba632ac2ab8b23b5','color','(1,1,1)'
'59e9cfbdba632ac2ab8b23b5','color','(2,2,1)'
'59e9cfbdba632ac2ab8b23b5','color','(4,4,1)'
'59e9cfbdba632ac2ab8b23b5','color','(8,8,2)'
'59e9cfbdba632ac2ab8b23b5','color','(16,16,4)'
'59e9cfbdba632ac2ab8b23b5','segmentation','(1,1,1)'
'59e9cfbdba632ac2ab8b23b5','segmentation','(2,2,1)'
'59e9cfbdba632ac2ab8b23b5','segmentation','(4,4,1)'
'59e9cfbdba632ac2ab8b23b5','segmentation','(8,8,2)'
'59e9cfbdba632ac2ab8b23b5','segmentation','(16,16,4)'
_dataSet,dataLayerName,mag,scale,path,realPath,hasLocalData
'59e9cfbdba632ac2ab8b23b3','color_1','(1,1,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_1','(2,2,2)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_1','(4,4,4)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_1','(8,8,8)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_1','(16,16,16)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_2','(1,1,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_2','(2,2,2)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_2','(4,4,4)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_2','(8,8,8)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_2','(16,16,16)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_3','(1,1,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_3','(2,2,2)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_3','(4,4,4)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_3','(8,8,8)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b3','color_3','(16,16,16)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','color','(1,1,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','color','(2,2,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','color','(4,4,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','color','(8,8,2)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','color','(16,16,4)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','segmentation','(1,1,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','segmentation','(2,2,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','segmentation','(4,4,1)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','segmentation','(8,8,2)','uninitialized','uninitialized',false
'59e9cfbdba632ac2ab8b23b5','segmentation','(16,16,4)','uninitialized','uninitialized',false
5 changes: 4 additions & 1 deletion tools/postgres/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ CREATE TABLE webknossos.releaseInformation (
schemaVersion BIGINT NOT NULL
);

INSERT INTO webknossos.releaseInformation(schemaVersion) values(125);
INSERT INTO webknossos.releaseInformation(schemaVersion) values(126);
COMMIT TRANSACTION;


Expand Down Expand Up @@ -178,6 +178,9 @@ CREATE TABLE webknossos.dataset_mags(
_dataset CHAR(24) NOT NULL,
dataLayerName VARCHAR(256),
mag webknossos.VECTOR3 NOT NULL,
path TEXT,
realPath TEXT,
hasLocalData BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (_dataset, dataLayerName, mag)
);

Expand Down
Loading