[BUG] Column profiling fails with SQL syntax errors on columns named after reserved keywords due to unforced identifier quoting in /api/db/profile

Open Beginner friendly
#2,245 2 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@saad-works is already working on this.

Since Nov 5, 2026.

  • #2250 by @saad-works — open

Assessment

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

Research direction

Read src/lib/query-generators.ts around the always option, then inspect the two quoteIdentifier calls in src/app/api/db/profile/route.ts (around lines 339 and 347). Pass { always: true } at both call sites and check the profile route’s existing tests, adding or running coverage for a column named order; done means profiling returns statistics and samples without SQL syntax errors.

Written by the indexing model from the issue text.

Description

bug

Description

When profiling a SQL database table via POST /api/db/profile, the route queries summary statistics (COUNT(*), COUNT(col), COUNT(DISTINCT col), MIN(col), MAX(col)) and samples the top rows.

To format column names for the target dialect, the route calls quoteIdentifier(col, capabilities) in src/app/api/db/profile/route.ts at line 339 and line 347. However, it omits { always: true }.

As documented in src/lib/query-generators.ts (lines 61–65):

 * `always` quotes a name that would round-trip bare as well. The bare test is about case and
 * characters only, so a lowercase reserved word passes it: `when`, `order`, `user` and `group`
 * come back bare and the engine reads them as keywords (#1396).

Because { always: true } is not passed, any column whose name matches a standard SQL keyword (e.g. order, group, user, from, key, default, type, date, time, check, value, offset, limit) passes the /^[a-z_][a-z0-9_$]*$/ test and is returned bare (unquoted).

When profileSqlColumn executes:

SELECT COUNT(*) AS total_count, COUNT(order) AS non_null_count, COUNT(DISTINCT order) AS distinct_count, MIN(order) AS min_value, MAX(order) AS max_value FROM "table"

and:

SELECT COUNT(*) AS null_count FROM "table" WHERE order IS NULL

and:

SELECT order FROM "table" LIMIT 5

every SQL dialect (PostgreSQL, SQLite, MySQL, SQL Server, Oracle, ClickHouse) rejects the statements with an immediate SQL syntax error (near ")": syntax error or syntax error at or near "order"). The entire profile request fails or reports an error for that column rather than returning statistics.

Steps to Reproduce

  1. In SQLite, PostgreSQL, or MySQL, create a table containing a column whose name is a reserved SQL keyword:
    CREATE TABLE customer_orders (id INT PRIMARY KEY, "order" INT);
    
  2. In LibreDB Studio Schema Explorer, click "Profile" on the customer_orders table, or invoke POST /api/db/profile directly:
    {
      "connection": { ... },
      "tablePath": ["customer_orders"],
      "columns": ["order"]
    }
    
  3. Inspect the response payload or UI card.
  4. Notice that profiling fails with a syntax error for the "order" column.

Expected Behavior

The column name should be escaped/quoted using the target dialect's identifier quotes (e.g., "order" in SQLite/PostgreSQL/Oracle or `order` in MySQL) so that queries like COUNT("order") and WHERE "order" IS NULL execute cleanly.

Actual Behavior

quoteIdentifier("order", capabilities) returns order unquoted because { always: true } is omitted. The database throws a SQL syntax error, and profiling for that column fails.

Environment

  • LibreDB Studio Version: 1.2.0
  • Browser: All
  • OS: macOS / Linux / Windows
  • Node.js/Bun Version: Bun 1.4.3 / Node.js 24
  • Database Type: PostgreSQL, MySQL, SQLite, Oracle, SQL Server, ClickHouse
  • Database Version: All

Additional Context

src/components/DataImportModal.tsx already uses { always: true } for this exact reason:

return capabilities === undefined ? name : quoteIdentifier(name, capabilities, { always: true });

src/app/api/db/profile/route.ts is missing this argument at lines 339 and 347.

Possible Solution

In src/app/api/db/profile/route.ts, pass { always: true } to quoteIdentifier:

// Line 339
for (const col of profiled) {
  columnProfiles.push(
    await profileSqlColumn(provider, safeTable, col, quoteIdentifier(col, capabilities, { always: true }), totalRows),
  );
}

// Line 347
const topCols = profiled.slice(0, 5);
const safeCols = topCols.map((c) => quoteIdentifier(c, capabilities, { always: true })).join(", ");
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.