Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/jetbrains-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ jobs:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v6
with:
# One cache writer: two filled the repo quota and evicted the pnpm caches.
cache-read-only: true
- run: ./gradlew test -PintegrationTests=true --stacktrace
env:
# Rootful Docker on the runner; skip the rootless strategy, whose JNA collides with the IntelliJ Platform's bundled jnidispatch.
Expand Down
37 changes: 37 additions & 0 deletions packages/jetbrains/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.5.0] - 2026-08-09

### Fixed
- A two-line answer is no longer clipped to one, with the note below it painting over the rest. The
text pane reported its height measured at its unwrapped width rather than the width it was given.
- The **Explain** button comes back after a failure. It disabled itself on click and, if the model
call failed, stayed disabled for the rest of that turn.
- A MongoDB turn no longer says "Writing SQL" or "Finding relevant tables" while it builds a pipeline.
- Content appended to a turn already on screen is scrolled into view, so an approval bar below the
fold is reachable. Scrolling only follows when you are already at the bottom, so it will not pull
you away from an earlier turn you are reading.
- A backticked placeholder such as `?`, a date, or `:param` is no longer reported as a name missing
from your schema.
- A proposed write showed three separate lines saying nothing had run. It shows one.
- The copy control reports failure instead of showing a tick when the clipboard write is refused.

### Added
- **A copy control on every answer**, not only on result grids: explanations, schema answers, and
errors each copy the model's own text rather than the rendered markup.
- **A query inside a prose answer renders as a real code block** with syntax highlighting and its own
Copy, instead of running together with the sentences around it.
- **The transcript reads as a conversation.** Your question sits on the right in a tinted bubble,
AskSQL answers on the left, and a rule separates one turn from the next.
- Progress is shown for the states that used to sit blank: preparing a turn, answering from the
schema, and correcting a query the database rejected.
- A MongoDB pipeline's collection name is selectable and travels with Copy, as a
`db.getCollection(...).aggregate(...)` call that pastes straight into mongosh.
- Progress is announced to screen readers where the IDE runtime supports it.

### Changed
- Connections are released when the project's services are disposed rather than through an
experimental platform listener, so cleanup also covers disabling or unloading the plugin. The
listener also woke both connection registries on every project close, including in projects where
AskSQL was never opened.
- The guard detects `LIMIT ALL` through JSqlParser's supported API. The two methods it used before
are deprecated, and the guard failing to compile on a future parser release is not a small problem.

## [0.4.2] - 2026-08-09

### Fixed
Expand Down
31 changes: 23 additions & 8 deletions packages/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ plain language, review the generated SQL, approve it, and get results.

![Schema tree browsing a connected MySQL database above the chat panel, with sample questions to get started](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/schema-and-chat.png)

![AskSQL settings configured against a local Ollama model, no API key needed](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/settings-ollama.png)
![AskSQL settings configured against a local Ollama model, no API key needed](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/settings.png)

![The AI provider dropdown in settings, listing OpenAI, Anthropic, Gemini, Groq, Ollama and the rest](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/settings-providers.png)
![The AI provider dropdown in settings, listing OpenAI, Anthropic, Google, Groq, Ollama, an OpenAI-compatible endpoint, LM Studio and NVIDIA](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/settings-ai-providers.png)

![A query result drawn as a bar chart, with a Table toggle beside Export CSV, Copy, Open in Editor and Explain](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/show-chart.png)

![Adding a database connection from the AskSQL settings page](https://github.com/rahulmahadik/AskSQL/raw/HEAD/packages/jetbrains/images/settings-add-db-connection.png)

## Getting started (using the plugin)

Expand All @@ -28,7 +32,7 @@ plain language, review the generated SQL, approve it, and get results.
`build/distributions/asksql-jetbrains-<version>.zip`, which installs via
**Plugins → ⚙ → Install Plugin from Disk...**.)
2. Restart the IDE when prompted.
3. Open the **AskSQL** tool window (usually a tab on the right/bottom edge).
3. Open the **AskSQL** tool window (a tab on the right edge).
4. **Add a connection**: on the empty-state screen, click **Add Connection** (Postgres,
MySQL, SQLite, DuckDB, Oracle, or MongoDB), or click **Try sample data** for a
ready-made SQLite database with no setup, good for a first look.
Expand Down Expand Up @@ -131,8 +135,11 @@ These hold on every engine (the plan doc carries the full list):
unless **Send sample column values to the model** is turned on in Settings. It defaults to off,
so out of the box the model sees field names, types and presence percentages only. Full row
data (arbitrary query results) is never sent on any engine.
- Chat history and query results are **in-memory only**; nothing is written to disk
except settings, and secrets live only in the OS keychain via PasswordSafe.
- Chat history and query results are **in-memory only**, and secrets live only in the OS
keychain via PasswordSafe. Apart from settings, the only things the plugin writes to disk
are under the IDE's system directory and are ones you asked for: the JDBC driver jars it
downloads on demand (`asksql/drivers`), the DuckDB database built when you load data files
(`asksql/uploads`), the sample SQLite database (`asksql/sample`), and a CSV you export.
- Zero telemetry.

## Requirements
Expand All @@ -142,7 +149,8 @@ These hold on every engine (the plan doc carries the full list):
- Docker, only if you want to run the Testcontainers-backed integration tests
(`./gradlew test -PintegrationTests=true`). Everything else (build, unit tests,
`runIde`) needs no Docker.
- Node.js 18+, only for `./gradlew parityVectors` (see below). Never required to build
- Node.js 20+ (the monorepo's `engines` floor; CI uses 22), only for
`./gradlew parityVectors` (see below), which builds `@asksql/core` from this repo. Never required to build
or run the plugin itself.

## Compatibility
Expand All @@ -157,10 +165,17 @@ Every release is run through the JetBrains **Plugin Verifier**: 15 verifications

| IDE | Versions verified |
| --- | --- |
| IntelliJ IDEA Community | 2024.2 (floor), 2024.3, 2025.1, 2025.2, 2025.3 |
| IntelliJ IDEA Ultimate, PyCharm Professional, WebStorm, PhpStorm, GoLand, Rider, CLion, RubyMine, RustRover | 2026.1 (latest stable) |
| IntelliJ IDEA Community | 2024.2 (floor), 2024.3, 2025.1 (251.29188.72), 2025.2.6.2 (252.28539.54), 2025.3 (253.28294.334) |
| IntelliJ IDEA Ultimate, PyCharm Professional, WebStorm, PhpStorm, GoLand, Rider, CLion, RubyMine, RustRover | 2026.1.4 (latest stable) |
| Android Studio | whichever build is installed locally |

The IC entries from 2025.1 on are pinned by build number because ideaIC publishes both build-number
and marketing-version artifacts; the other IDEs publish marketing versions only.

`untilBuild` is deliberately open-ended (`null`), so the plugin stays installable on IntelliJ
majors that did not exist when it was published. The Plugin Verifier is the safety net: a
verification failure blocks the release.

2024.1 is out of reach: `com.intellij.util.net.JdkProxyProvider`, which routes model calls through
the IDE's proxy settings, arrives in 242.

Expand Down
5 changes: 5 additions & 0 deletions packages/jetbrains/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ tasks {
}
}
systemProperty("idea.force.use.core.classloader", "true")
// Painting Swing to a PNG needs real font metrics and a window peer: opt in with -PrenderUi=true.
if (providers.gradleProperty("renderUi").orNull == "true") {
systemProperty("java.awt.headless", "false")
systemProperty("renderUi", "true")
}
maxHeapSize = "2g"
}

Expand Down
2 changes: 1 addition & 1 deletion packages/jetbrains/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

pluginGroup = com.rahulmahadik.asksql
pluginName = AskSQL
pluginVersion = 0.4.2
pluginVersion = 0.5.0

# IntelliJ Platform target used to COMPILE and RUN the sandbox. Broad
# compatibility is governed by pluginSinceBuild/pluginUntilBuild in
Expand Down
Binary file modified packages/jetbrains/images/onboarding.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/jetbrains/images/open-in-editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/jetbrains/images/schema-and-chat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/jetbrains/images/settings-more.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed packages/jetbrains/images/settings-ollama.png
Binary file not shown.
Binary file added packages/jetbrains/images/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/jetbrains/images/show-chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.rahulmahadik.asksql.ide.db

import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
Expand All @@ -17,7 +18,7 @@ import java.util.concurrent.atomic.AtomicInteger
* in-flight [withConnection] still uses: each [Slot] tracks a lease count and closes only once the last lease ends.
*/
@Service(Service.Level.PROJECT)
class ConnectionRegistry(private val project: Project, private val scope: CoroutineScope) {
class ConnectionRegistry(private val project: Project, private val scope: CoroutineScope) : Disposable {

private val log = logger<ConnectionRegistry>()

Expand Down Expand Up @@ -140,7 +141,11 @@ class ConnectionRegistry(private val project: Project, private val scope: Corout
}
}

/** Called by [com.rahulmahadik.asksql.ide.AskSqlProjectCloseListener] to release every connection deterministically before the scope is torn down. */
/** [closeNowOrCancel] is synchronous, so it is safe at dispose time. */
override fun dispose() {
closeAll()
}

fun closeAll() {
slots.keys.toList().forEach { id ->
slots.remove(id)?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.rahulmahadik.asksql.ide.db

import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
Expand All @@ -13,7 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger

/** Owns the lifecycle of every configured [MongoClient] for a project. */
@Service(Service.Level.PROJECT)
class MongoClientRegistry(private val project: Project, private val scope: CoroutineScope) {
class MongoClientRegistry(private val project: Project, private val scope: CoroutineScope) : Disposable {

private val log = logger<MongoClientRegistry>()

Expand Down Expand Up @@ -101,7 +102,11 @@ class MongoClientRegistry(private val project: Project, private val scope: Corou
}
}

/** Called by [com.rahulmahadik.asksql.ide.AskSqlProjectCloseListener] to release every client deterministically before the scope is torn down. */
/** [closeNowOrCancel] is synchronous, so it is safe at dispose time. */
override fun dispose() {
closeAll()
}

fun closeAll() {
slots.keys.toList().forEach { id ->
slots.remove(id)?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,15 @@ object Grounding {
return (aliases + ctes).toList()
}

private val PROSE_IDENTIFIER_RE = Regex("""`([^`\s]+)`|"([\w.]+)"|\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b""", RegexOption.IGNORE_CASE)
// Java's \s is ASCII-only where JavaScript's is not; these are the extras JS matches.
// UNICODE_CHARACTER_CLASS would widen \w below and diverge the other way.
private val PROSE_IDENTIFIER_RE = Regex(
"""`([^`\s\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+)`|"([\w.]+)"|\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b""",
RegexOption.IGNORE_CASE,
)

/** An identifier, optionally schema-qualified. Placeholders, literals and operators do not match. */
private val IDENTIFIER_SHAPE = Regex("""[a-z_][a-z0-9_$-]*(?:\.[a-z_][a-z0-9_$-]*)*""", RegexOption.IGNORE_CASE)

/**
* Identifier-shaped names in a prose answer absent from the catalog - the grounding floor for
Expand Down Expand Up @@ -141,6 +149,8 @@ object Grounding {
val scanned = if (documentStyle) answer.replace(Regex("```[\\s\\S]*?```"), " ") else answer
for (m in PROSE_IDENTIFIER_RE.findAll(scanned)) {
if (documentStyle && m.groupValues[2].isNotEmpty()) continue // "shipped" is a value
// Backticks wrap anything, so a placeholder or a literal can arrive here.
if (m.groupValues[1].isNotEmpty() && !IDENTIFIER_SHAPE.matches(m.groupValues[1])) continue
val raw = (m.groupValues[1].ifEmpty { m.groupValues[2] }.ifEmpty { m.groupValues[3] }).lowercase()
if (raw.startsWith("$")) continue // $lookup / $group are operators
// Backticked SQL vocabulary is not a name claim; a call with parentheses is a function.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.rahulmahadik.asksql.ide.model.GuardPolicy
import com.rahulmahadik.asksql.ide.model.GuardVerdict
import com.rahulmahadik.asksql.ide.model.LimitStyle
import net.sf.jsqlparser.JSQLParserException
import net.sf.jsqlparser.expression.AllValue
import net.sf.jsqlparser.expression.Expression
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter
import net.sf.jsqlparser.expression.Function
Expand Down Expand Up @@ -535,13 +536,12 @@ object SqlGuard {
return LimitStatus.Ok
}
val limit = target?.limit ?: return LimitStatus.None
// `LIMIT ALL` parses to a Limit with no row count. Reading that as "no limit present" left
// the statement uncapped, since the clause is already there for an append to bind to.
if (limit.isLimitAll) {
return LimitStatus.Unbounded {
limit.isLimitAll = false
limit.rowCount = LongValue(maxRows.toLong())
}
// `LIMIT ALL` parses to a Limit whose row count is an AllValue, which bounds nothing. Reading
// that as "no limit present" left the statement uncapped, since the clause is already there
// for an append to bind to. Overwriting the row count is what clears it (the deprecated
// setLimitAll(false) was a no-op).
if (limit.rowCount is AllValue) {
return LimitStatus.Unbounded { limit.rowCount = LongValue(maxRows.toLong()) }
}
val rowCount: Expression = limit.rowCount ?: return LimitStatus.None
val value = (rowCount as? LongValue)?.value ?: return LimitStatus.NonLiteral
Expand Down
Loading