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

Add dbml parser module #21

Merged
merged 29 commits into from
May 19, 2020
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
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