Skip to content

Commit

Permalink
Add run configuration for sqlite (#2718)
Browse files Browse the repository at this point in the history
* Add run configuration for sqlite

* Add some tests

* Run spotless

* Fix up code so it works with master

* Remove need for an identifier

* Reuse ConnectionManager in compiler module

* Move the connection manager to the dialect API

* Remove tests and run spotless

* Simplify RunSqlAction

* Update sqldelight-idea-plugin/src/main/resources/META-INF/plugin.xml

* Address feedback

Co-authored-by: Alec Strong <astrong@squareup.com>
Co-authored-by: Alec Strong <AlecStrong@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 21, 2022
1 parent 5be1775 commit 80e2799
Show file tree
Hide file tree
Showing 22 changed files with 616 additions and 21 deletions.
1 change: 1 addition & 0 deletions dialects/sqlite-3-18/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ grammarKit {
dependencies {
compileOnly project(':sqldelight-compiler:dialect')
compileOnly deps.intellij.lang
compileOnly deps.intellij.core.ui

testImplementation deps.intellij.core
testImplementation deps.intellij.lang
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package app.cash.sqldelight.dialects.sqlite_3_18

import app.cash.sqldelight.dialect.api.ConnectionManager.ConnectionProperties
import com.intellij.openapi.fileChooser.FileTypeDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.RecentsManager
import com.intellij.ui.TextFieldWithHistoryWithBrowseButton
import com.intellij.ui.layout.ValidationInfoBuilder
import com.intellij.ui.layout.panel
import java.io.File
import javax.swing.JComponent
import javax.swing.JTextField

private const val RECENT_DB_PATH = "app.cash.sqldelight.recentPath"

internal class SelectConnectionTypeDialog(
project: Project,
) : DialogWrapper(project) {
private val recentsManager: RecentsManager = RecentsManager.getInstance(project)

private var connectionName: String = ""
private var filePath: String = ""

init {
title = "Select Connection Type"
init()
}

fun connectionProperties(): ConnectionProperties {
return ConnectionProperties(connectionName, filePath)
}

override fun createCenterPanel(): JComponent {
return panel {
row("Connection Name") {
textField(
getter = { connectionName },
setter = { connectionName = it }
).withValidationOnApply(validateKey())
.withValidationOnInput(validateKey())
}
row(label = "DB File Path") {
textFieldWithHistoryWithBrowseButton(
browseDialogTitle = "Choose File",
getter = { filePath },
setter = { filePath = it },
fileChooserDescriptor = FileTypeDescriptor("Choose File", "db"),
historyProvider = { recentsManager.getRecentEntries(RECENT_DB_PATH).orEmpty() },
fileChosen = { vFile ->
vFile.path.also { path ->
filePath = path
recentsManager.registerRecentEntry(RECENT_DB_PATH, path)
}
}
)
.withValidationOnInput(validateFilePath())
.withValidationOnApply(validateFilePath())
}
}.also {
validate()
}
}
}

private fun validateKey(): ValidationInfoBuilder.(JTextField) -> ValidationInfo? =
{
if (it.text.isNullOrEmpty()) {
error("You must supply a connection key.")
} else {
null
}
}

private fun validateFilePath(): ValidationInfoBuilder.(TextFieldWithHistoryWithBrowseButton) -> ValidationInfo? =
{
if (it.text.isEmpty()) {
error("The file path is empty.")
} else if (!File(it.text).exists()) {
error("This file does not exist.")
} else {
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package app.cash.sqldelight.dialects.sqlite_3_18

import app.cash.sqldelight.dialect.api.ConnectionManager
import app.cash.sqldelight.dialect.api.ConnectionManager.ConnectionProperties
import com.intellij.openapi.project.Project
import java.sql.Connection
import java.sql.DriverManager
import java.sql.SQLException

class SqliteConnectionManager : ConnectionManager {
override fun createNewConnectionProperties(project: Project): ConnectionProperties? {
val dialog = SelectConnectionTypeDialog(project)
if (!dialog.showAndGet()) return null
return dialog.connectionProperties()
}

override fun getConnection(connectionProperties: ConnectionProperties): Connection {
val path = connectionProperties.serializedProperties
val previousContextLoader = Thread.currentThread().contextClassLoader
return try {
// When it iterates the ServiceLoader we want to make sure its on the plugins classpath.
Thread.currentThread().contextClassLoader = this::class.java.classLoader
DriverManager.getConnection("jdbc:sqlite:$path")
} catch (e: SQLException) {
DriverManager.getConnection("jdbc:sqlite:$path")
} finally {
Thread.currentThread().contextClassLoader = previousContextLoader
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ open class SqliteDialect : SqlDelightDialect {
override val isSqlite = true
override val icon = AllIcons.Providers.Sqlite
override val migrationStrategy = SqliteMigrationStrategy()
override val connectionManager = SqliteConnectionManager()

override fun setup() {
Timber.i("Setting up SQLite Dialect")
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ jgrapht = { module = "org.jgrapht:jgrapht-core", version = "1.5.1" }
truth = { module = "com.google.truth:truth", version = "1.1.3" }
turbine = { module = "app.cash.turbine:turbine", version = "0.7.0" }
bugsnag = { module = "com.bugsnag:bugsnag", version = "3.6.3" }
picnic = { module = "com.jakewharton.picnic:picnic", version = "0.6.0" }

intellij-java = { module = "com.jetbrains.intellij.java:java-psi", version.ref = "idea" }
intellij-core = { module = "com.jetbrains.intellij.platform:core-impl", version.ref = "idea" }
intellij-core-ui = { module = "com.jetbrains.intellij.platform:core-ui", version.ref = "idea" }
intellij-lang = { module = "com.jetbrains.intellij.platform:lang-impl", version.ref = "idea" }
intellij-testFramework = { module = "com.jetbrains.intellij.platform:test-framework", version.ref = "idea" }
intellij-analysis = { module = "com.jetbrains.intellij.platform:analysis", version.ref = "idea" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.cash.sqldelight.dialect.api

import com.intellij.openapi.project.Project
import java.sql.Connection

interface ConnectionManager {
fun createNewConnectionProperties(project: Project): ConnectionProperties?

fun getConnection(connectionProperties: ConnectionProperties): Connection

data class ConnectionProperties(
val key: String,
val serializedProperties: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ interface SqlDelightDialect {

val migrationStrategy: SqlGeneratorStrategy get() = NoOp()

val connectionManager: ConnectionManager? get() = null

/**
* A type resolver specific to this dialect.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract class BindableQuery(
/**
* The collection of parameters exposed in the generated api for this query.
*/
internal val parameters: List<IntermediateType> by lazy {
val parameters: List<IntermediateType> by lazy {
if (statement is SqlInsertStmt && statement.acceptsTableInterface()) {
val table = statement.tableName.reference!!.resolve()!!
return@lazy listOf(
Expand All @@ -71,7 +71,7 @@ abstract class BindableQuery(
/**
* The collection of all bind expressions in this query.
*/
internal val arguments: List<Argument> by lazy {
val arguments: List<Argument> by lazy {
if (statement is SqlInsertStmt && statement.acceptsTableInterface()) {
return@lazy statement.columns.mapIndexed { index, column ->
Argument(
Expand Down Expand Up @@ -194,7 +194,7 @@ abstract class BindableQuery(
private val SqlBindParameter.identifier: SqlIdentifier?
get() = childOfType(SqlTypes.IDENTIFIER) as? SqlIdentifier

internal data class Argument(
data class Argument(
val index: Int,
val type: IntermediateType,
val bindArgs: MutableList<SqlBindExpr> = mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app.cash.sqldelight.core.lang

import app.cash.sqldelight.core.SqlDelightFileIndex
import app.cash.sqldelight.core.SqlDelightProjectService
import app.cash.sqldelight.dialect.api.ConnectionManager.ConnectionProperties
import com.intellij.lang.Language
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.vfs.VirtualFile
Expand All @@ -10,9 +11,6 @@ import com.intellij.psi.FileViewProviderFactory
import com.intellij.psi.PsiManager
import com.intellij.psi.SingleRootFileViewProvider
import com.intellij.testFramework.LightVirtualFile
import java.sql.Connection
import java.sql.DriverManager
import java.sql.SQLException

class DatabaseFileViewProviderFactory : FileViewProviderFactory {
override fun createFileViewProvider(
Expand Down Expand Up @@ -65,7 +63,10 @@ internal class DatabaseFileViewProvider(

val virtualFile = super.getVirtualFile()
try {
val statements = createConnection(virtualFile.path).use {
val connectionManager = SqlDelightProjectService.getInstance(manager.project)
.dialect.connectionManager ?: return null
val properties = ConnectionProperties("temp", virtualFile.path)
val statements = connectionManager.getConnection(properties).use {
it.prepareStatement("SELECT sql FROM sqlite_master WHERE sql IS NOT NULL;").use {
it.executeQuery().use {
mutableListOf<String>().apply {
Expand All @@ -85,19 +86,6 @@ internal class DatabaseFileViewProvider(
return null
}
}

private fun createConnection(path: String): Connection {
val previousContextLoader = Thread.currentThread().contextClassLoader
return try {
// When it iterates the ServiceLoader we want to make sure its on the plugins classpath.
Thread.currentThread().contextClassLoader = this::class.java.classLoader
DriverManager.getConnection("jdbc:sqlite:$path")
} catch (e: SQLException) {
DriverManager.getConnection("jdbc:sqlite:$path")
} finally {
Thread.currentThread().contextClassLoader = previousContextLoader
}
}
}

private val VirtualFile.version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ fun PsiElement.rawSqlText(
).second
}

internal val PsiElement.range: IntRange
val PsiElement.range: IntRange
get() = node.startOffset until (node.startOffset + node.textLength)

fun Collection<SqlDelightQueriesFile>.forInitializationStatements(
Expand Down
1 change: 1 addition & 0 deletions sqldelight-idea-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ dependencies {
implementation(deps.bugsnag) {
exclude group: "org.slf4j"
}
implementation deps.picnic

testImplementation deps.truth
testImplementation project(':dialects:sqlite-3-18')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import app.cash.sqldelight.core.lang.SqlDelightFileType
import app.cash.sqldelight.dialect.api.SqlDelightDialect
import app.cash.sqldelight.dialect.api.TypeResolver
import app.cash.sqldelight.intellij.gradle.FileIndexMap
import app.cash.sqldelight.intellij.run.window.SqlDelightToolWindowFactory
import app.cash.sqldelight.intellij.util.GeneratedVirtualFile
import com.alecstrong.sql.psi.core.SqlFileBase
import com.alecstrong.sql.psi.core.SqlParserUtil
Expand All @@ -43,6 +44,9 @@ import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
import com.intellij.openapi.wm.RegisterToolWindowTask
import com.intellij.openapi.wm.ToolWindowAnchor
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiManager
import com.intellij.psi.impl.PsiDocumentManagerImpl
Expand Down Expand Up @@ -131,6 +135,25 @@ class ProjectService(val project: Project) : SqlDelightProjectService, Disposabl
this.dialect = dialect
MigrationParserDefinition.stubVersion++
ApplicationManager.getApplication().runReadAction { invalidateAllFiles() }
ApplicationManager.getApplication().invokeLater {
ToolWindowManager.getInstance(project).getToolWindow("SqlDelight")?.remove()

val connectionManager = dialect.connectionManager
if (connectionManager != null) {
ToolWindowManager.getInstance(project).registerToolWindow(
RegisterToolWindowTask(
id = "SqlDelight",
anchor = ToolWindowAnchor.BOTTOM,
contentFactory = SqlDelightToolWindowFactory(connectionManager),
canCloseContent = true,
icon = dialect.icon,
)
).apply {
show()
hide()
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package app.cash.sqldelight.intellij.run

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.layout.panel
import javax.swing.JComponent

internal interface ArgumentsInputDialog {
val result: List<SqlParameter>

fun showAndGet(): Boolean

interface Factory {
fun create(project: Project, parameters: List<SqlParameter>): ArgumentsInputDialog
}
}

internal class ArgumentsInputDialogImpl(
project: Project,
private val parameters: List<SqlParameter>
) : DialogWrapper(project), ArgumentsInputDialog {

init {
init()
}

private val _result = mutableListOf<SqlParameter>()
override val result: List<SqlParameter> get() = _result

override fun createCenterPanel(): JComponent {
return panel {
parameters.forEach { parameter ->
row("${parameter.name}:") {
textField(parameter::value, {
_result.add(parameter.copy(value = it))
})
}
}
}
}
}

internal class ArgumentsInputDialogFactoryImpl : ArgumentsInputDialog.Factory {
override fun create(project: Project, parameters: List<SqlParameter>): ArgumentsInputDialog {
return ArgumentsInputDialogImpl(project, parameters)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package app.cash.sqldelight.intellij.run

import app.cash.sqldelight.dialect.api.ConnectionManager.ConnectionProperties
import com.intellij.ide.util.propComponentProperty
import com.intellij.openapi.project.Project
import com.squareup.moshi.Moshi

internal class ConnectionOptions(val project: Project) {
private var options: String by propComponentProperty(project, "connection_options", "")
var selectedOption: String by propComponentProperty(project, "selected_option", "")

fun addOption(properties: ConnectionProperties) {
val currentOptions = if (options.isEmpty()) StoredOptions() else adapter.fromJson(options)!!
currentOptions.map[properties.key] = properties.serializedProperties
options = adapter.toJson(currentOptions)
selectedOption = properties.key
}

fun unselectOption() {
selectedOption = ""
}

fun getKeys(): Collection<String> {
return adapter.fromJson(options.ifEmpty { return emptyList() })!!.map.keys
}

fun selectedProperties(): ConnectionProperties {
val currentOptions = adapter.fromJson(options)!!
return ConnectionProperties(selectedOption, currentOptions.map[selectedOption]!!)
}

companion object {
private val adapter = Moshi.Builder().build()
.adapter(StoredOptions::class.java)
}

private class StoredOptions(
val map: MutableMap<String, String> = linkedMapOf()
)
}
Loading

0 comments on commit 80e2799

Please sign in to comment.