Skip to content

Schema table actions: open generated SQL in a new query tab #180

Description

@BorisTyshkevich

Problem

The schema tree currently generates SQL for a table directly into the active editor:

  • double-click replaces the active editor with:

    SELECT * FROM db.table LIMIT 100
  • Shift-click fetches SHOW CREATE, formats the returned DDL, and replaces the active editor with it.

Both actions can destroy an unrelated query that the user was editing.

The application already has a normal tab-opening path:

app.actions.loadIntoNewTab(name, sql)

Generated table SQL should use that path instead of replacing the current document.

Goal

For table rows in the schema tree:

  • double-click opens the generated SELECT * in a new query tab;

  • Shift-click opens the formatted SHOW CREATE result in a new query tab;

  • the new tab is named with the unquoted qualified table name:

    db.table
    
  • the previously active tab and its SQL remain unchanged.

Required behavior

Double-click

Given a table:

database: db1
table: orders

double-click opens a new active tab:

Tab name: db1.orders
SQL:      SELECT * FROM db1.orders LIMIT 100

Identifier quoting remains SQL-safe:

Tab name: analytics.daily events
SQL:      SELECT * FROM analytics.`daily events` LIMIT 100

The display name uses the original database and table names. The SQL uses the existing qualifyIdent() result.

Shift-click

Shift-click executes the existing two-step DDL retrieval:

SHOW CREATE db1.orders FORMAT JSON

followed by best-effort formatting through formatQuery().

After a successful response, open a new active tab:

Tab name: db1.orders
SQL:      <formatted DDL>

If formatting fails, open the raw DDL, matching the existing fallback behavior.

If SHOW CREATE fails or returns no statement:

  • do not create a tab;
  • do not modify the current editor;
  • preserve the existing error toast behavior.

Existing tab preservation

For both actions:

  • current tab SQL is byte-for-byte unchanged;
  • current tab name, result, saved-query link, panel configuration, and dirty state are unchanged;
  • no SQL is automatically executed;
  • no generated tab is automatically saved to the Library.

Expansion behavior

Keep the current cross-browser double-click model.

The first click of a quick two-click sequence may still perform the ordinary table expand/collapse action before the second click opens the new tab. Avoiding that would require delaying every single-click expansion and is outside this fix.

Database and column rows

Do not change other schema-row behavior in this issue:

  • database double-click still inserts the database identifier at the cursor;
  • database Shift-click keeps its existing SHOW CREATE DATABASE behavior;
  • column double-click and Shift-click keep their insert-at-cursor behavior;
  • drag-and-drop behavior is unchanged.

Implementation

src/ui/schema.js

Replace the table double-click action:

app.actions.replaceEditor(
  'SELECT * FROM ' + qname + ' LIMIT 100',
);

with:

app.actions.loadIntoNewTab(
  key,
  'SELECT * FROM ' + qname + ' LIMIT 100',
);

Here:

key = db.db + '.' + tb.name

is the requested human-readable tab name, while qname remains the SQL-safe qualified identifier.

Replace the table Shift-click action with a dedicated new-tab DDL action:

app.actions.openCreateInNewTab(qname, key);

Do not call the current editor-replacing insertCreate() path for a table.

Update the no-comment table hover text so the destination is explicit:

Click to expand · double-click SELECT * in new tab ·
shift-click SHOW CREATE in new tab · drag to insert name

src/ui/app.js

Separate fetching/formatting the DDL from deciding where it is placed.

A suitable shape is:

async function fetchCreateSql(target) {
  await ensureConfig();
  if (!(await getToken())) {
    chCtx.onSignedOut();
    return null;
  }

  try {
    const show = await ch.queryJson(
      chCtx,
      'SHOW CREATE ' + target + ' FORMAT JSON',
    );

    const stmt =
      (show.data && show.data[0] && show.data[0].statement) || '';

    if (!stmt) return null;

    try {
      const formatted = await ch.queryJson(
        chCtx,
        'SELECT formatQuery(' + sqlString(stmt) + ') AS q FORMAT JSON',
      );

      return (
        formatted.data &&
        formatted.data[0] &&
        formatted.data[0].q
      ) || stmt;
    } catch {
      return stmt;
    }
  } catch (error) {
    flashToast(
      'SHOW CREATE failed: ' +
        String((error && error.message) || error),
      { document: doc },
    );
    return null;
  }
}

Keep the existing editor-replacing action for its existing consumers:

async function insertCreate(target) {
  const sql = await fetchCreateSql(target);
  if (sql != null) app.editor.replaceDocument(sql);
}

Add:

async function openCreateInNewTab(target, name) {
  const sql = await fetchCreateSql(target);
  if (sql == null) return;

  loadIntoNewTab(app, name, sql);
  toEditorOnMobile();
}

Expose it through:

app.actions.openCreateInNewTab

Equivalent factoring is acceptable. The important contract is that fetching and formatting are shared rather than duplicated.

Tests

Schema dispatch

Update tests/unit/schema.test.js.

Double-clicking a table:

  • calls:

    loadIntoNewTab(
      'db1.orders',
      'SELECT * FROM db1.orders LIMIT 100',
    )
  • does not call replaceEditor().

Shift-clicking a table:

  • calls:

    openCreateInNewTab('db1.events', 'db1.events')
  • does not call insertCreate();

  • does not expand the table;

  • does not load columns.

Add a quoted-identifier case verifying:

display name: analytics.daily events
SQL: SELECT * FROM analytics.`daily events` LIMIT 100

DDL new-tab action

Update tests/unit/app.test.js.

On success:

  • preserve the original active tab and its SQL;
  • create exactly one additional tab;
  • make it active;
  • set its name to db1.events;
  • set its SQL to the formatted DDL;
  • do not execute the generated SQL.

When formatting fails:

  • create the tab with raw DDL.

When SHOW CREATE fails or returns no statement:

  • create no tab;
  • preserve the current tab;
  • show the existing failure toast where applicable.

Regressions

Verify:

  • existing insertCreate() behavior remains covered for unchanged consumers;
  • loadIntoNewTab() still focuses the editor;
  • database and column schema interactions remain unchanged;
  • table single-click expansion and lazy column loading remain unchanged.

Files

Expected changes:

  • src/ui/schema.js
  • src/ui/app.js
  • tests/unit/schema.test.js
  • tests/unit/app.test.js

No new runtime dependency.

Acceptance criteria

  • Double-clicking a table opens SELECT * FROM … LIMIT 100 in a new active tab.
  • Shift-clicking a table opens formatted SHOW CREATE DDL in a new active tab.
  • Both generated tabs are named db.table using original display names.
  • SQL identifiers remain correctly quoted through qualifyIdent().
  • The previously active tab is not modified.
  • Formatting failure falls back to raw DDL.
  • SHOW CREATE failure or an empty result creates no tab.
  • No generated SQL is automatically executed or saved.
  • Database, column, drag, and single-click expansion behavior remains unchanged.
  • Coverage gates hold; no new runtime dependency.

Non-goals

  • Preventing the first click of a double-click from toggling expansion.
  • Changing database-row actions.
  • Changing column-row actions.
  • Reusing an already-open tab with the same name.
  • Automatically running generated SQL.
  • Automatically saving generated tabs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions