Skip to content

Commit

Permalink
Add dbml parser module (#21)
Browse files Browse the repository at this point in the history
Adds module to allow for creating an in-memory model of DBML schemas.

To be done separately:
- Add support for Index parsing (currently hack in place)
- Parse column default values
- Include table aliases and table notes in modeling
- Pull out DBML modeling classes into shared module that Room translator can depend on (and remove duplication)
  • Loading branch information
julioz committed May 19, 2020
1 parent 8d7a388 commit 550451b
Show file tree
Hide file tree
Showing 16 changed files with 747 additions and 0 deletions.
23 changes: 23 additions & 0 deletions dbml-parser/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm'
}

sourceCompatibility = 1.8

repositories {
mavenCentral()
jcenter()
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testImplementation "junit:junit:4.12"
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
14 changes: 14 additions & 0 deletions dbml-parser/src/main/kotlin/com/zynger/floorplan/Column.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.zynger.floorplan

data class Column(
val rawValue: String,
val name: String,
val type: String,
val note: String? = null,
val primaryKey: Boolean = false,
val notNull: Boolean = false,
val increment: Boolean = false,
val reference: Reference? = null // not null when this column references another through a column attribute
) {
// example column: address varchar(255) [unique, not null, note: 'to include unit number']
}
6 changes: 6 additions & 0 deletions dbml-parser/src/main/kotlin/com/zynger/floorplan/Index.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.zynger.floorplan

data class Index(val name: String, val columnNames: List<String>? = null, val unique: Boolean = false){
// TODO should we enforce that indexes must have column names? as to make the property not nullable
//(urn) [name:'index_TimeToLives_urn', unique]
}
15 changes: 15 additions & 0 deletions dbml-parser/src/main/kotlin/com/zynger/floorplan/Parser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.zynger.floorplan

import com.zynger.floorplan.lex.LoneReferenceParser
import com.zynger.floorplan.lex.TableParser

object Parser {

fun parse(dbmlInput: String): Project {
val tables = TableParser.parseTables(dbmlInput)
val columnReferences = tables.map { it.columns }.flatten().mapNotNull { it.reference }
val references = LoneReferenceParser.parseReferences(dbmlInput) + columnReferences
return Project(tables, references)
}

}
6 changes: 6 additions & 0 deletions dbml-parser/src/main/kotlin/com/zynger/floorplan/Project.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.zynger.floorplan

data class Project(
val tables: List<Table>,
val reference: List<Reference>
)
27 changes: 27 additions & 0 deletions dbml-parser/src/main/kotlin/com/zynger/floorplan/Reference.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.zynger.floorplan

import java.lang.IllegalArgumentException

// Ref: trending_shows.show_id - shows.id [delete: cascade, update: cascade]
data class Reference(
val rawValue: String,
val fromTable: String,
val fromColumn: String,
val toTable: String,
val toColumn: String,
val referenceOrder: ReferenceOrder
)

enum class ReferenceOrder {
OneToOne, OneToMany, ManyToOne;
companion object {
fun fromString(str: String): ReferenceOrder {
return when (str.trim()) {
"-" -> OneToOne
">" -> ManyToOne
"<" -> OneToMany
else -> throw IllegalArgumentException("Could not parse $str as reference order.")
}
}
}
}
8 changes: 8 additions & 0 deletions dbml-parser/src/main/kotlin/com/zynger/floorplan/Table.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.zynger.floorplan

data class Table(
val rawValue: String,
val name: String,
val columns: List<Column>,
val indexes: List<Index> = emptyList()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.zynger.floorplan.lex

import com.zynger.floorplan.Column
import com.zynger.floorplan.Reference
import org.intellij.lang.annotations.Language

object ColumnParser {
@Language("RegExp") private const val WORD = """("\w+"|\w+)"""
@Language("RegExp") private const val COLUMN_NAME = """$WORD+"""
@Language("RegExp") private const val COLUMN_TYPE = WORD
@Language("RegExp") private const val COLUMN_PROPERTIES = """\[[^]]*]"""
private val COLUMN_REGEX = Regex("""$COLUMN_NAME\s+$COLUMN_TYPE(\s+$COLUMN_PROPERTIES|)\s*\n""")

// TODO parse column default values

fun parseColumns(tableName: String, columnsInput: String): List<Column> {
return COLUMN_REGEX.findAll(columnsInput).map {
val rawValue = it.groups[0]!!.value
val name = it.groups[1]!!.value.removeSurroundQuotes()
val type = it.groups[2]!!.value
val columnProperties = it.groups[3]!!.value.trim()
val notNull = columnProperties.contains("not null")
val pk = columnProperties.contains("pk")
val increment = columnProperties.contains("increment")
val reference: Reference? = ColumnReferenceParser.parse(tableName, name, columnProperties)

Column(
rawValue = rawValue,
name = name,
type = type,
primaryKey = pk,
notNull = notNull,
increment = increment,
reference = reference
)
}.toList()
}

private fun String.removeSurroundQuotes(): String {
return this.removeSurrounding("\"")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.zynger.floorplan.lex

import com.zynger.floorplan.Reference
import com.zynger.floorplan.ReferenceOrder
import org.intellij.lang.annotations.Language

object ColumnReferenceParser {
@Language("RegExp") private const val WORD = """("\w+"|\w+)+"""
@Language("RegExp") private const val REFERENCE_ORDER = """([-<>])"""
private val COLUMN_REFERENCE_REGEX = Regex("""(([Rr]ef)\s*:\s*$REFERENCE_ORDER\s*$WORD\.$WORD)""")

fun parse(tableName: String, fromColumn: String, columnProperties: String): Reference? {
return if (COLUMN_REFERENCE_REGEX.containsMatchIn(columnProperties)) {
val referenceProperties = COLUMN_REFERENCE_REGEX.find(columnProperties)!!
Reference(
rawValue = referenceProperties.groups[0]!!.value,
fromTable = tableName.removeSurroundQuotes(),
fromColumn = fromColumn.removeSurroundQuotes(),
referenceOrder = ReferenceOrder.fromString(referenceProperties.groups[3]!!.value),
toTable = referenceProperties.groups[4]!!.value.removeSurroundQuotes(),
toColumn = referenceProperties.groups[5]!!.value.removeSurroundQuotes()
)
} else null
}

private fun String.removeSurroundQuotes(): String {
return this.removeSurrounding("\"")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.zynger.floorplan.lex

import com.zynger.floorplan.Reference
import com.zynger.floorplan.ReferenceOrder
import org.intellij.lang.annotations.Language

object LoneReferenceParser {
@Language("RegExp") private const val WORD = """("\w+"|\w+)+"""
@Language("RegExp") private const val REFERENCE_ORDER = """([-<>])"""
private val REFERENCE_REGEX = Regex("""[Rr]ef:\s*$WORD\.$WORD\s+$REFERENCE_ORDER\s+$WORD\.$WORD""")

fun parseReferences(dbmlInput: String): List<Reference> {
return REFERENCE_REGEX.findAll(dbmlInput).map {
Reference(
rawValue = it.groups[0]!!.value,
fromTable = it.groups[1]!!.value.removeSurroundQuotes(),
fromColumn = it.groups[2]!!.value.removeSurroundQuotes(),
referenceOrder = ReferenceOrder.fromString(it.groups[3]!!.value),
toTable = it.groups[4]!!.value.removeSurroundQuotes(),
toColumn = it.groups[5]!!.value.removeSurroundQuotes()
)
}.toList()
}

private fun String.removeSurroundQuotes(): String {
return this.removeSurrounding("\"")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.zynger.floorplan.lex

import com.zynger.floorplan.Table
import org.intellij.lang.annotations.Language

object TableParser {
@Language("RegExp") private const val TABLE_NAME = """(."\w+"|\w+.)"""
@Language("RegExp") private const val TABLE_ALIAS = """(\s*as\s+[\w]+|\s*as\s+"[\w]+"|)"""
@Language("RegExp") private const val TABLE_NOTES = """(\s\[.*]|)"""
@Language("RegExp") private const val TABLE_CONTENT = """\{(\s|\n|[^}]*)}"""
private val TABLE_REGEX = Regex("""[Tt]able\s+$TABLE_NAME\s*$TABLE_ALIAS$TABLE_NOTES\s*$TABLE_CONTENT""")

fun parseTables(dbmlInput: String): List<Table> {
// TODO: aliases and table notes also get parsed; should we update the modeling to include them?

return TABLE_REGEX.findAll(dbmlInput).map {
val tableName = it.groups[1]!!.value.trim()
val tableContent = it.groups[4]!!.value.run {
// TODO BUG, hack: the TABLE_REGEX doesn't take in account the Indexes block properly
val indexesMatchResult = Regex("""Indexes\s+\{""").find(this)
if (indexesMatchResult != null) {
this.substringBefore(indexesMatchResult.value)
} else {
this
}
}
Table(
rawValue = it.groups[0]!!.value,
name = tableName,
columns = ColumnParser.parseColumns(tableName, tableContent),
indexes = emptyList() // TODO include indexes parsing
)
}.toList()
}
}
Loading

0 comments on commit 550451b

Please sign in to comment.