Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct delete cascade handling in case of replacing a link cell #176

Merged
merged 7 commits into from
Apr 23, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -321,61 +321,59 @@ class TableauxModel(
}
}

def updateCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A): Future[Cell[_]] = {
private def updateOrReplaceValue[A](
table: Table,
columnId: ColumnId,
rowId: RowId,
value: A,
replace: Boolean = false
): Future[Cell[_]] = {
for {
_ <- (table.tableType, columnId) match {
case (SettingsTable, 1 | 2) =>
Future.failed(ForbiddenException("can't update key cell of a settings table", "cell"))
case _ => Future.successful(())
}
_ <- checkForSettingsTable(table, columnId, "can't update key cell of a settings table")

column <- retrieveColumn(table, columnId)
_ <- checkValueTypeForColumn(column, value)

_ <- updateRowModel.updateRow(column.table, rowId, Seq((column, value)))

_ <- invalidateCellAndDependentColumns(column, rowId)

updatedCell <- retrieveCell(column, rowId)
} yield updatedCell
}

def replaceCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A): Future[Cell[_]] = {
for {
_ <- (table.tableType, columnId) match {
case (SettingsTable, 1 | 2) =>
Future.failed(ForbiddenException("can't update key cell of a settings table", "cell"))
case _ => Future.successful(())
_ <- if (replace) {
updateRowModel.clearRowWithValues(table, rowId, Seq((column, value)), deleteRow)
} else {
Future.successful(())
}

column <- retrieveColumn(table, columnId)
_ <- checkValueTypeForColumn(column, value)

_ <- updateRowModel.clearRow(table, rowId, Seq(column), deleteRow)
_ <- updateRowModel.updateRow(table, rowId, Seq((column, value)))

_ <- invalidateCellAndDependentColumns(column, rowId)

replacedCell <- retrieveCell(column, rowId)
} yield replacedCell
changedCell <- retrieveCell(column, rowId)
} yield changedCell
}

def updateCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A): Future[Cell[_]] =
updateOrReplaceValue(table, columnId, rowId, value)

def replaceCellValue[A](table: Table, columnId: ColumnId, rowId: RowId, value: A): Future[Cell[_]] =
updateOrReplaceValue(table, columnId, rowId, value, replace = true)

def clearCellValue(table: Table, columnId: ColumnId, rowId: RowId): Future[Cell[_]] = {
for {
_ <- (table.tableType, columnId) match {
case (SettingsTable, 1 | 2) =>
Future.failed(ForbiddenException("can't update key cell of a settings table", "cell"))
case _ => Future.successful(())
}
_ <- checkForSettingsTable(table, columnId, "can't clear key cell of a settings table")

column <- retrieveColumn(table, columnId)

_ <- updateRowModel.clearRow(table, rowId, Seq(column), deleteRow)

_ <- invalidateCellAndDependentColumns(column, rowId)

replacedCell <- retrieveCell(column, rowId)
} yield replacedCell
clearedCell <- retrieveCell(column, rowId)
} yield clearedCell
}

private def checkForSettingsTable[A](table: Table, columnId: ColumnId, exceptionMessage: String) = {
(table.tableType, columnId) match {
case (SettingsTable, 1 | 2) =>
Future.failed(ForbiddenException(exceptionMessage, "cell"))
case _ => Future.successful(())
}
}

private def invalidateCellAndDependentColumns(column: ColumnType[_], rowId: RowId): Future[Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ class UpdateRowModel(val connection: DatabaseConnection) extends DatabaseQuery w
def clearRow(
table: Table,
rowId: RowId,
values: Seq[(ColumnType[_])],
columns: Seq[ColumnType[_]],
deleteRowFn: (Table, RowId) => Future[EmptyObject]
): Future[Unit] = {
val (simple, multis, links, attachments) = ColumnType.splitIntoTypes(values)
val (simple, multis, links, attachments) = ColumnType.splitIntoTypes(columns)

for {
_ <- if (simple.isEmpty) Future.successful(()) else updateSimple(table, rowId, simple.map((_, None)))
Expand All @@ -136,6 +136,28 @@ class UpdateRowModel(val connection: DatabaseConnection) extends DatabaseQuery w
} yield ()
}

def clearRowWithValues(
table: Table,
rowId: RowId,
values: Seq[(ColumnType[_], _)],
deleteRowFn: (Table, RowId) => Future[EmptyObject]
): Future[Unit] = {
ColumnType.splitIntoTypesWithValues(values) match {
case Failure(ex) =>
Future.failed(ex)

case Success((simple, multis, links, attachments)) =>
// we only care about links because of delete cascade handling
// for simple, multis, and attachments do same as in clearRow()
for {
_ <- if (simple.isEmpty) Future.successful(()) else updateSimple(table, rowId, simple.unzip._1.map((_, None)))
_ <- if (multis.isEmpty) Future.successful(()) else clearTranslation(table, rowId, multis.unzip._1)
_ <- if (links.isEmpty) Future.successful(()) else clearLinksWithValues(table, rowId, links, deleteRowFn)
_ <- if (attachments.isEmpty) Future.successful(()) else clearAttachments(table, rowId, attachments.unzip._1)
} yield ()
}
}

private def clearTranslation(table: Table, rowId: RowId, columns: Seq[SimpleValueColumn[_]]): Future[_] = {
val setExpression = columns
.map({
Expand All @@ -148,29 +170,38 @@ class UpdateRowModel(val connection: DatabaseConnection) extends DatabaseQuery w
} yield ()
}

private def clearLinks(
private def clearLinksWithValues(
table: Table,
rowId: RowId,
columns: Seq[LinkColumn],
deleteRowFn: (Table, RowId) => Future[EmptyObject]
columnsWithValues: Seq[(LinkColumn, Seq[RowId])],
deleteRowFn: (Table, RowId) => Future[EmptyObject],
): Future[_] = {
val futureSequence = columns.map(column => {
val fromIdColumn = column.linkDirection.fromSql
val futureSequence = columnsWithValues.map({
case (column, values) => {
val fromIdColumn = column.linkDirection.fromSql

val linkTable = s"link_table_${column.linkId}"
val linkTable = s"link_table_${column.linkId}"

for {
_ <- if (column.linkDirection.constraint.deleteCascade) {
deleteLinkedRows(table, rowId, column, deleteRowFn)
} else {
connection.query(s"DELETE FROM $linkTable WHERE $fromIdColumn = ?", Json.arr(rowId))
}
} yield ()
for {
_ <- if (column.linkDirection.constraint.deleteCascade) {
deleteLinkedRows(table, rowId, column, deleteRowFn, values)
} else {
connection.query(s"DELETE FROM $linkTable WHERE $fromIdColumn = ?", Json.arr(rowId))
}
} yield ()
}
})

Future.sequence(futureSequence)
}

private def clearLinks(
table: Table,
rowId: RowId,
columns: Seq[LinkColumn],
deleteRowFn: (Table, RowId) => Future[EmptyObject],
): Future[_] = clearLinksWithValues(table, rowId, columns.map((_, Seq.empty)), deleteRowFn)

private def clearAttachments(table: Table, rowId: RowId, columns: Seq[AttachmentColumn]): Future[_] = {
val cleared = columns.map((c: AttachmentColumn) => attachmentModel.deleteAll(table.id, c.id, rowId))
Future.sequence(cleared)
Expand All @@ -180,8 +211,38 @@ class UpdateRowModel(val connection: DatabaseConnection) extends DatabaseQuery w
table: Table,
rowId: RowId,
column: LinkColumn,
deleteRowFn: (Table, RowId) => Future[EmptyObject]
deleteRowFn: (Table, RowId) => Future[EmptyObject],
newForeignRowIds: Seq[RowId] = Seq.empty
): Future[_] = {
val linkTable = s"link_table_${column.linkId}"
val fromIdColumn = column.linkDirection.fromSql
val toIdColumn = column.linkDirection.toSql

// TODO here we could end up in a endless loop,
// TODO but we could argue that a schema like this doesn't make sense
retrieveLinkedRows(table, rowId, column)
.flatMap(foreignRowIds => {
val futures = foreignRowIds
.map(foreignRowId => {
// only delete foreign row if it's not part of new values...
// ... otherwise delete link
if (newForeignRowIds.contains(foreignRowId)) {
connection.query(s"DELETE FROM $linkTable WHERE $fromIdColumn = ? AND $toIdColumn = ?",
Json.arr(rowId, foreignRowId))
} else {
deleteRowFn(column.to.table, foreignRowId)
}
})

Future.sequence(futures)
})
}

private def retrieveLinkedRows(
table: Table,
rowId: RowId,
column: LinkColumn
): Future[Seq[Long]] = {
val toIdColumn = column.linkDirection.toSql
val fromIdColumn = column.linkDirection.fromSql
val linkTable = s"link_table_${column.linkId}"
Expand All @@ -196,19 +257,12 @@ class UpdateRowModel(val connection: DatabaseConnection) extends DatabaseQuery w
| (SELECT COUNT(*) FROM $linkTable sub WHERE sub.$toIdColumn = lt.$toIdColumn) = 1;""".stripMargin

// select foreign rows which are only used once
// these which are only used once should be deleted then
// these should be deleted then
// do this in a recursive manner
connection
.query(selectForeignRows, Json.arr(rowId))
.map(resultObjectToJsonArray)
.map(_.map(_.getLong(0)))
// TODO here we could end up in a endless loop,
// TODO but we could argue that a schema like this doesn't make sense
.flatMap(foreignRowIdSeq => {
val futures = foreignRowIdSeq.map(deleteRowFn(column.to.table, _))

Future.sequence(futures)
})
.map(_.map(_.getLong(0).asInstanceOf[Long]))
}

def deleteLink(
Expand Down
Loading