[BUG] Inline row editing refuses SQL Server table-click, Generate Query, and SELECT TOP queries because selectsPlainColumn does not skip TOP

Open
#2,265 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
38/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
sql, typescript
Domain
database

Research direction

Read src/lib/sql/update-target.ts, especially selectsPlainColumn around lines 380–473, and the existing cases in tests/unit/sql/update-target.test.ts. Reproduce with the SQL Server SELECT TOP examples in the issue, then add coverage for supported TOP forms and cases that must remain accurate. Done when direct base-table columns behind TOP are recognized while aliases, expressions, and columns or functions named top are handled correctly.

Written by the indexing model from the issue text.

Description

Description

In src/lib/sql/update-target.ts, selectsPlainColumn (lines 380–473) strips ALL, DISTINCT, and DISTINCT ON (...) after SELECT (lines 390–404), but never skips a TOP row-count clause (TOP <n> or TOP (<expr>) [PERCENT] [WITH TIES]).

On SQL Server (mssql), both of LibreDB Studio's built-in Schema Explorer query actions emit SELECT TOP ...:

  1. Clicking a table in Schema Explorer (generateTableQuery → MSSQL_SQL_PORT_GENERATORS.select in src/lib/db/providers/sql/mssql-generators.ts, line 17):
    SELECT TOP 50 * FROM dbo.users;
    
  2. Clicking "Generate Query" on a table in Schema Explorer (generateSelectQuery → MSSQL_SQL_PORT_GENERATORS.selectList in src/lib/db/providers/sql/mssql-generators.ts, line 18):
    SELECT TOP 100
      id,
      name
    FROM dbo.users
    WHERE 1=1;
    

When the user edits a cell in the Results Grid and clicks Apply Changes, handleApplyChanges in src/hooks/use-inline-editing.ts first calls resolveUpdateTarget(sql, activeConnection.type) (line 676), which succeeds and resolves { kind: "table", table: "dbo.users" } (explicitly covered by tests/unit/sql/update-target.test.ts line 62: expect(table("SELECT TOP 50 * FROM users;", "mssql")).toBe("users")).

Immediately afterward, handleApplyChanges checks selectsPlainColumn(currentTab.resultQuery ?? currentTab.query, pkColumn, activeConnection.type) (line 753). Because selectsPlainColumn does not advance start past TOP, the TOP tokens remain attached to the first item of items before the first comma:

Query shape resolveUpdateTarget(sql, "mssql") First item in selectsPlainColumn selectsPlainColumn(sql, "id", "mssql") handleApplyChanges outcome
SELECT TOP 50 * FROM dbo.users; (Schema Explorer table click) { kind: "table", table: "dbo.users" } [TOP, 5, 0, *] (isStar → false) false (for every column) Refused with destructive toast
SELECT TOP 100\n id,\n name\nFROM dbo.users\nWHERE 1=1; (Schema Explorer "Generate Query") { kind: "table", table: "dbo.users" } [TOP, 1, 0, 0, id] (isReference → false) false Refused with destructive toast
SELECT TOP (50) * FROM [dbo].[users]; { kind: "table", table: "[dbo].[users]" } [TOP, (, ), *] (isStar → false) false (for every column) Refused with destructive toast
SELECT TOP (100) id, name FROM dbo.users; { kind: "table", table: "dbo.users" } [TOP, (, ), id] (isReference → false) false Refused with destructive toast
SELECT DISTINCT TOP 50 id, name FROM dbo.users; { kind: "table", table: "dbo.users" } [TOP, 5, 0, id] (isReference → false) false Refused with destructive toast
SELECT TOP 10 PERCENT WITH TIES id, name FROM dbo.users ORDER BY id; { kind: "table", table: "dbo.users" } [TOP, 1, 0, PERCENT, WITH, TIES, id] false Refused with destructive toast

Steps to Reproduce

  1. On main (f9f03596e), run with Bun:
import { generateSelectQuery, generateTableQuery } from "@/lib/query-generators";
import { resolveUpdateTarget, selectsPlainColumn } from "@/lib/sql/update-target";

const mssqlCaps = { defaultPort: 1433, queryLanguage: "sql" } as any;
const cols = [
  { name: "id", type: "int", nullable: false },
  { name: "name", type: "nvarchar(100)", nullable: true },
];

const tableClickSql = generateTableQuery(["dbo", "users"], cols, mssqlCaps);
console.log("Table-click SQL:", tableClickSql);
console.log("  resolveUpdateTarget:", resolveUpdateTarget(tableClickSql, "mssql"));
console.log("  selectsPlainColumn(id):", selectsPlainColumn(tableClickSql, "id", "mssql"));

const generateQuerySql = generateSelectQuery(["dbo", "users"], cols, mssqlCaps);
console.log("\nGenerate-Query SQL:\n" + generateQuerySql);
console.log("  resolveUpdateTarget:", resolveUpdateTarget(generateQuerySql, "mssql"));
console.log("  selectsPlainColumn(id):", selectsPlainColumn(generateQuerySql, "id", "mssql"));

for (const sql of [
  "SELECT TOP (50) * FROM [dbo].[users]",
  "SELECT TOP (100) id, name FROM dbo.users",
  "SELECT DISTINCT TOP 50 id, name FROM dbo.users",
  "SELECT TOP 10 PERCENT WITH TIES id, name FROM dbo.users ORDER BY id",
]) {
  console.log(`\n${sql}`);
  console.log("  selectsPlainColumn(id):", selectsPlainColumn(sql, "id", "mssql"));
}
  1. Observed output on main (f9f03596e):
Table-click SQL: SELECT TOP 50 * FROM dbo.users;
  resolveUpdateTarget: { kind: "table", table: "dbo.users" }
  selectsPlainColumn(id): false

Generate-Query SQL:
SELECT TOP 100
  id,
  name
FROM dbo.users
WHERE 1=1;
  resolveUpdateTarget: { kind: "table", table: "dbo.users" }
  selectsPlainColumn(id): false

SELECT TOP (50) * FROM [dbo].[users]
  selectsPlainColumn(id): false

SELECT TOP (100) id, name FROM dbo.users
  selectsPlainColumn(id): false

SELECT DISTINCT TOP 50 id, name FROM dbo.users
  selectsPlainColumn(id): false

SELECT TOP 10 PERCENT WITH TIES id, name FROM dbo.users ORDER BY id
  selectsPlainColumn(id): false
  1. In LibreDB Studio connected to SQL Server (mssql), click any table with an id or *_id primary key in Schema Explorer (or use Generate Query), double-click a cell in the Results Grid to edit it, and click Apply Changes. useInlineEditing aborts before issuing any query and shows the destructive toast:

    Cannot Apply Changes
    id is not read straight from the table here, so it cannot identify a row to write to. Edit the SQL manually.

Expected Behavior

  • selectsPlainColumn skips a leading TOP <n> or TOP (<expr>) clause (and optional PERCENT / WITH TIES modifiers) after SELECT [ALL | DISTINCT], just as it already skips ALL, DISTINCT, and DISTINCT ON (...).
  • Inline row editing on SQL Server succeeds for Schema Explorer table-click queries (SELECT TOP 50 * FROM ...), Generate Query tabs (SELECT TOP 100\n id, ...\nFROM ...), and hand-written single-table SELECT TOP ... queries when the key column is selected directly from the base table.
  • Expressions or renames behind TOP (such as SELECT TOP 50 sku AS id, name FROM products or SELECT TOP 50 ROW_NUMBER() OVER (ORDER BY name) AS id, name FROM products), as well as a column or scalar function named top (SELECT top FROM boxes, SELECT top(x) AS id FROM users), continue to be handled accurately without false positives.

Actual Behavior

selectsPlainColumn leaves TOP ... inside the first select-list item, returning false for every column on SELECT TOP <n> * FROM ... and false for the first projected column on SELECT TOP <n> id, ... FROM ..., which causes useInlineEditing to refuse inline edits on SQL Server.

Environment

  • LibreDB Studio Version: 1.0.0 (main @ f9f03596e)
  • OS: Linux (WSL2 on Windows 11)
  • Node.js/Bun Version: Bun 1.4.2
  • Database Type: SQL Server (mssql), DuckDB (duckdb)

Possible Solution

In selectsPlainColumn (src/lib/sql/update-target.ts, lines 390–405), after advancing start past ALL / DISTINCT / DISTINCT ON (...), advance start past a TOP clause when isWord(top[start], "TOP") is followed by:

  1. a numeric literal (top[start + 1]?.kind === "other" && /^[0-9]$/.test(top[start + 1].text)), stepping past contiguous digit/decimal pieces (/^[0-9.]$/), or
  2. a parenthesised count (top[start + 1]?.kind === "other" && top[start + 1].text === "(") whose closing ) is followed by a select-list item rather than ,, FROM, or AS (so a scalar function call like SELECT top(x) AS id FROM users or SELECT top(x) FROM users is not mistaken for a TOP clause),

followed by optional PERCENT and optional WITH TIES.


I have reproduced this locally and can put together the fix and unit tests in tests/unit/sql/update-target.test.ts and tests/hooks/use-inline-editing.test.ts. Could you please assign this issue to me?

Dominant language
TypeScript
Stars
810
Forks
158
Avg merge
9h 45m
Merged PRs (30d)
539

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from libredb/libredb-studio

All issues in libredb/libredb-studio

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.