/
Model.scala
117 lines (106 loc) · 4.28 KB
/
Model.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package scala.slick.model
import scala.slick.ast.ColumnOption
/** Qualified name of a database table */
case class QualifiedName(table: String, schema: Option[String]=None, catalog: Option[String]=None)
case class Table(
name: QualifiedName,
columns: Seq[Column],
primaryKey: Option[PrimaryKey],
foreignKeys: Seq[ForeignKey],
indices: Seq[Index]
){
require( name.table != "", "name cannot be empty string" )
}
/** @param tpe qualified Scala type, e.g. java.sql.Date */
case class Column(
name: String,
table: QualifiedName,
tpe: String,
nullable: Boolean,
options: Set[ColumnOption[_]]
){
require( name != "", "name cannot be empty string" )
}
case class PrimaryKey(
name: Option[String],
table: QualifiedName,
columns: Seq[Column]
){
require( !name.exists(_ == ""), "name cannot be empty string" )
}
case class ForeignKey(
name: Option[String],
referencingTable: QualifiedName,
referencingColumns: Seq[Column],
referencedTable: QualifiedName,
referencedColumns: Seq[Column],
onUpdate: ForeignKeyAction,
onDelete: ForeignKeyAction
){
require( !name.exists(_ == ""), "name cannot be empty string" )
}
sealed abstract class ForeignKeyAction(val action: String)
object ForeignKeyAction {
case object Cascade extends ForeignKeyAction("CASCADE")
case object Restrict extends ForeignKeyAction("RESTRICT")
case object NoAction extends ForeignKeyAction("NO ACTION")
case object SetNull extends ForeignKeyAction("SET NULL")
case object SetDefault extends ForeignKeyAction("SET DEFAULT")
}
case class Index(
name: Option[String],
table: QualifiedName,
columns: Seq[Column],
unique: Boolean
){
require( !name.exists(_ == ""), "name cannot be empty string" )
}
/**
* A container class for Slick's data model
* The model can have circular references (e.g. a table has a foreign key which points back to the table).
* The references are broken apart by using names instead of object references for back references.
*/
case class Model(
tables: Seq[Table]
){
lazy val tablesByName = tables.map(t => t.name->t).toMap
/**
* Verifies consistency of the model by checking for duplicate names and references to non-existing entities.
* In case such things are found, throws an AssertionError.
*/
def assertConsistency{
assert(tables.size == tables.map(_.name).distinct.size, "duplicate tables names detected")
tables.foreach{ table =>
import table._
assert(columns.size == columns.map(_.name).distinct.size, "duplicate column names detected")
def msg( what: String, where: String ) = s"Reference to non-existent $what in $where of table $table."
primaryKey.foreach{ pk =>
assert( tablesByName.isDefinedAt(pk.table), msg("table "+pk.table,"primary key "+pk) )
pk.columns.foreach{ column =>
assert( table.columns.contains(column), msg("column "+column,"primary key "+pk) )
}
}
assert(foreignKeys.filter(_.name!=None).size == foreignKeys.filter(_.name!=None).map(_.name).distinct.size, "duplicate foreign key names detected")
foreignKeys.foreach{ fk =>
assert( tablesByName.isDefinedAt(fk.referencedTable), msg("table "+fk.referencedTable,"foreign key "+fk) )
assert( tablesByName.isDefinedAt(fk.referencingTable), msg("table "+fk.referencingTable,"foreign key "+fk) )
val pkTable = tablesByName(fk.referencedTable)
val fkTable = tablesByName(fk.referencingTable)
assert( table == fkTable, "Referencing table $fkTable does not match table $table the foreign key $fk is contained in." )
fk.referencedColumns.foreach{ pkColumn =>
assert( pkTable.columns.contains(pkColumn), msg("column "+pkColumn+" of table "+pkTable,"foreign key "+fk) )
}
fk.referencingColumns.foreach{ fkColumn =>
assert( fkTable.columns.contains(fkColumn), msg("column "+fkColumn+" of table "+fkTable,"foreign key "+fk) )
}
}
assert(indices.filter(_.name!=None).size == indices.filter(_.name!=None).map(_.name).distinct.size, "duplicate index names detected")
indices.foreach{ idx =>
assert( tablesByName.isDefinedAt(idx.table), msg("table "+idx.table,"index "+idx) )
idx.columns.foreach{ column =>
assert( table.columns.contains(column), msg("column "+column,"index "+idx) )
}
}
}
}
}