/
Model.scala
133 lines (121 loc) · 4.77 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package slick.model
import slick.ast.ColumnOption
trait ModelOption[T]
trait TableOption[T]
trait PrimaryKeyOption[T]
trait ForeignKeyOption[T]
trait IndexOption[T]
/** Qualified name of a database table */
case class QualifiedName(table: String, schema: Option[String]=None, catalog: Option[String]=None){
/** human readable String representation */
def asString = catalog.map(_+".").getOrElse("") +
schema.map(_+".").getOrElse("") +
table
}
case class Table(
name: QualifiedName,
columns: Seq[Column],
primaryKey: Option[PrimaryKey],
foreignKeys: Seq[ForeignKey],
indices: Seq[Index],
options: Set[TableOption[_]] = Set()
){
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[_]] = Set()
){
require( name != "", "name cannot be empty string" )
}
case class PrimaryKey(
name: Option[String],
table: QualifiedName,
columns: Seq[Column],
options: Set[PrimaryKeyOption[_]] = Set()
){
require( !name.contains(""), "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,
options: Set[ForeignKeyOption[_]] = Set()
){
require( !name.contains(""), "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,
options: Set[IndexOption[_]] = Set()
){
require( !name.contains(""), "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],
options: Set[ModelOption[_]] = Set()
){
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.count(_.name.isDefined) == foreignKeys.filter(_.name.isDefined).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.count(_.name.isDefined) == indices.filter(_.name.isDefined).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) )
}
}
}
}
}