Repository: julioz/FloorPlan
Pull request #31 (2020-06-04 13:02:57)

WIP: FloorPlan gradle plugin
Adds Gradle Plugin implementation, with initial integration tests, sample project for manual end to end test and updates documentation.

```
floorPlan {
    schemaLocation = "$projectDir/schemas-dir".toString()
    outputLocation = "$projectDir/output-dir".toString()
    outputFormat {
        svg {
            enabled = true
        }
    }
}
```

Nice to haves (follow-ups):
- Output files stored within directories and preserve structure
  - For example, `<input_dir>/com.mypackage.mydatabase/1.json` should end up as `<output_dir>/com.mypackage.mydatabase/1.dbml`
- Try out the `Provider` API
  - [Example](https://docs.gradle.org/current/userguide/lazy_configuration.html#connecting_properties_together)
  - instead of String for the directory we can declare the field as `DirectoryProperty` and then to pass it, use something like `project.layout.projectDirectory.dir("schemas")`, so that the actual file will be resolved when the task runs
- Allow for `closure.property = value` in consumer's build.gradle, instead of forcing a full block like `svg { enabled = true }`


Patch:
diff --git a/.gitignore b/.gitignore
index 2a36be0..1dff827 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ samples/**
 # Gradle
 .idea/**/gradle.xml
 .idea/**/libraries
+local.properties
 
 # Gradle and Maven with auto-import
 # When using Gradle or Maven with auto-import, you should exclude module files,
diff --git a/arguments.gradle b/arguments.gradle
new file mode 100644
index 0000000..482990f
--- /dev/null
+++ b/arguments.gradle
@@ -0,0 +1,3 @@
+ext {
+  includeSampleApp = (properties['floorplan.includeSampleApp'] ?: 'false').toBoolean()
+}
diff --git a/build.gradle b/build.gradle
index 271901c..1c5aee5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,28 @@
+buildscript {
+    apply from: "arguments.gradle"
+
+    repositories {
+        google()
+        jcenter()
+        if (includeSampleApp) {
+            mavenLocal()
+        }
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.6.3'
+        if (includeSampleApp) {
+            classpath "com.zynger.floorplan:floorplan-gradle-plugin:0.1-SNAPSHOT"
+        }
+    }
+}
 plugins {
     id 'org.jetbrains.kotlin.jvm' version '1.3.71' apply false
 }
 
-group 'zynger'
-version '0.1-SNAPSHOT'
+subprojects {
+    group 'com.zynger.floorplan'
+    version '0.1-SNAPSHOT'
+}
 
 repositories {
     mavenCentral()
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..d6caed4
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,46 @@
+# Contributing
+
+Thank you for your interest in contributing to FloorPlan!
+
+## Setup
+
+* Check out the [project](https://github.com/julioz/FloorPlan/)
+* Download [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) or [Android Studio](https://developer.android.com/studio) and import it.
+
+## Build
+
+* Build the CLI and its subprojects with `./gradlew :floorplan-cli:build`.
+* Build the Gradle Plugin:
+  * Publish a snapshot to your local maven repository: `./gradlew :floorplan-gradle-plugin:publishToMavenLocal`
+  * Enable the sample app building: `echo floorplan.includeSampleApp=true >> ~/.gradle/gradle.properties`
+  * (Optionally) Trigger the task on the sample app with `./gradlew :sample-android-project:generateFloorPlan`
+
+!!! note "Note: Gradle Plugin - workflow"
+    The gradle plugin development workflow requires republishing of the latest builds to `mavenLocal` after each code change. You will need to the deployment steps above if you want to manually test the changes in an end-to-end fashion with the help of the sample Android application.
+
+## Tests
+
+The project is unit tested and has integration tests for its Gradle plugin. These can be run via Gradle with:
+```
+gradlew test
+```
+
+You can also run only the Gradle Plugin tests by specifying that task explicitly:
+
+```
+gradlew :floorplan-gradle-plugin:test
+```
+
+## Deploying the docs locally
+
+Install `mkdocs` and the required extensions:
+
+```
+pip install -r requirements.txt
+```
+
+Deploy it locally:
+
+```
+mkdocs serve
+```
diff --git a/docs/gradle-plugin.md b/docs/gradle-plugin.md
new file mode 100644
index 0000000..a907eb9
--- /dev/null
+++ b/docs/gradle-plugin.md
@@ -0,0 +1,82 @@
+# FloorPlan as a Gradle Plugin
+
+### Apply the plugin
+
+Apply the gradle plugin in your root `build.gradle` file:
+
+```
+buildscript {
+  dependencies {
+    classpath "com.zynger.floorplan:floorplan-gradle-plugin:<version>"
+  }
+}
+```
+
+and in the modules you want `FloorPlan`'s Gradle task to exist.
+
+For example, in an Android application module:
+
+```
+plugins {
+    id 'com.android.application' // could be library or any other module
+    id 'com.zynger.floorplan'
+}
+```
+
+### Gradle extension
+
+Configure FloorPlan's Gradle extension by definining a `floorPlan` block:
+
+```
+floorPlan {
+    schemaLocation = "$projectDir/schemas".toString()
+    outputLocation = "$projectDir/floorplan-output".toString()
+    outputFormat {
+        svg {
+            enabled = true
+        }
+    }
+}
+```
+
+#### Output formats
+
+FloorPlan is able to output different file formats.
+
+Take a look at what a full configuration would look like, defining `DBML` as an output format (and providing [extra configurations for `DBML`](../run/#output-format)):
+
+```
+floorPlan {
+    schemaLocation = "$projectDir/schemas".toString()
+    outputLocation = "$projectDir/floorplan-output".toString()
+    outputFormat {
+        dbml {
+            enabled = true
+            creationSqlAsTableNote = false
+            renderNullableFields = false
+        }
+        svg {
+            enabled = false
+        }
+        png {
+            enabled = false
+        }
+        dot {
+            enabled = false
+        }
+    }
+}
+```
+
+!!! warning
+    So far, you can only specify one output format per run, by defining which one is `enabled`.
+
+### Generate Floor Plan
+
+Once everything is setup, you can finally run FloorPlan to translate database schemas into ER diagrams.
+
+For example, to generate the diagrams associated to the `sample-android-project` module schemas, we can run:
+
+```
+./gradlew :sample-android-project:generateFloorPlan
+```
\ No newline at end of file
diff --git a/docs/run.md b/docs/run.md
index b3e6ea9..43a4176 100644
--- a/docs/run.md
+++ b/docs/run.md
@@ -1,4 +1,4 @@
-# Run FloorPlan
+# Run FloorPlan from the command line
 
 After checking out this repository, [make sure you have Gradle installed](https://gradle.org/install/) and run:
 ```
diff --git a/floorplan-cli/build.gradle b/floorplan-cli/build.gradle
index 41e220c..b604788 100644
--- a/floorplan-cli/build.gradle
+++ b/floorplan-cli/build.gradle
@@ -4,9 +4,6 @@ plugins {
     id 'org.jetbrains.kotlin.jvm'
 }
 
-group 'zynger'
-version '0.1-SNAPSHOT'
-
 sourceCompatibility = 1.8
 
 repositories {
diff --git a/floorplan-gradle-plugin/build.gradle b/floorplan-gradle-plugin/build.gradle
new file mode 100644
index 0000000..30538ae
--- /dev/null
+++ b/floorplan-gradle-plugin/build.gradle
@@ -0,0 +1,58 @@
+plugins {
+    id 'java'
+    id 'java-gradle-plugin'
+    id 'maven-publish'
+    id 'org.jetbrains.kotlin.jvm'
+    id 'com.github.johnrengelman.shadow' version '5.2.0'
+}
+
+repositories {
+    mavenCentral()
+}
+
+publishing {
+    publications {
+        shadow(MavenPublication) { publication ->
+            project.shadow.component(publication)
+        }
+    }
+}
+
+shadowJar {
+    minimize {
+        /**
+         * graphviz-java bundles graphviz, a native dependency, accessed via reflection through System.load()
+         * we should not minimize it, otherwise the native code gets stripped.
+         */
+        exclude(dependency('guru.nidi:.*:.*'))
+    }
+    classifier = ''
+}
+
+gradlePlugin {
+    plugins {
+        simplePlugin {
+            id = 'com.zynger.floorplan'
+            implementationClass = 'com.zynger.floorplan.FloorPlanPlugin'
+        }
+    }
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+    compileOnly gradleApi()
+    implementation project(":dbml")
+    implementation project(":dbml2viz")
+    implementation project(":room-consumer")
+
+    testImplementation gradleTestKit()
+    testImplementation "junit:junit:4.12"
+    testImplementation "com.google.truth:truth:1.0.1"
+}
+
+compileKotlin {
+    kotlinOptions.jvmTarget = "1.8"
+}
+compileTestKotlin {
+    kotlinOptions.jvmTarget = "1.8"
+}
\ No newline at end of file
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/FloorPlanPlugin.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/FloorPlanPlugin.kt
new file mode 100644
index 0000000..4fc026a
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/FloorPlanPlugin.kt
@@ -0,0 +1,60 @@
+package com.zynger.floorplan
+
+import com.zynger.floorplan.gradle.model.DbmlConfiguration
+import com.zynger.floorplan.gradle.FloorPlanTask
+import com.zynger.floorplan.gradle.model.OutputFormat
+import com.zynger.floorplan.gradle.extension.*
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionAware
+import java.io.File
+
+class FloorPlanPlugin: Plugin<Project> {
+
+    override fun apply(project: Project) {
+        val floorPlanExtension = project.extensions.create("floorPlan", FloorPlanExtension::class.java)
+        floorPlanExtension as ExtensionAware
+        val outputFormatExtension = floorPlanExtension.extensions.create("outputFormat", OutputFormatExtension::class.java)
+
+        project.afterEvaluate {
+            project.tasks
+                .register("generateFloorPlan", FloorPlanTask::class.java)
+                .configure { task ->
+                    check(floorPlanExtension.schemaLocation.isPresent) { "Missing schemaLocation property." }
+                    check(floorPlanExtension.outputLocation.isPresent) { "Missing outputLocation property." }
+
+                    task.schemaLocation = File(floorPlanExtension.schemaLocation.get())
+                    task.outputLocation = File(floorPlanExtension.outputLocation.get())
+                    task.outputFormat = outputFormatExtension.getFormat()
+                }
+        }
+    }
+
+    private fun OutputFormatExtension.getFormat(): OutputFormat {
+        val outputFormats = getEnabledOutputFormats(this)
+        check(outputFormats.isNotEmpty()) { "There are no enabled output formats." }
+        check(outputFormats.size == 1) { "There can only be one enabled output format." }
+
+        return when (val output = outputFormats.first()) {
+            is DbmlConfigurationExtension -> OutputFormat.DBML(
+                DbmlConfiguration(
+                    output.creationSqlAsTableNote.get(),
+                    output.renderNullableFields.get()
+                )
+            )
+            is SvgConfigurationExtension -> OutputFormat.SVG
+            is PngConfigurationExtension -> OutputFormat.PNG
+            is DotConfigurationExtension -> OutputFormat.DOT
+        }
+    }
+
+    private fun getEnabledOutputFormats(outputFormatExtension: OutputFormatExtension): List<OutputFormatConfiguration> {
+        val outputFormats = listOf(
+            outputFormatExtension.dbmlConfiguration,
+            outputFormatExtension.svgConfiguration,
+            outputFormatExtension.pngConfiguration,
+            outputFormatExtension.dotConfiguration
+        )
+        return outputFormats.associateWith { it.enabled.get() }.filter { it.value }.map { it.key }
+    }
+}
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt
new file mode 100644
index 0000000..c69d97b
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/FloorPlanTask.kt
@@ -0,0 +1,59 @@
+package com.zynger.floorplan.gradle
+
+import com.zynger.floorplan.Destination
+import com.zynger.floorplan.FloorPlan
+import com.zynger.floorplan.Format
+import com.zynger.floorplan.Output
+import com.zynger.floorplan.dbml.Project
+import com.zynger.floorplan.DbmlConfiguration
+import com.zynger.floorplan.gradle.model.OutputFormat
+import com.zynger.floorplan.room.RoomConsumer
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.*
+import java.io.File
+
+open class FloorPlanTask : DefaultTask() {
+
+    @get:InputDirectory
+    lateinit var schemaLocation: File
+
+    @get:Nested
+    lateinit var outputFormat: OutputFormat
+
+    @OutputDirectory
+    lateinit var outputLocation: File
+
+    @TaskAction
+    fun generateFloorPlan() {
+        val schemas: List<File> = schemaLocation.findRoomSchemas()
+        schemas.forEach { schema ->
+            val project: Project = RoomConsumer.read(schema)
+            val outputFormat = outputFormat.mapOutputFormat()
+            val outputFileName = "${schema.nameWithoutExtension}.${outputFormat.extension}"
+            val outputFile = File(outputLocation, outputFileName)
+
+            FloorPlan.render(
+                project = project,
+                output = Output(outputFormat, Destination.Disk(outputFile))
+            )
+        }
+    }
+
+    private fun File.findRoomSchemas(): List<File> {
+        return walk().filter { it.extension == "json" }.toList()
+    }
+
+    private fun OutputFormat.mapOutputFormat(): Format {
+        return when (this) {
+            is OutputFormat.DBML -> Format.DBML(
+                DbmlConfiguration(
+                    this.config.creationSqlAsTableNote,
+                    this.config.renderNullableFields
+                )
+            )
+            OutputFormat.DOT -> Format.DOT
+            OutputFormat.SVG -> Format.SVG
+            OutputFormat.PNG -> Format.PNG
+        }
+    }
+}
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/extension/FloorPlanExtension.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/extension/FloorPlanExtension.kt
new file mode 100644
index 0000000..f9dd786
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/extension/FloorPlanExtension.kt
@@ -0,0 +1,14 @@
+package com.zynger.floorplan.gradle.extension
+
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import javax.inject.Inject
+
+open class FloorPlanExtension
+@Inject
+constructor(
+    objects: ObjectFactory
+) {
+    val schemaLocation: Property<String> = objects.property(String::class.java)
+    val outputLocation: Property<String> = objects.property(String::class.java)
+}
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/extension/OutputFormatExtension.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/extension/OutputFormatExtension.kt
new file mode 100644
index 0000000..ba2586d
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/extension/OutputFormatExtension.kt
@@ -0,0 +1,81 @@
+package com.zynger.floorplan.gradle.extension
+
+import org.gradle.api.Action
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import javax.inject.Inject
+
+open class OutputFormatExtension
+@Inject
+constructor(
+    objects: ObjectFactory
+) {
+
+    val dbmlConfiguration: DbmlConfigurationExtension = objects.newInstance(
+        DbmlConfigurationExtension::class.java)
+    val svgConfiguration: SvgConfigurationExtension = objects.newInstance(
+        SvgConfigurationExtension::class.java)
+    val pngConfiguration: PngConfigurationExtension = objects.newInstance(
+        PngConfigurationExtension::class.java)
+    val dotConfiguration: DotConfigurationExtension = objects.newInstance(
+        DotConfigurationExtension::class.java)
+
+    fun dbml(action: Action<DbmlConfigurationExtension>) {
+        action.execute(dbmlConfiguration)
+    }
+    fun svg(action: Action<OutputFormatConfiguration>) {
+        action.execute(svgConfiguration)
+    }
+    fun png(action: Action<OutputFormatConfiguration>) {
+        action.execute(pngConfiguration)
+    }
+    fun dot(action: Action<OutputFormatConfiguration>) {
+        action.execute(dotConfiguration)
+    }
+}
+
+sealed class OutputFormatConfiguration(objects: ObjectFactory) {
+    val enabled: Property<Boolean> = objects.property(Boolean::class.java).also { it.convention(false) }
+
+    fun enabled(value: Boolean) {
+        enabled.set(value)
+        enabled.disallowChanges()
+    }
+}
+
+open class DbmlConfigurationExtension
+@Inject
+constructor(
+    objects: ObjectFactory
+): OutputFormatConfiguration(objects) {
+    val creationSqlAsTableNote: Property<Boolean> = objects.property(Boolean::class.java).also { it.convention(false) }
+    val renderNullableFields: Property<Boolean> = objects.property(Boolean::class.java).also { it.convention(false) }
+
+    fun creationSqlAsTableNote(enabled: Boolean) {
+        creationSqlAsTableNote.set(enabled)
+        creationSqlAsTableNote.disallowChanges()
+    }
+
+    fun renderNullableFields(enabled: Boolean) {
+        renderNullableFields.set(enabled)
+        renderNullableFields.disallowChanges()
+    }
+}
+
+open class SvgConfigurationExtension
+@Inject
+constructor(
+    objects: ObjectFactory
+):  OutputFormatConfiguration(objects)
+
+open class PngConfigurationExtension
+@Inject
+constructor(
+    objects: ObjectFactory
+):  OutputFormatConfiguration(objects)
+
+open class DotConfigurationExtension
+@Inject
+constructor(
+    objects: ObjectFactory
+):  OutputFormatConfiguration(objects)
diff --git a/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/model/OutputFormat.kt b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/model/OutputFormat.kt
new file mode 100644
index 0000000..804bec0
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/kotlin/com/zynger/floorplan/gradle/model/OutputFormat.kt
@@ -0,0 +1,13 @@
+package com.zynger.floorplan.gradle.model
+
+sealed class OutputFormat {
+    data class DBML(val config: DbmlConfiguration): OutputFormat()
+    object DOT: OutputFormat()
+    object SVG: OutputFormat()
+    object PNG: OutputFormat()
+}
+
+data class DbmlConfiguration(
+    val creationSqlAsTableNote: Boolean = false,
+    val renderNullableFields: Boolean = false
+)
diff --git a/floorplan-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.zynger.floorplan.properties b/floorplan-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.zynger.floorplan.properties
new file mode 100644
index 0000000..c065799
--- /dev/null
+++ b/floorplan-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.zynger.floorplan.properties
@@ -0,0 +1,1 @@
+implementation-class=com.zynger.floorplan.FloorPlanPlugin
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 29e08e8..07673db 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,1 +1,2 @@
-kotlin.code.style=official
\ No newline at end of file
+kotlin.code.style=official
+android.useAndroidX=true
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index 9a8fc11..1443483 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -12,13 +12,15 @@ markdown_extensions:
   - codehilite:
       guess_lang: false
   - pymdownx.superfences
+  - mdx_truly_sane_lists
 
 theme:
   name: 'material'
 
 nav:
   - 'Overview': 'index.md'
-  - 'Run':
-      - 'Introduction': 'run.md'
-      - 'Testing': 'tests.md'
+  - 'Usage':
+      - 'Command-line': 'run.md'
+      - 'Gradle Plugin': 'gradle-plugin.md'
+  - 'Contributing': 'contributing.md'
   - 'Architecture': 'architecture.md'
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..9bdaead
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+mkdocs
+mkdocs-material>=5.1.0
+mkdocs-markdownextradata-plugin
+pymdown-extensions>=7.0
+mdx_truly_sane_lists
diff --git a/sample-android-project/build.gradle b/sample-android-project/build.gradle
new file mode 100644
index 0000000..cda15f7
--- /dev/null
+++ b/sample-android-project/build.gradle
@@ -0,0 +1,39 @@
+plugins {
+    id 'com.android.application'
+    id 'com.zynger.floorplan'
+}
+
+repositories {
+    mavenCentral()
+    google()
+}
+
+android {
+    compileSdkVersion 28
+    defaultConfig {
+        applicationId "com.zynger.floorplan.gradle.sample"
+        minSdkVersion 23
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+}
+
+floorPlan {
+    schemaLocation = "$projectDir/schemas".toString()
+    outputLocation = "$projectDir/schemas".toString()
+    outputFormat {
+        dbml {
+            enabled = true
+            creationSqlAsTableNote = true
+            renderNullableFields = true
+        }
+    }
+}
+
+dependencies {
+    implementation "androidx.appcompat:appcompat:1.1.0"
+
+    testImplementation "junit:junit:4.13"
+}
diff --git a/sample-android-project/schemas/1.json b/sample-android-project/schemas/1.json
new file mode 100644
index 0000000..bff3212
--- /dev/null
+++ b/sample-android-project/schemas/1.json
@@ -0,0 +1,835 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "352dce1a8d7242ae0bd33ce1985bef31",
+    "entities": [
+      {
+        "tableName": "shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT, `original_title` TEXT, `trakt_id` INTEGER, `tmdb_id` INTEGER, `tmdb_poster_path` TEXT, `tmdb_backdrop_path` TEXT, `trakt_updated` TEXT, `tmdb_updated` TEXT, `overview` TEXT, `homepage` TEXT, `rating` REAL, `certification` TEXT, `country` TEXT, `network` TEXT, `runtime` INTEGER, `genres` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "originalTitle",
+            "columnName": "original_title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbId",
+            "columnName": "tmdb_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbPosterPath",
+            "columnName": "tmdb_poster_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbBackdropPath",
+            "columnName": "tmdb_backdrop_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTraktUpdate",
+            "columnName": "trakt_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTmdbUpdate",
+            "columnName": "tmdb_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "overview",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "homepage",
+            "columnName": "homepage",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rating",
+            "columnName": "rating",
+            "affinity": "REAL",
+            "notNull": false
+          },
+          {
+            "fieldPath": "certification",
+            "columnName": "certification",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "country",
+            "columnName": "country",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "network",
+            "columnName": "network",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "runtime",
+            "columnName": "runtime",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "_genres",
+            "columnName": "genres",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_shows_trakt_id",
+            "unique": true,
+            "columnNames": [
+              "trakt_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_shows_trakt_id` ON `${TABLE_NAME}` (`trakt_id`)"
+          },
+          {
+            "name": "index_shows_tmdb_id",
+            "unique": true,
+            "columnNames": [
+              "tmdb_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_shows_tmdb_id` ON `${TABLE_NAME}` (`tmdb_id`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "trending_shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `watchers` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "page",
+            "columnName": "page",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "watchers",
+            "columnName": "watchers",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_trending_shows_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_trending_shows_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "popular_shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `page_order` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "page",
+            "columnName": "page",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pageOrder",
+            "columnName": "page_order",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_popular_shows_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_popular_shows_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "users",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `username` TEXT NOT NULL, `name` TEXT NOT NULL, `joined_date` TEXT, `location` TEXT, `about` TEXT, `avatar_url` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "username",
+            "columnName": "username",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "joined",
+            "columnName": "joined_date",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "location",
+            "columnName": "location",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "about",
+            "columnName": "about",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "avatarUrl",
+            "columnName": "avatar_url",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "watched_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `last_watched` TEXT NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastWatched",
+            "columnName": "last_watched",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_watched_entries_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_watched_entries_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "myshows_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_myshows_entries_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_myshows_entries_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "seasons",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER, `trakt_id` INTEGER, `tmdb_id` INTEGER, `title` TEXT, `overview` TEXT, `number` INTEGER, `ep_count` INTEGER, `ep_aired` INTEGER, `rating` REAL, `network` TEXT, `votes` INTEGER, `tmdb_poster_path` TEXT, `tmdb_backdrop_path` TEXT, `trakt_updated` TEXT, `tmdb_updated` TEXT, `episodes_updated` TEXT, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbId",
+            "columnName": "tmdb_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "overview",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "number",
+            "columnName": "number",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "episodeCount",
+            "columnName": "ep_count",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "airedEpisodes",
+            "columnName": "ep_aired",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rating",
+            "columnName": "rating",
+            "affinity": "REAL",
+            "notNull": false
+          },
+          {
+            "fieldPath": "network",
+            "columnName": "network",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "votes",
+            "columnName": "votes",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbPosterPath",
+            "columnName": "tmdb_poster_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbBackdropPath",
+            "columnName": "tmdb_backdrop_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTraktUpdate",
+            "columnName": "trakt_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTmdbUpdate",
+            "columnName": "tmdb_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastEpisodeUpdate",
+            "columnName": "episodes_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_seasons_show_id_number",
+            "unique": true,
+            "columnNames": [
+              "show_id",
+              "number"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_seasons_show_id_number` ON `${TABLE_NAME}` (`show_id`, `number`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "episodes",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `season_id` INTEGER, `trakt_id` INTEGER, `tmdb_id` INTEGER, `title` TEXT, `overview` TEXT, `number` INTEGER, `first_aired` TEXT, `rating` REAL, `votes` INTEGER, `tmdb_poster_path` TEXT, `tmdb_backdrop_path` TEXT, `trakt_updated` TEXT, `tmdb_updated` TEXT, `last_watched_at` TEXT, FOREIGN KEY(`season_id`) REFERENCES `seasons`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "seasonId",
+            "columnName": "season_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbId",
+            "columnName": "tmdb_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "overview",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "number",
+            "columnName": "number",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "firstAired",
+            "columnName": "first_aired",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rating",
+            "columnName": "rating",
+            "affinity": "REAL",
+            "notNull": false
+          },
+          {
+            "fieldPath": "votes",
+            "columnName": "votes",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbPosterPath",
+            "columnName": "tmdb_poster_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbBackdropPath",
+            "columnName": "tmdb_backdrop_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTraktUpdate",
+            "columnName": "trakt_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTmdbUpdate",
+            "columnName": "tmdb_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastWatched",
+            "columnName": "last_watched_at",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_episodes_season_id_number",
+            "unique": true,
+            "columnNames": [
+              "season_id",
+              "number"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_episodes_season_id_number` ON `${TABLE_NAME}` (`season_id`, `number`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "seasons",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "season_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "related_shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `other_show_id` INTEGER NOT NULL, `order_index` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`other_show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "otherShowId",
+            "columnName": "other_show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "orderIndex",
+            "columnName": "order_index",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_related_shows_show_id",
+            "unique": false,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE  INDEX `index_related_shows_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          },
+          {
+            "name": "index_related_shows_other_show_id",
+            "unique": false,
+            "columnNames": [
+              "other_show_id"
+            ],
+            "createSql": "CREATE  INDEX `index_related_shows_other_show_id` ON `${TABLE_NAME}` (`other_show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          },
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "other_show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "episode_watch_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `episode_id` INTEGER NOT NULL, `trakt_id` INTEGER NOT NULL, `watched_at` TEXT NOT NULL, `source` INTEGER NOT NULL, FOREIGN KEY(`episode_id`) REFERENCES `episodes`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "episodeId",
+            "columnName": "episode_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "watchedAt",
+            "columnName": "watched_at",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_episode_watch_entries_episode_id",
+            "unique": false,
+            "columnNames": [
+              "episode_id"
+            ],
+            "createSql": "CREATE  INDEX `index_episode_watch_entries_episode_id` ON `${TABLE_NAME}` (`episode_id`)"
+          },
+          {
+            "name": "index_episode_watch_entries_trakt_id",
+            "unique": true,
+            "columnNames": [
+              "trakt_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_episode_watch_entries_trakt_id` ON `${TABLE_NAME}` (`trakt_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "episodes",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "episode_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"352dce1a8d7242ae0bd33ce1985bef31\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/sample-android-project/schemas/2.json b/sample-android-project/schemas/2.json
new file mode 100644
index 0000000..2814dc5
--- /dev/null
+++ b/sample-android-project/schemas/2.json
@@ -0,0 +1,835 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "88c53ad5131ac2ceb969883bc9e337ce",
+    "entities": [
+      {
+        "tableName": "shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT, `original_title` TEXT, `trakt_id` INTEGER, `tmdb_id` INTEGER, `tmdb_poster_path` TEXT, `tmdb_backdrop_path` TEXT, `trakt_updated` TEXT, `tmdb_updated` TEXT, `overview` TEXT, `homepage` TEXT, `rating` REAL, `certification` TEXT, `country` TEXT, `network` TEXT, `runtime` INTEGER, `genres` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "originalTitle",
+            "columnName": "original_title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbId",
+            "columnName": "tmdb_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbPosterPath",
+            "columnName": "tmdb_poster_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbBackdropPath",
+            "columnName": "tmdb_backdrop_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTraktUpdate",
+            "columnName": "trakt_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTmdbUpdate",
+            "columnName": "tmdb_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "overview",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "homepage",
+            "columnName": "homepage",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rating",
+            "columnName": "rating",
+            "affinity": "REAL",
+            "notNull": false
+          },
+          {
+            "fieldPath": "certification",
+            "columnName": "certification",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "country",
+            "columnName": "country",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "network",
+            "columnName": "network",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "runtime",
+            "columnName": "runtime",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "_genres",
+            "columnName": "genres",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_shows_trakt_id",
+            "unique": true,
+            "columnNames": [
+              "trakt_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_shows_trakt_id` ON `${TABLE_NAME}` (`trakt_id`)"
+          },
+          {
+            "name": "index_shows_tmdb_id",
+            "unique": true,
+            "columnNames": [
+              "tmdb_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_shows_tmdb_id` ON `${TABLE_NAME}` (`tmdb_id`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "trending_shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `watchers` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "page",
+            "columnName": "page",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "watchers",
+            "columnName": "watchers",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_trending_shows_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_trending_shows_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "popular_shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `page_order` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "page",
+            "columnName": "page",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pageOrder",
+            "columnName": "page_order",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_popular_shows_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_popular_shows_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "users",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `username` TEXT NOT NULL, `name` TEXT NOT NULL, `joined_date` TEXT, `location` TEXT, `about` TEXT, `avatar_url` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "username",
+            "columnName": "username",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "joined",
+            "columnName": "joined_date",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "location",
+            "columnName": "location",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "about",
+            "columnName": "about",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "avatarUrl",
+            "columnName": "avatar_url",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "watched_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `last_watched` TEXT NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastWatched",
+            "columnName": "last_watched",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_watched_entries_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_watched_entries_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "myshows_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_myshows_entries_show_id",
+            "unique": true,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_myshows_entries_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "seasons",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER, `trakt_id` INTEGER, `tmdb_id` INTEGER, `title` TEXT, `overview` TEXT, `number` INTEGER, `ep_count` INTEGER, `ep_aired` INTEGER, `rating` REAL, `network` TEXT, `votes` INTEGER, `tmdb_poster_path` TEXT, `tmdb_backdrop_path` TEXT, `trakt_updated` TEXT, `tmdb_updated` TEXT, `episodes_updated` TEXT, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbId",
+            "columnName": "tmdb_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "overview",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "number",
+            "columnName": "number",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "episodeCount",
+            "columnName": "ep_count",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "airedEpisodes",
+            "columnName": "ep_aired",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rating",
+            "columnName": "rating",
+            "affinity": "REAL",
+            "notNull": false
+          },
+          {
+            "fieldPath": "network",
+            "columnName": "network",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "votes",
+            "columnName": "votes",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbPosterPath",
+            "columnName": "tmdb_poster_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbBackdropPath",
+            "columnName": "tmdb_backdrop_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTraktUpdate",
+            "columnName": "trakt_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTmdbUpdate",
+            "columnName": "tmdb_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastEpisodeUpdate",
+            "columnName": "episodes_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_seasons_show_id_number",
+            "unique": true,
+            "columnNames": [
+              "show_id",
+              "number"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_seasons_show_id_number` ON `${TABLE_NAME}` (`show_id`, `number`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "episodes",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `season_id` INTEGER, `trakt_id` INTEGER, `tmdb_id` INTEGER, `title` TEXT, `overview` TEXT, `number` INTEGER, `first_aired` TEXT, `rating` REAL, `votes` INTEGER, `tmdb_poster_path` TEXT, `tmdb_backdrop_path` TEXT, `trakt_updated` TEXT, `tmdb_updated` TEXT, `last_watched_at` TEXT, FOREIGN KEY(`season_id`) REFERENCES `seasons`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "seasonId",
+            "columnName": "season_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbId",
+            "columnName": "tmdb_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "overview",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "number",
+            "columnName": "number",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "firstAired",
+            "columnName": "first_aired",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rating",
+            "columnName": "rating",
+            "affinity": "REAL",
+            "notNull": false
+          },
+          {
+            "fieldPath": "votes",
+            "columnName": "votes",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbPosterPath",
+            "columnName": "tmdb_poster_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tmdbBackdropPath",
+            "columnName": "tmdb_backdrop_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTraktUpdate",
+            "columnName": "trakt_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastTmdbUpdate",
+            "columnName": "tmdb_updated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastWatched",
+            "columnName": "last_watched_at",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_episodes_season_id_number",
+            "unique": true,
+            "columnNames": [
+              "season_id",
+              "number"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_episodes_season_id_number` ON `${TABLE_NAME}` (`season_id`, `number`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "seasons",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "season_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "related_shows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `show_id` INTEGER NOT NULL, `other_show_id` INTEGER NOT NULL, `order_index` INTEGER NOT NULL, FOREIGN KEY(`show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`other_show_id`) REFERENCES `shows`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "showId",
+            "columnName": "show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "otherShowId",
+            "columnName": "other_show_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "orderIndex",
+            "columnName": "order_index",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_related_shows_show_id",
+            "unique": false,
+            "columnNames": [
+              "show_id"
+            ],
+            "createSql": "CREATE  INDEX `index_related_shows_show_id` ON `${TABLE_NAME}` (`show_id`)"
+          },
+          {
+            "name": "index_related_shows_other_show_id",
+            "unique": false,
+            "columnNames": [
+              "other_show_id"
+            ],
+            "createSql": "CREATE  INDEX `index_related_shows_other_show_id` ON `${TABLE_NAME}` (`other_show_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          },
+          {
+            "table": "shows",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "other_show_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "episode_watch_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `episode_id` INTEGER NOT NULL, `trakt_id` INTEGER NOT NULL, `watched_at` TEXT NOT NULL, `pending_action` INTEGER NOT NULL, FOREIGN KEY(`episode_id`) REFERENCES `episodes`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "episodeId",
+            "columnName": "episode_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "traktId",
+            "columnName": "trakt_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "watchedAt",
+            "columnName": "watched_at",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pendingAction",
+            "columnName": "pending_action",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_episode_watch_entries_episode_id",
+            "unique": false,
+            "columnNames": [
+              "episode_id"
+            ],
+            "createSql": "CREATE  INDEX `index_episode_watch_entries_episode_id` ON `${TABLE_NAME}` (`episode_id`)"
+          },
+          {
+            "name": "index_episode_watch_entries_trakt_id",
+            "unique": true,
+            "columnNames": [
+              "trakt_id"
+            ],
+            "createSql": "CREATE UNIQUE INDEX `index_episode_watch_entries_trakt_id` ON `${TABLE_NAME}` (`trakt_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "episodes",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "episode_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"88c53ad5131ac2ceb969883bc9e337ce\")"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/sample-android-project/src/main/AndroidManifest.xml b/sample-android-project/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b98859c
--- /dev/null
+++ b/sample-android-project/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.zynger.floorplan.gradle.sample"/>
diff --git a/settings.gradle b/settings.gradle
index 6027773..94bd9d6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,5 @@
+apply from: "arguments.gradle"
+
 rootProject.name = 'floorplan'
 include 'dbml-render'
 include 'dbml'
@@ -5,3 +7,8 @@ include 'dbml-parser'
 include 'dbml2viz'
 include 'floorplan-cli'
 include 'room-consumer'
+include 'floorplan-gradle-plugin'
+
+if (includeSampleApp) {
+    include 'sample-android-project'
+}


Test patch:
diff --git a/docs/tests.md b/docs/tests.md
deleted file mode 100644
index 92ddb2d..0000000
--- a/docs/tests.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Tests
-
-The project is unit tested. These can be run via Gradle with:
-```
-gradlew test
-```
-
diff --git a/floorplan-gradle-plugin/src/test/kotlin/com/zynger/floorplan/FloorPlanGradlePluginIntegrationTest.kt b/floorplan-gradle-plugin/src/test/kotlin/com/zynger/floorplan/FloorPlanGradlePluginIntegrationTest.kt
new file mode 100644
index 0000000..a71c226
--- /dev/null
+++ b/floorplan-gradle-plugin/src/test/kotlin/com/zynger/floorplan/FloorPlanGradlePluginIntegrationTest.kt
@@ -0,0 +1,260 @@
+package com.zynger.floorplan
+
+import com.google.common.truth.Truth.assertThat
+import org.gradle.testkit.runner.BuildResult
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class FloorPlanGradlePluginIntegrationTest {
+
+    @get:Rule
+    var testProjectRoot = TemporaryFolder()
+
+    private fun writeBuildGradle(build: String) {
+        testProjectRoot.newFile("build.gradle").run { writeText(build) }
+    }
+    private fun createSchemasDirectory() {
+        testProjectRoot.newFolder("schemas")
+    }
+
+    @Test
+    fun applyingBarePluginSucceeds() {
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}""".trimMargin()
+        )
+        GradleRunner.create()
+            .withProjectDir(testProjectRoot.root)
+            .withPluginClasspath()
+            .build()
+    }
+
+    @Test
+    fun testMissingSchemaLocationFailsBuild() {
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |    svg {
+             |      enabled = true
+             |    }
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .buildAndFail()
+            .withFailureMessage("Missing schemaLocation property.")
+    }
+
+    @Test
+    fun testMissingOutputLocationFailsBuild() {
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputFormat {
+             |    svg {
+             |      enabled = true
+             |    }
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .buildAndFail()
+            .withFailureMessage("Missing outputLocation property.")
+    }
+
+    @Test
+    fun testMissingOutputFormatFailsBuild() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .buildAndFail()
+            .withFailureMessage("There are no enabled output formats.")
+    }
+
+    @Test
+    fun testEmptyOutputFormatFailsBuild() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .buildAndFail()
+            .withFailureMessage("There are no enabled output formats.")
+    }
+
+    @Test
+    fun testAllDisabledOutputFormatFailsBuild() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |    dbml {
+             |      enabled = false
+             |    }
+             |    svg {
+             |      enabled = false
+             |    }
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .buildAndFail()
+            .withFailureMessage("There are no enabled output formats.")
+    }
+
+    @Test
+    fun testMultipleEnabledOutputFormatFailsBuild() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |    dbml {
+             |      enabled = true
+             |    }
+             |    svg {
+             |      enabled = true
+             |    }
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .buildAndFail()
+            .withFailureMessage("There can only be one enabled output format.")
+    }
+
+    @Test
+    fun testSingleOutputFormatEnabled() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |    svg {
+             |      enabled = true
+             |    }
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .build()
+            .withSuccessfulMessage()
+    }
+
+    @Test
+    fun testMultipleOutputFormatButSingleEnabled() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |    dbml {
+             |      enabled = false
+             |    }
+             |    svg {
+             |      enabled = true
+             |    }
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .build()
+            .withSuccessfulMessage()
+    }
+
+    @Ignore("Unsupported feature. To be worked on.")
+    @Test
+    fun testSingleOutputFormatEnabledWithCollapsibleStatement() {
+        createSchemasDirectory()
+        writeBuildGradle(
+            """plugins {
+             |  id "com.zynger.floorplan"
+             |}
+             |
+             |floorPlan {
+             |  schemaLocation = "schemas"
+             |  outputLocation = "schemas"
+             |  outputFormat {
+             |    svg.enabled = true
+             |  }
+             |}""".trimMargin()
+        )
+        floorPlanRunner()
+            .build()
+            .withSuccessfulMessage()
+    }
+
+    private fun floorPlanRunner(): GradleRunner {
+        return GradleRunner.create()
+            .withProjectDir(testProjectRoot.root)
+            .withPluginClasspath()
+            .withArguments(GRADLE_TASK_NAME)
+    }
+
+    private fun BuildResult.withSuccessfulMessage(): BuildResult {
+        assertThat(output).contains(BUILD_SUCCESSFUL)
+        return this
+    }
+
+    private fun BuildResult.withFailureMessage(message: String): BuildResult {
+        assertThat(output).contains(message)
+        return this
+    }
+
+    private companion object {
+        private const val GRADLE_TASK_NAME = "generateFloorPlan"
+        private const val BUILD_SUCCESSFUL = "BUILD SUCCESSFUL"
+    }
+}