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
1 change: 1 addition & 0 deletions .tabularium
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"views": true,
"routines": true,
"routine_management": true,
"table_query_templates": true,
"triggers": true,
"user_management": true,
"file_based": false,
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Opt-in `get_table_query_template` RPC for driver-owned SELECT, UPDATE and
DELETE previews, with SQL Server `TOP`, schema qualification and identifier
quoting (#26). Older hosts keep their existing generation path; no minimum
runtime version increase is required for this optional extension.

### Fixed

- Preserve explicit outer `TOP` and `OFFSET/FETCH` limits instead of adding
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ This plugin enables Tabularis to connect to SQL Server instances, providing sche
- Microsoft's `mssql-tds` protocol implementation through `mssql-tiberius-bridge`, with `deadpool` connection pooling, session reset (`sp_reset_connection`), startup scripts, and pool lifecycle handling
- Schema, table, column, PK/FK, index, view, routine, and trigger introspection
- Query execution with pagination, CTE/DML classification, multiple result sets, and session-preserving batches
- Driver-owned SELECT/UPDATE/DELETE previews on hosts supporting optional [SQL templates](docs/query-templates.md), including SQL Server `TOP` syntax
- Accurate affected rows, including multi-statement DML and DML `OUTPUT`
- INSERT/UPDATE/DELETE with composite primary keys and safe `IDENTITY_INSERT` recovery
- Table/view/index/foreign-key DDL and safe `ALTER COLUMN` generation
Expand Down
138 changes: 138 additions & 0 deletions docs/query-templates-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Table query templates: end-to-end verification

This checklist verifies the joint fix for [issue #26](https://github.com/TabularisDB/tabularis-sqlserver-plugin/issues/26):
SQL Server-native Generate SQL previews and preservation of explicit row limits.
It requires the plugin changes from PRs #30/#31 and the host extension from
[Tabularis PR #818](https://github.com/TabularisDB/tabularis/pull/818).

## Automated checks

From the plugin checkout:

```bash
cargo test --locked --bins --test conformance
cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
just test-explain
```

Start a local SQL Server with `just run-sqlserver`, or use an existing disposable
instance. Run live tests against a dedicated database, not production:

```bash
export SQLSERVER_TEST_HOST=127.0.0.1
export SQLSERVER_TEST_PORT=1433
export SQLSERVER_TEST_USER=sa
# Set SQLSERVER_TEST_PASSWORD to your local container password if not the default.
export SQLSERVER_TEST_DATABASE=tabularis_pr31_test
cargo test --locked --test live_db -- --test-threads=1
```

The suite creates the database and its own scratch schema. Coverage includes:

- SELECT previews executed with conflicting host pagination parameters;
- explicit outer TOP, TOP PERCENT, WITH TIES and OFFSET/FETCH, in queries and batches;
- normal pagination for unbounded SELECT statements;
- escaped table/column identifiers containing `]`;
- unique UPDATE placeholders for similarly named columns;
- preview generation without writes, and zero affected rows when executing guarded
UPDATE/DELETE statements after filling their parameters.

In the matching Tabularis checkout, run frontend tests and compile **all** Rust
test targets so optional-capability additions also check integration fixtures:

```bash
pnpm exec vitest run
pnpm build
cd src-tauri
cargo test --locked --all-targets --no-run
cargo test --locked --lib plugins::
```

## Prepare the desktop fixture

1. Build and install the plugin with `just dev-install`, then restart Tabularis.
If Cargo uses a global target directory, use `CARGO_TARGET_DIR=target just
dev-install`: the recipe expects the binary under `target/debug`.
2. Run a host containing PR #818, for example `pnpm tauri dev` from the matching
Tabularis checkout. A browser-only `pnpm dev` does not exercise Tauri commands.
3. Enable SQL Server in Settings → Plugins.
4. Create a SQL-authenticated connection to the dedicated `tabularis_pr31_test`
database. For a local container with a self-signed certificate, use SSL mode
`require`; use certificate verification for real servers.
5. Execute `tests/fixtures/query_templates_e2e.sql` in that database, via sqlcmd
or the editor. It creates `pr31.orders` (150 rows) and `pr31.order]details`
(2 rows), without dropping or resetting existing tables. Refresh the explorer.

Debug hosts may show a runtime-floor warning when their development version
predates the plugin minimum. Do not lower the packaged manifest's runtime floor
just to suppress this warning; use a compatible host for release verification.

## SELECT previews and pagination

Right-click `pr31.orders` → **Generate SQL**:

- **SELECT \*** must show `SELECT * FROM [pr31].[orders];` (line breaks may vary).
It is unbounded SQL; normal host pagination still applies when executed.
- **SELECT [fields]** must start with `SELECT TOP (100)`, bracket-quote every
column, and target `[pr31].[orders]`. There must be **no LIMIT clause**.
- **Run in console** must open an editor on this connection without automatically
executing the statement.
- Set the editor page size to **50** and explicitly execute the generated TOP
statement. Expect **100 rows**, not 50, and no TOP/OFFSET syntax error.
- Execute the unbounded SELECT instead: paging should still work through the 150
rows. Add `ORDER BY [id]` for deterministic page contents.

The CREATE TABLE tab is deliberately unchanged by the template extension.

## Guarded UPDATE and DELETE

- UPDATE must target `[pr31].[orders]`, use four distinct `:value_1` … `:value_4`
placeholders, and end with `WHERE 1 = 0;`. `[a b]` and `[a-b]` must not share
a placeholder.
- Open it in the editor and fill the parameter dialog with SQL literals `999`,
`N'closed'`, `20`, `30`. Values are substituted verbatim; quote string values.
- **Keep `WHERE 1 = 0` unchanged.** Execute: expect **0 affected rows**.
- DELETE must also end with `WHERE 1 = 0;`. Execute unchanged: expect **0 affected
rows**.
- Verify the fixture remains unchanged:

```sql
SELECT COUNT(*) AS rows_remaining,
SUM([a b]) AS sum_ab,
SUM([a-b]) AS sum_dash
FROM [pr31].[orders];
-- Expected: 150, 113250, 1132500
```

The fixtures deliberately avoid IDENTITY columns. The template contract accepts
column names, not metadata for excluding read-only columns; remove IDENTITY or
computed assignments before executing UPDATE previews on other tables.

## Escaping and alternate entry points

Generate SELECT fields on `pr31.order]details`. Expect
`FROM [pr31].[order]]details]` and `[order]]status]`; execution returns 2 rows.

With page size 50, explicitly execute each statement separately:

```sql
SELECT TOP (3) [id] FROM [pr31].[orders] ORDER BY [id];
-- 1, 2, 3

SELECT [id] FROM [pr31].[orders]
ORDER BY [id] OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY;
-- 3, 4, 5
```

Also open Generate SQL through the Command Palette with split editors or multiple
connections. The selected table's connection and schema must be retained.
A legacy/non-opted-in driver, such as built-in SQLite, must still open Generate
SQL normally. Automated host tests cover missing-method fallback and propagation
of real RPC errors; failures must not silently substitute another SQL dialect.

## Registry rollout

Local installation bypasses the registry. Register the optional capability in
Tabularium before publishing, as described in [query-templates.md](query-templates.md),
and keep the release's documented host minimum aligned with its manifest.
87 changes: 87 additions & 0 deletions docs/query-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Optional table query templates

The plugin advertises `capabilities.table_query_templates: true` and implements
`get_table_query_template` for compatible Tabularis hosts. The request and result
are additive: existing RPC methods and the minimum runtime version are unchanged.

```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_table_query_template",
"params": {
"params": { "driver": "sqlserver" },
"request": {
"table": "orders",
"schema": "sales",
"kind": "select",
"columns": ["id", "status"],
"limit": 100
}
}
}
```

The result is a string:

```sql
SELECT TOP (100)
[id],
[status]
FROM [sales].[orders];
```

- `kind`: `select`, `update` or `delete`.
- Names are unquoted identifiers. The plugin applies SQL Server bracket escaping.
- `columns` defaults to `[]`; SELECT then uses `*`, UPDATE emits a placeholder.
- Missing/null `schema` uses `dbo`; missing/null `limit` leaves SELECT unbounded.
- `limit` is a non-negative u32, including zero, and is rejected for UPDATE/DELETE.
- UPDATE emits unique `:value_N` host-editor placeholders. Both UPDATE and DELETE
include `WHERE 1 = 0` so the preview cannot accidentally modify all rows.
- This method does not open a database connection or execute SQL.

## Local end-to-end verification

See [query-templates-e2e.md](query-templates-e2e.md) for the joint host/plugin
setup, automated results, disposable SQL fixture and desktop acceptance checklist.

## Compatibility and rollout

1. Merge the pagination fix in [PR #30](https://github.com/TabularisDB/tabularis-sqlserver-plugin/pull/30).
2. Register the optional capability in Tabularium's driver-kind schema (below).
3. Release Tabularis with the optional template RPC. Drivers without the capability
retain legacy generation. Only a remote `-32601` selects the legacy fallback;
real errors and malformed results are surfaced.
4. Release this plugin's template support. The full issue #26 fix requires both
the host extension and the plugin pagination fix. Older hosts continue working,
but their Generate SQL dialog still uses the old generation logic.

CREATE TABLE inspection remains unchanged in this extension. It is separate from
these query templates and from the existing DDL RPC contract.

## Tabularium

Tabularium distributes and validates the manifest; it does not participate in
runtime SQL generation. Register the following optional property under the driver
kind's `capabilities.properties` for validation and generated docs:

```json
{
"table_query_templates": {
"type": "boolean",
"default": false,
"description": "Generate SQL SELECT/UPDATE/DELETE previews through the optional get_table_query_template RPC."
}
}
```

Do not replace the other capability definitions or add this flag to `required`.
Although the registry's current capabilities schema allows additional properties,
its ingestion calls `validateManifest` with `lenient: true` (AJV
`removeAdditional: 'all'`). Undeclared capability keys are therefore stripped from
the normalized registry metadata. Register the property **before publishing** the
plugin release; if it was already ingested, refresh its manifest afterward.

This is a registry administrator's schema/configuration update, not a backend or
SDK protocol change. No database migration, new API endpoint, historical release
archive rewrite or minimum-host-version increase is needed.
1 change: 1 addition & 0 deletions src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod helpers;
pub mod introspection;
pub mod ops;
pub mod pool;
pub mod query_templates;
pub mod routines;
pub mod triggers;
pub mod types;
Expand Down
78 changes: 78 additions & 0 deletions src/driver/query_templates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Pure, opt-in SQL previews for the host's Generate SQL dialog.

use serde::Deserialize;

use super::helpers::{bracket_quote, qualify};

#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TemplateKind {
Select,
Update,
Delete,
}

#[derive(Debug, Deserialize)]
pub struct TemplateRequest {
pub table: String,
pub schema: Option<String>,
pub kind: TemplateKind,
#[serde(default)]
pub columns: Vec<String>,
pub limit: Option<u32>,
}

pub fn build(request: &TemplateRequest) -> Result<String, String> {
if request.table.trim().is_empty() || request.columns.iter().any(|name| name.trim().is_empty())
{
return Err("Table and column names must not be empty".into());
}
let target = qualify(request.schema.as_deref(), &request.table);
match request.kind {
TemplateKind::Select => {
let top = request
.limit
.map(|limit| format!(" TOP ({limit})"))
.unwrap_or_default();
let fields = if request.columns.is_empty() {
" *".to_string()
} else {
format!(
"\n{}",
request
.columns
.iter()
.map(|name| format!(" {}", bracket_quote(name)))
.collect::<Vec<_>>()
.join(",\n")
)
};
Ok(format!("SELECT{top}{fields}\nFROM {target};"))
}
TemplateKind::Update | TemplateKind::Delete if request.limit.is_some() => {
Err("Template limit is only supported for SELECT".into())
}
TemplateKind::Update => {
let assignments = if request.columns.is_empty() {
" [column] = :value_1".to_string()
} else {
request
.columns
.iter()
.enumerate()
.map(|(index, name)| {
// Host editor placeholders, not SQL Server @parameters.
// Ordinals prevent collisions for similarly named columns.
format!(" {} = :value_{}", bracket_quote(name), index + 1)
})
.collect::<Vec<_>>()
.join(",\n")
};
Ok(format!("UPDATE {target}\nSET\n{assignments}\nWHERE 1 = 0;"))
}
TemplateKind::Delete => Ok(format!("DELETE\nFROM {target}\nWHERE 1 = 0;")),
}
}

#[cfg(test)]
mod tests;
Loading
Loading