From 99bc229c6be3ae7953c1b4b3ee471e97afdda9d2 Mon Sep 17 00:00:00 2001 From: Bob Cozzi Date: Mon, 15 Sep 2025 13:04:26 -0500 Subject: [PATCH 1/2] Checkpoint from VS Code for coding agent session --- QCSRC/DB2JSON.CPP | 100 ++++++-- changelog.md | 3 + css/{db2json.css => db2query.css} | 47 +++- db2json.html | 144 ----------- db2json.md => db2query.md | 24 +- index.html | 33 +-- js/{db2json.js => db2query.js} | 235 +++++++++++------- js/{db2json_sqlFmt.js => db2query_sqlFmt.js} | 2 +- ...db2json_sqlHist.js => db2query_sqlHist.js} | 0 9 files changed, 310 insertions(+), 278 deletions(-) rename css/{db2json.css => db2query.css} (89%) delete mode 100644 db2json.html rename db2json.md => db2query.md (60%) rename js/{db2json.js => db2query.js} (84%) rename js/{db2json_sqlFmt.js => db2query_sqlFmt.js} (96%) rename js/{db2json_sqlHist.js => db2query_sqlHist.js} (100%) diff --git a/QCSRC/DB2JSON.CPP b/QCSRC/DB2JSON.CPP index e5d4a8c..34a72cc 100644 --- a/QCSRC/DB2JSON.CPP +++ b/QCSRC/DB2JSON.CPP @@ -177,6 +177,7 @@ struct json_symbols_t { const char* POSITION; const char* ERROR; const char* SQLSTATE; + const char* SQLCODE; const char* MSGTEXT; const char* DB2JSON; }; @@ -219,6 +220,7 @@ const json_symbols_t json_ascii = { "\"position\"", "\"error\"", "\"sqlstate\"", + "\"sqlcode\"", "\"msgtext\"", "\"DB2JSON\"" }; @@ -260,6 +262,7 @@ const json_symbols_t json_job = { "\"position\"", "\"error\"", "\"sqlstate\"", + "\"sqlcode\"", "\"msgtext\"", "\"DB2JSON\"" }; @@ -423,13 +426,15 @@ int checkError(std::ostream& output, SQLRETURN rc, SQLHANDLE handle, SQLSMALLINT { return 0; } + + char buff[256]; SQLCHAR sqlstate[11] = {0}; SQLCHAR message[SQL_MAX_MESSAGE_LENGTH+1] = {0}; - SQLINTEGER nativeError = 0; + SQLINTEGER sqlcode = 0; SQLSMALLINT textLen = 0; if (handle != SQL_NULL_HANDLE) { - SQLGetDiagRec(type, handle, 1, sqlstate, &nativeError, message, sizeof(message)-1, &textLen); + SQLGetDiagRec(type, handle, 1, sqlstate, &sqlcode, message, sizeof(message)-1, &textLen); } else if (msg != NULL && strlen(msg) > 0) { textLen = strlen(msg); @@ -439,26 +444,66 @@ int checkError(std::ostream& output, SQLRETURN rc, SQLHANDLE handle, SQLSMALLINT } } message[textLen] = 0x00; - logSQLMsg("Error: %5s, %s",sqlstate, message); + logSQLMsg("SQLSTATE: %5s SQLCODE: %d - %s",sqlstate, sqlcode, message); std::string sqlstateStr = cvtToASCII(reinterpret_cast(sqlstate)); std::string msgtextStr = cvtToASCII(reinterpret_cast(message)); + std::string sqlcodeStr; + char sqlcodeBuf[128]; + + switch (sqlcode) { + case -901: + strcpy(sqlcodeBuf, "SQL system error (-901)"); + break; + case -902: + strcpy(sqlcodeBuf, "System error (-902)"); + break; + case -204: + strcpy(sqlcodeBuf, "Object not found (-204)"); + break; + case -443: + strcpy(sqlcodeBuf, "Routine/function error (-443)"); + break; + case -7008: + strcpy(sqlcodeBuf, "Object not valid for operation (-7008)"); + break; + case -30020: + strcpy(sqlcodeBuf, "Communication error with database host server (-30020)"); + break; + default: + // fallback: just put the numeric code in text form + if (sqlcode != 0) { + sprintf(sqlcodeBuf, "non-zero SQLCODE detected", (int)sqlcode); + } + break; + } + + // Convert to ASCII for JSON + if (sqlcode != 0) + { + sprintf(buff," SQLCODE: %d - %s", sqlcode, sqlcodeBuf); + sqlcodeStr = cvtToASCII(reinterpret_cast(buff)); + } + + +// Then include sqlcodeStr in your JSON output + bool rootWasOpen = closeJsonNode(output); if (!rootWasOpen) { output << json->LCURLY; // open root object if not already open g_rootOpen = true; } - char buf[16]; - sprintf(buf,"%d", errPos); - std::string errPosStr = cvtToASCII(reinterpret_cast(buf)); + sprintf(buff,"%d", errPos); + std::string errPosStr = cvtToASCII(reinterpret_cast(buff)); + output << json->LF << json->ERROR << json->COLON << json->LCURLY << json->SQLSTATE << json->COLON << json->QUOTE << jsonEscape(sqlstateStr) << json->QUOTE << json->COMMA << json->POSITION << json->COLON << errPosStr << json->COMMA << json->MSGTEXT << json->COLON - << json->QUOTE << jsonEscape(msgtextStr) << json->QUOTE + << json->QUOTE << jsonEscape(msgtextStr) << jsonEscape(sqlcodeStr) << json->QUOTE << json->RCURLY; if (g_rootOpen) { output << json->LF << json->RCURLY << json->LF; @@ -1229,6 +1274,10 @@ int main(int argc, char *argv[]) rc = SQLSetConnectAttr(hDbc,SQL_ATTR_EXTENDED_COL_INFO, (SQLPOINTER)&attr,0); ifCheckError(output, rc, hDbc, SQL_HANDLE_DBC, "SetConnAttr"); + attr = SQL_AUTOCOMMIT_ON; + SQLSetConnectAttr(hDbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)&attr, 0); + + #ifndef TOASCII attr = SQL_TRUE; // Return UTF-8 data rc = SQLSetConnectAttr(hDbc, SQL_ATTR_UTF8, (SQLPOINTER)&attr,0); @@ -1564,8 +1613,10 @@ int main(int argc, char *argv[]) g_rootOpen = false; // Cleanup - if (hStmt) + if (hStmt) { + SQLCloseCursor(hStmt); SQLFreeHandle(SQL_HANDLE_STMT, hStmt); + } if (hDbc) SQLDisconnect(hDbc); if (hDbc) @@ -1651,34 +1702,35 @@ const char* sqlTypeToJSONText(int sqlType) { int SQLSyntaxCheck( std::string& sqlStmt, std::string& rtnMsg ) { -Qus_EC_t ec; -int recProc = 0; -int restrictedVerb = 0; -char langID[10]; -char msgid[8]; -char msgfile[11]; -char msgflib[11]; -char msgf[21]; -char subValues[11]; -char fmtCtrl[11]; -char buffer[4096]; -char* msgInfo = buffer; + Qus_EC_t ec; + int recProc = 0; + int restrictedVerb = 0; + char langID[10]; + char msgid[8]; + char msgfile[11]; + char msgflib[11]; + char msgf[21]; + char subValues[11]; + char fmtCtrl[11]; + char buffer[4096]; + char* msgInfo = buffer; typedef _Packed struct tag_Statement_Info - { +{ char Message_File_Name[10]; char Message_File_Library_Name[10]; int Number_of_Statements_Processed; Qsq_Statement_I_t Statement; char msgdata[1920]; - } sqlStmtInfo_t; +} sqlStmtInfo_t; -typedef _Packed struct tagsqlOptions { +typedef _Packed struct tagsqlOptions +{ int key; int length; char data[10]; - } sqlOption_t; +} sqlOption_t; struct { int keys; diff --git a/changelog.md b/changelog.md index a1e41f9..d3e47ce 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # Changelog +## 0.0.10 - 15-SEPT-2025 +- We now close the SQL Cursor before freeing the statement handle. We also now use AUTOCOMMIT for users who insist on using the DB2JSON.PGM to run non-query statements, such as INSERT/UPDATE/DELETE/MERGE. Also the SQL CODE is returned with the SQLSTATE when an error is detected. + ## 0.0.9 - 12-SEPT-2025 - - The SQL Statement History in the Db2 Query HTML App now avoids adding duplicate entries. Once a statement has been archived in the history log, running that statement in the future avoids adding it a second time. Previously it would avoid adding duplicate statements only when they were run consecutively. Now if the SQL statement being run has been previously logged, it is not logged. - A new "Remove Duplicates" button has been added to the SQL Statement History dialog. Note this takes effect immediately. diff --git a/css/db2json.css b/css/db2query.css similarity index 89% rename from css/db2json.css rename to css/db2query.css index 97f34ca..3f46ebf 100644 --- a/css/db2json.css +++ b/css/db2query.css @@ -47,6 +47,9 @@ body { box-shadow: 0 8px 16px -4px #bbb; vertical-align: middle; background-clip: padding-box; + white-space: normal; /* headers can wrap (you already inject
) */ + word-break: normal; + overflow-wrap: normal; } /* Add minimal top/bottom breathing room for header cells without using padding, to avoid sticky bleed */ @@ -88,6 +91,10 @@ td { font-size: var(--table-font-size, 1em); font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; white-space: pre-wrap; + white-space: nowrap; /* prevent 2-line rows */ + word-break: normal; + overflow: hidden; /* clip inside the cell */ + text-overflow: ellipsis; /* … when clipped */ } .scroll-table th.right { @@ -696,7 +703,41 @@ td { gap: 8px; } -/* Optional: allow wrapping on narrow screens */ -@media (max-width: 520px) { - .modal .modal-actions { flex-wrap: wrap; } +/* Simple-DataTables inside our scroll wrapper: our wrapper scrolls, not the plugin */ +#results .scroll-table-wrapper .dataTable-container { + overflow: visible !important; + max-height: none !important; + padding-bottom: 44px; /* avoid last rows hiding under sticky pager */ +} + +/* Keep pager/top visible inside the wrapper */ +#results .scroll-table-wrapper .dataTable-bottom { + position: sticky; + bottom: 0; + background: #fff; + border-top: 1px solid #ddd; + z-index: 3; +} +#results .scroll-table-wrapper .dataTable-top { + position: sticky; + top: 0; + background: #fff; + z-index: 3; +} + +/* If Simple-DataTables forces wrapping, neutralize it here */ +#results .scroll-table-wrapper .dataTable-table td { + white-space: nowrap; + word-break: normal; + overflow: hidden; + text-overflow: ellipsis; +} + +#results .scroll-table tbody td.wrap { + white-space: normal; + max-width: 250ch; + overflow-wrap: anywhere; + word-break: break-word; + overflow: visible; + text-overflow: clip; } diff --git a/db2json.html b/db2json.html deleted file mode 100644 index d7b364d..0000000 --- a/db2json.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - Bob Cozzi's Db2JSON Test Composer using SQL - - - - - - - - -

DB2JSON Query

-
-
- - - -
- -
-
- - - -
- -
-
- - - -
-
-
-
- - - - -
-
-
- -
-
-
-
-
-
- - - - - -
- - - - - \ No newline at end of file diff --git a/db2json.md b/db2query.md similarity index 60% rename from db2json.md rename to db2query.md index 209600a..1b77dca 100644 --- a/db2json.md +++ b/db2query.md @@ -1,13 +1,14 @@ -# db2json Query User Guide +# db2json Query (Db2QUERY) Web App User Guide ## Overview -The **db2json Query** HTML page provides a simple interface for entering, editing, and running SQL statements against IBM i databases. It features syntax checking, error highlighting, statement history, and a toolbar for file and clipboard operations. The results of SELECT, CTE, or VALUES (commonly referred to as “query statements”) are displayed in a scrolling table. That result set table has a copy button that copies the entire contents to the clipboard. This is a demonstration of how to use the DB2JSON C++ program that runs on your IBM i server to convert SQL query statements into JSON. While it is just an example, it is a very robust application. In fact, you might consider it to be a lightweight alternative to the `RUN SQL Scripts` tool found in IBM i Access Client Solutions (ACS). +The **DB2Query** Web app is a sample web app that lets you experience the DB2JSON program's capabilities. In fact, you might consider it to be an HTML-based, lightweight `RUN SQL Scripts`. +It is an interface for entering, editing, and running SQL statements against IBM i databases. It features syntax checking, error highlighting, statement history, and a toolbar for file and clipboard operations. The results of SELECT, CTE, or VALUES (commonly referred to as “query statements”) are displayed in a scrolling table. That result set table has a copy button that copies the entire contents to the clipboard. This is a demonstration of how to use the DB2JSON C++ program that runs on your IBM i server to convert SQL query statements into JSON. While it is just an example, it is a very robust application. ### Requirements -- An IBM i server running V7R2 or later. The server-side code (DB2JSON.CPP) will compile on earlier releases, but it was not tested. +- An IBM i server running V7R2 or later. +- The open source DB2JSON program compiled on your IBM i server. This is used as both the JSON generator and the CGI processor. - The IBM HTTP Server Powered by Apache with the configurations outlined in our [README file](https://github.com/bobcozzi/db2json#readme) -- A compiled version of the DB2JSON.CPP program object (*PGM) on your IBM i server. This is both the JSON generator and the CGI processor. ## Features @@ -15,23 +16,26 @@ The **db2json Query** HTML page provides a simple interface for entering, editin - Enter or paste your SQL statements in the main textarea. - Multi-line statements are supported. +- Separate individual statements with a semicolon. - Syntax errors are automatically detected and highlighted. ### Syntax Checking & Error Messages -- When you submit SQL, the page checks for syntax errors. +- When you Run SQL, the page checks for syntax errors. - If an error is found: - - The error message appears above the results area. - - The problematic token or character is highlighted in the input. + - The error message appears below the SQL statement input area. + - If detected, the problematic token or character is highlighted in the input area. - SQLSTATE and detailed error info are shown for non-syntax errors. ### Statement History -Your SQL statement history is stored locally in your browser and can be used to retrieve SQL statements that were previously run. We set an arbitrary limit of 512 SQL statements. You can change that in the code if you prefer. To access the history, use the history dropdown and select from your previously run statements. You can also edit entries and clear them at any time. +Your SQL statement history is stored locally in your browser and can be used to retrieve SQL statements that were previously run. We set an arbitrary limit of 512 SQL statements. You can change that in the code if you prefer. To access the history, use the history dropdown and select from your previously run statements. You can also edit statement history and clear the history at any time. - The history dropdown lets you quickly recall previous SQL statements. - Use the **Edit History** button to manage or clean up your saved statements. - Use the **Clear History** button to remove all saved SQL statements from your browser (a confirmation dialog prompts you to confirm). +- De-Dup SQL Statement History. + - Only unique statements are saved to the SQL statement history. That way if you run the same statement multiples times, it is only logged to your history file, once. However in some rare cases duplicates may occur. When that happens, if you prefer to remove those duplicates, use the "Remove Duplicates" button on the Edit History dialog. This takes effect immediately and is non-reverseable. But as mentioned, it is highly unlikely that this will ever be needed. ### Open/Save SQL Statements from Local File @@ -59,6 +63,10 @@ The toolbar near the top‑right of the SQL statement input provides Open, Save, - A brief checkmark confirmation appears on success; if copying is blocked, a toast notification explains the issue and suggests retrying. - Very large results may take a moment to copy depending on your browser and device. +### ResultSet Stats + +- When a resultSet table is generated, the number of rows returned by the query is shown along with the number of columns returned. An example might look like `Rows: 5382 Columns: 38`. This status bar appears immediately to the left of the Copy ResultSet table button. + ### Limitations - Save is disabled unless you open a file using the browser’s file picker in a secure context (HTTPS or http://localhost in Chrome/Edge). diff --git a/index.html b/index.html index d7b364d..9d58428 100644 --- a/index.html +++ b/index.html @@ -4,16 +4,19 @@ - Bob Cozzi's Db2JSON Test Composer using SQL + Db2 Query: Bob Cozzi's Db2JSON Example HTML App + - - - - + + + + + + -

DB2JSON Query

+

Db2 Query for DB2JSON

@@ -128,16 +131,16 @@

Edit SQL History

const ver = String(Date.now()); const withV = (path) => ver ? `${path}?v=${ver}` : path; - const mainCSS = document.getElementById('db2JSON_maincss'); - const mainjs = document.getElementById('db2JSON_mainjs'); - const fmtJS = document.getElementById('db2JSON_sqlfmtjs'); - const histJS = document.getElementById('db2JSON_sqlhistjs'); - console.log(`mainjs=${withV('js/db2json.js')}`); + const mainCSS = document.getElementById('db2query_maincss'); + const mainjs = document.getElementById('db2query_mainjs'); + const fmtJS = document.getElementById('db2query_sqlfmtjs'); + const histJS = document.getElementById('db2query_sqlhistjs'); + console.log(`mainjs=${withV('js/db2query.js')}`); - if (mainCSS) mainCSS.href = withV('css/db2json.css'); - if (fmtJS) fmtJS.src = withV('js/db2json_sqlFmt.js'); - if (histJS) histJS.src = withV('js/db2json_sqlHist.js'); - if (mainjs) mainjs.src = withV('js/db2json.js'); + if (mainCSS) mainCSS.href = withV('css/db2query.css'); + if (fmtJS) fmtJS.src = withV('js/db2query_sqlFmt.js'); + if (histJS) histJS.src = withV('js/db2query_sqlHist.js'); + if (mainjs) mainjs.src = withV('js/db2query.js'); diff --git a/js/db2json.js b/js/db2query.js similarity index 84% rename from js/db2json.js rename to js/db2query.js index 77c5241..a8cc807 100644 --- a/js/db2json.js +++ b/js/db2query.js @@ -455,42 +455,55 @@ function renderTable(json, resultsDiv) { const isRightAlignType = t => /^(DECIMAL|DEC|NUMERIC|DECFLOAT|ZONED|INT|INTEGER|SMALLINT|BIGINT|TINYINT|FLOAT|REAL|DOUBLE|DATE|TIME|TIMESTAMP)$/i.test(t); - if (colMeta) { - for (const col of colMeta) { - let hdr = col.colhdr || col.name; - if (hdr.length > 20) { - let lines = []; - for (let i = 0; i < 60 && i < hdr.length; i += 20) { - let part = hdr.substr(i, 20).trim(); - if (part) lines.push(part); - } - hdr = lines.join('
'); + // Determine which columns to wrap (LOBs always; long char types > 250) + const wrapCols = new Set(); + if (Array.isArray(colMeta)) { + colMeta.forEach((c, i) => { + const len = Number(c.length) || 0; + const t = String(c.type || ''); + const isLob = /CLOB|DBCLOB|BLOB|XML/i.test(t); + const isLongChar = len > 250 && /(CHAR|VARCHAR|GRAPHIC|VARGRAPHIC)/i.test(t); + if (isLob || isLongChar) wrapCols.add(i); + }); + } + + if (Array.isArray(colMeta)) { + colMeta.forEach((col, idx) => { + let hdr = col.colhdr || col.name; + if (hdr.length > 20) { + let lines = []; + for (let i = 0; i < 60 && i < hdr.length; i += 20) { + let part = hdr.substr(i, 20).trim(); + if (part) lines.push(part); } - let thTitle = buildColTitle(col); - let thClass = isRightAlignType(col.type) ? ' class="right"' : ''; - html += `${hdr}`; - } + hdr = lines.join('
'); + } + let thTitle = buildColTitle(col); + const thClasses = []; + if (isRightAlignType(col.type)) thClasses.push('right'); + if (wrapCols.has(idx)) thClasses.push('wrap'); + const thClassAttr = thClasses.length ? ` class="${thClasses.join(' ')}"` : ''; + html += `${hdr}`; + }); } else { - for (const col of columns) { - html += `${col}`; - } + for (const col of columns) { + html += `${col}`; + } } html += ''; for (const row of rows) { html += ''; for (let colIdx = 0; colIdx < columns.length; ++colIdx) { - const colName = columns[colIdx]; - // Always use colMeta (from attr) for type, as in thead - let tdClass = ''; - if (colMeta && colMeta[colIdx] && isRightAlignType(colMeta[colIdx].type)) { - tdClass = ' class="right"'; - } - // Extract the cell data for this row/column - const cellData = row[colName] ?? ''; - // Escape the cell data for HTML safety - const safeCellData = escapeHTML(cellData); - html += `${safeCellData}`; + const colName = columns[colIdx]; + const classes = []; + if (colMeta && colMeta[colIdx] && isRightAlignType(colMeta[colIdx].type)) classes.push('right'); + if (wrapCols.has(colIdx)) classes.push('wrap'); + const tdClassAttr = classes.length ? ` class="${classes.join(' ')}"` : ''; + const cellData = row[colName] ?? ''; + const safeCellData = escapeHTML(cellData); + // Add title for full value on hover + html += `${safeCellData}`; } html += ''; } @@ -498,28 +511,43 @@ function renderTable(json, resultsDiv) { html += '
'; // close .scroll-table-wrapper - // Now inject the table + // Inject the table resultsDiv.innerHTML = html; + + // Column widths before DataTables modifies the DOM + try { syncColWidths(resultsDiv); } catch {} + try { + // Destroy prior instance if any + if (resultsDiv._dt) { resultsDiv._dt.destroy(); resultsDiv._dt = null; } + + const tableEl = resultsDiv.querySelector('table'); + if (tableEl && window.simpleDatatables?.DataTable) { + resultsDiv._dt = new simpleDatatables.DataTable(tableEl, { + searchable: true + // perPage: 27 // remove fixed size; we fit dynamically + }); + + // Fit per-page after DT builds its DOM + requestAnimationFrame(() => fitDataTablePerPage(resultsDiv)); + + // Re-fit on DT updates + const dt = resultsDiv._dt; + if (dt?.on) { + dt.on('datatable.init', () => requestAnimationFrame(() => fitDataTablePerPage(resultsDiv))); + dt.on('datatable.sort', () => requestAnimationFrame(() => fitDataTablePerPage(resultsDiv))); + dt.on('datatable.page', () => requestAnimationFrame(() => fitDataTablePerPage(resultsDiv))); + dt.on('datatable.search', () => requestAnimationFrame(() => fitDataTablePerPage(resultsDiv))); + } + } + setResultsMeta({ rowsCount: rows.length, colsCount: columns.length, tblname, libname }); document.getElementById('copyTableBtn')?.classList.remove('is-hidden'); document.getElementById('resultsMeta')?.classList.remove('is-hidden'); } catch {} - // Debug: Log the generated HTML to check for - console.log('Generated table HTML:', html); - // Wait for DOM update before measuring and setting col widths - window.requestAnimationFrame(() => { - // adjustTableMaxHeight(resultsDiv); // REMOVE: CSS controls max-height now - syncColWidths(resultsDiv); - }); - - // Remove any previous resize event to avoid duplicates, then set a fresh one - if (window._db2jsonResizeHandler) { - window.removeEventListener('resize', window._db2jsonResizeHandler); - } - window._db2jsonResizeHandler = null; // no longer needed - // window.addEventListener('resize', window._db2jsonResizeHandler); // REMOVE + // Debug (optional) + // console.debug('Generated table HTML:', html); } function copyTableToClipboard(resultsDiv) { @@ -584,49 +612,57 @@ function adjustTableMaxHeight(resultsDiv) { function syncColWidths(resultsDiv) { - const table = resultsDiv.querySelector('.scroll-table'); - if (table) { - const headerCells = table.querySelectorAll('thead th'); - const maxColWidthCh = 150; - let colWidths = []; - - headerCells.forEach((th, colIdx) => { - const isNumeric = th.classList.contains('right'); - // Calculate header width based on the longest line after splitting on
- let headerLines = (th.innerHTML || '').split(/]+>/g, '').trim(); - if (text.length > headerLen) headerLen = text.length; - } - let maxWidthCh = headerLen; - - table.querySelectorAll('tbody tr').forEach(tr => { - const td = tr.children[colIdx]; - if (td) { - const cellLen = (td.textContent || '').trim().length; - if (cellLen > maxWidthCh) maxWidthCh = cellLen; - } - }); + const table = resultsDiv?.querySelector?.('.scroll-table'); + if (!table) return; + const thead = table.tHead; + const headerRow = thead?.rows?.[0]; + if (!headerRow) return; + const headerCells = headerRow.cells; + const maxColWidthCh = 150; + let colWidths = []; + Array.from(headerCells).forEach((th, colIdx) => { + const isNumeric = th.classList.contains('right'); + // Calculate header width based on the longest line after splitting on
+ let headerLines = (th.innerHTML || '').split(/]+>/g, '').trim(); + if (text.length > headerLen) headerLen = text.length; + } + let maxWidthCh = headerLen; + + const bodyRows = table.tBodies?.[0]?.rows || []; + Array.from(bodyRows).forEach(tr => { + const td = tr.cells?.[colIdx]; + if (!td) return; + const cellLen = (td.textContent || '').trim().length; + if (cellLen > maxWidthCh) maxWidthCh = cellLen; + }); - if (maxWidthCh > maxColWidthCh) maxWidthCh = maxColWidthCh; + if (maxWidthCh > maxColWidthCh) maxWidthCh = maxColWidthCh; - // If numeric, don’t let the column balloon too wide - if (isNumeric) { - if (maxWidthCh > 20) maxWidthCh = 20; - maxWidthCh += 2; // add a little extra space for right alignment - } - colWidths.push(maxWidthCh); - }); + // If numeric, don’t let the column balloon too wide + if (isNumeric) { + if (maxWidthCh > 20) maxWidthCh = 20; + maxWidthCh += 2; // add a little extra space for right alignment + } else { + maxWidthCh += 2; // small cushion for proportional fonts so 20 chars don’t wrap early + } + colWidths.push(maxWidthCh); + }); - let colgroupHtml = ''; - for (let w of colWidths) { - // colgroupHtml += ``; - colgroupHtml += ``; - } - table.querySelector('colgroup').innerHTML = colgroupHtml; + let colgroupHtml = ''; + for (let w of colWidths) { + // colgroupHtml += ``; + colgroupHtml += ``; } + let colgroup = table.querySelector('colgroup'); + if (!colgroup) { + colgroup = document.createElement('colgroup'); + table.insertBefore(colgroup, table.firstChild); + } + colgroup.innerHTML = colgroupHtml; } function copySqlInput() { @@ -1099,7 +1135,7 @@ function sizeResultsViewport() { } (function initResultsViewportSizing() { - const run = () => { try { sizeResultsViewport(); } catch {} }; + const run = () => { try { sizeResultsViewport(); fitDataTablePerPage(); } catch {} }; // Recompute when window/viewport changes window.addEventListener('resize', run); @@ -1138,3 +1174,36 @@ function setResultsMeta({ rowsCount, colsCount, tblname, libname }) { meta.innerHTML = `${typeof escapeHTML === 'function' ? escapeHTML(txt) : txt}`; meta.title = txt; } + +function fitDataTablePerPage(resultsDiv = document.getElementById('results')) { + try { + const root = resultsDiv; + const dt = root?._dt; + if (!root || !dt) return; + + const wrap = root.querySelector('.scroll-table-wrapper'); + const firstRow = root.querySelector('tbody tr'); + const thead = root.querySelector('thead'); + const topBar = wrap?.querySelector('.dataTable-top'); + const bottomBar = wrap?.querySelector('.dataTable-bottom'); + if (!wrap || !firstRow) return; + + const wrapH = wrap.clientHeight || 0; + const headH = thead ? thead.offsetHeight : 0; + const topH = topBar ? topBar.offsetHeight : 0; + const bottomH = bottomBar ? bottomBar.offsetHeight : 0; + const padding = 12; + const avail = Math.max(0, wrapH - headH - topH - bottomH - padding); + const rowH = Math.max(20, firstRow.getBoundingClientRect().height || 24); + + let perPage = Math.floor(avail / rowH); + perPage = Math.max(8, Math.min(perPage, 200)); // sane bounds + if (!Number.isFinite(perPage)) return; + + if (dt.options?.perPage !== perPage) { + if (typeof dt.update === 'function') dt.update({ perPage }); + else { dt.options.perPage = perPage; if (typeof dt.refresh === 'function') dt.refresh(); } + if (typeof dt.setPage === 'function') dt.setPage(1); + } + } catch {} +} diff --git a/js/db2json_sqlFmt.js b/js/db2query_sqlFmt.js similarity index 96% rename from js/db2json_sqlFmt.js rename to js/db2query_sqlFmt.js index 2a6f350..2e3e26d 100644 --- a/js/db2json_sqlFmt.js +++ b/js/db2query_sqlFmt.js @@ -1,4 +1,4 @@ -// sqlFmt.js + /** * Checks if the match is a real SQL keyword (not a host variable like :SELECT or &WHERE). */ diff --git a/js/db2json_sqlHist.js b/js/db2query_sqlHist.js similarity index 100% rename from js/db2json_sqlHist.js rename to js/db2query_sqlHist.js From 33a3788b87157f1e67451a53a82c0ba6c484e871 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:04:31 +0000 Subject: [PATCH 2/2] Initial plan