diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 30c06004..833e76bc 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -37,8 +37,8 @@ jobs: type=ref,event=pr type=sha,prefix=dev-,enable={{is_not_default_branch}} type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=3.29.3,enable={{is_default_branch}} - type=raw,value=v3.29.3,enable={{is_default_branch}} + type=raw,value=3.29.4,enable={{is_default_branch}} + type=raw,value=v3.29.4,enable={{is_default_branch}} - name: Build and push Docker image uses: docker/build-push-action@v6 diff --git a/AGENT.md b/AGENT.md index 46deb5c6..d101e65c 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,4 +1,4 @@ -# Couchbase Slow Query Analysis Tool v3.29.3 +# Couchbase Slow Query Analysis Tool v3.29.4 ## Setup and Installation @@ -33,7 +33,7 @@ npm test See [PLAYWRIGHT_TESTING.md](./PLAYWRIGHT_TESTING.md) and [settings/TESTING_WORKFLOW.md](./settings/TESTING_WORKFLOW.md) for details. ## Version Management -- **Current Version**: 3.29.3 (Last Updated: 2025-12-02) +- **Current Version**: 3.29.4 (Last Updated: 2026-06-30) ### Workflow Order for Updates When making changes, follow this order: diff --git a/Dockerfile b/Dockerfile index 2d282bc5..8312bba9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM nginx:alpine # Add version and metadata labels -LABEL version="3.29.3" +LABEL version="3.29.4" LABEL description="Couchbase Slow Query Analysis Tool" LABEL maintainer="Fujio Turner" diff --git a/README.md b/README.md index 8c881c21..e994586c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Couchbase Slow Query Analysis Tool v3.29.3 +# Couchbase Slow Query Analysis Tool v3.29.4 diff --git a/en/index.html b/en/index.html index 391f8246..9c34ef73 100644 --- a/en/index.html +++ b/en/index.html @@ -2,8 +2,8 @@ @@ -617,7 +617,7 @@
- v3.29.3 + v3.29.4
@@ -6183,9 +6183,10 @@

${operatorName}

// Group requests by normalized_statement const groups = {}; + let _grpSkippedNoStmt = 0; requests.forEach((request) => { const stmt = request.statement || request.preparedText; - if (!stmt) return; + if (!stmt) { _grpSkippedNoStmt++; return; } // Note: System query filtering is now handled globally in parseJSON() const normalized = normalizeStatement(stmt); if (!groups[normalized]) { @@ -6194,6 +6195,26 @@

${operatorName}

groups[normalized].push(request); }); + if (isDebugMode()) { + const groupKeys = Object.keys(groups); + Logger.debug('[Query Groups] Grouping diagnostics:', { + inputRequests: requests.length, + skippedNoStatement: _grpSkippedNoStmt, + uniqueNormalizedGroups: groupKeys.length + }); + // Show the largest groups so we can spot over-normalization collapsing distinct queries + const topGroups = groupKeys + .map(k => ({ count: groups[k].length, normalized: k.slice(0, 200) })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + Logger.debug('[Query Groups] Top normalized groups (count + normalized statement preview):', topGroups); + if (groupKeys.length <= 1 && requests.length > 1) { + Logger.warn('[Query Groups] All requests collapsed into a single normalized group — check raw statement variety below:'); + Logger.debug('[Query Groups] First 5 raw statements:', + requests.slice(0, 5).map(r => String(r.statement || r.preparedText || '').slice(0, 200))); + } + } + // Calculate stats for each group const groupData = Object.keys(groups) .map((key) => calculateGroupStats(key, groups[key])) @@ -16053,7 +16074,7 @@

${operatorName}

const data = JSON.parse(jsonInput); if (Array.isArray(data)) { requests = data.map((item) => ({ - ...item.completed_requests, + ...unwrapCompletedRequest(item), plan: item.plan ? typeof item.plan === "string" ? JSON.parse(item.plan) @@ -20358,8 +20379,43 @@

${operatorName}

} // Centralized request data processor with single-pass optimization + // Unwrap a single completed_requests row regardless of the SELECT * wrapper key. + // `SELECT *, meta().plan FROM system:completed_requests` wraps the row under + // `completed_requests`, but a keyspace alias (e.g. `... FROM system:completed_requests request`) + // wraps it under that alias instead (commonly `request`). This handles both, plus any + // single-alias wrapper, and falls back to the item itself when already unwrapped. + function unwrapCompletedRequest(item) { + if (!item || typeof item !== "object") return item; + + // Known wrapper keys first (fast path) + if (item.completed_requests && typeof item.completed_requests === "object") { + return item.completed_requests; + } + if (item.request && typeof item.request === "object") { + return item.request; + } + + // Generic fallback: a single non-plan wrapper whose value looks like a + // completed_requests row (has statement/requestId/elapsedTime). + const keys = Object.keys(item).filter((k) => k !== "plan"); + if (keys.length === 1) { + const inner = item[keys[0]]; + if ( + inner && + typeof inner === "object" && + (inner.statement !== undefined || + inner.requestId !== undefined || + inner.elapsedTime !== undefined) + ) { + return inner; + } + } + + return item; + } + function processRequestData(item) { - const request = item.completed_requests || item; + const request = unwrapCompletedRequest(item); // Parse and cache plan data immediately let plan = null; @@ -21098,7 +21154,7 @@

${operatorName}

for (let i = startIndex; i < endIndex; i++) { try { const item = processData[i]; - const request = item.completed_requests || item; + const request = unwrapCompletedRequest(item); // Combined filter check (Step 5 optimization) if (!shouldProcessRequest(request)) { @@ -21543,12 +21599,65 @@

${operatorName}

// Collect all unique collections from requests const collectionsSet = new Set(); + + // --- Diagnostics (only computed in debug mode) --- + const diag = isDebugMode() ? { + total: 0, + hasStatement: 0, + hasPreparedText: 0, + hasNeither: 0, + emptyAfterTrim: 0, + zeroCollections: 0, + noStatementKeySamples: [], + zeroCollectionSamples: [] + } : null; + requests.forEach(request => { const sql = request.statement || request.preparedText || ""; const collections = extractCollectionsFromSQL(sql); collections.forEach(collection => collectionsSet.add(collection)); + + if (diag) { + diag.total++; + if (request.statement) diag.hasStatement++; + if (request.preparedText) diag.hasPreparedText++; + if (!request.statement && !request.preparedText) { + diag.hasNeither++; + if (diag.noStatementKeySamples.length < 3) { + // Print keys inline (stringified) so the console shows them without expansion + diag.noStatementKeySamples.push(JSON.stringify(Object.keys(request).slice(0, 40))); + } + } + if (!String(sql).trim()) diag.emptyAfterTrim++; + if (String(sql).trim() && collections.length === 0) { + diag.zeroCollections++; + if (diag.zeroCollectionSamples.length < 5) { + diag.zeroCollectionSamples.push(String(sql).slice(0, 160)); + } + } + } }); + if (diag) { + Logger.debug('[populateCollectionFilter] Statement-field diagnostics:', { + totalRequests: diag.total, + hasStatement: diag.hasStatement, + hasPreparedTextOnly: diag.hasPreparedText, + hasNeitherStatementNorPreparedText: diag.hasNeither, + emptySqlAfterTrim: diag.emptyAfterTrim, + sqlPresentButZeroCollectionsExtracted: diag.zeroCollections, + uniqueCollectionsFound: collectionsSet.size + }); + if (diag.noStatementKeySamples.length) { + diag.noStatementKeySamples.forEach((keys, i) => { + Logger.debug(`[populateCollectionFilter] Missing-statement record #${i} top-level keys:`, keys); + }); + } + if (diag.zeroCollectionSamples.length) { + Logger.debug('[populateCollectionFilter] Sample SQL where extractCollectionsFromSQL() returned 0 collections (regex may not match FROM clause):', diag.zeroCollectionSamples); + } + } + // Sort collections alphabetically const sortedCollections = Array.from(collectionsSet).sort(); @@ -24772,8 +24881,8 @@

${operatorName}

// Hook removed - buildIndexQueryFlow will be called directly after data processing // Version management - const APP_VERSION = "3.29.3"; - const LAST_UPDATED = "2025-11-15"; + const APP_VERSION = "3.29.4"; + const LAST_UPDATED = "2026-06-30"; // Timezone management - initialize early to avoid undefined errors let detectedTimezone = "UTC"; // Timezone detected from data @@ -24790,7 +24899,7 @@

${operatorName}

// Get first requestTime that exists for (let i = 0; i < Math.min(processData.length, 10); i++) { const item = processData[i]; - const request = item.completed_requests || item; + const request = unwrapCompletedRequest(item); if (request.requestTime) { const requestTime = request.requestTime; // Check for timezone offset (e.g., "2025-01-15T10:30:00-05:00" or "2025-01-15T10:30:00+00:00") @@ -24920,16 +25029,12 @@

${operatorName}

let totalIndexReferences = 0; requests.forEach((request, index) => { - // Handle different JSON structures - let actualRequest = request; - if (request.completed_requests) { - // User's format: { completed_requests: {...}, plan: "..." } - actualRequest = { - ...request.completed_requests, - plan: request.plan, - }; - } else { - } + // Handle different JSON structures (raw wrapped rows vs already-processed rows). + // Wrapper key may be `completed_requests` or a keyspace alias like `request`. + const inner = unwrapCompletedRequest(request); + let actualRequest = inner === request + ? request + : { ...inner, plan: request.plan }; if (actualRequest.plan) { } @@ -25977,7 +26082,7 @@

${index.name} ${usedBadge}

const info = getVersionInfo(); console.log(` 🔍 Couchbase Query Analyzer v${info.version} -📅 Last Updated: 2025-10-20${info.lastUpdated} +📅 Last Updated: ${info.lastUpdated} 🎯 Purpose: Analyze Couchbase N1QL query performance from system:completed_requests 🚀 Features: diff --git a/index.html b/index.html index 982c8e4d..f92a4c41 100644 --- a/index.html +++ b/index.html @@ -3,13 +3,13 @@ - - - Couchbase Slow Query Analyzer & N1QL Query Optimizer v3.29.3 (Execution Plan Analyzer & Index Advisor) + + + Couchbase Slow Query Analyzer & N1QL Query Optimizer v3.29.4 (Execution Plan Analyzer & Index Advisor) - + @@ -394,7 +394,7 @@

FAQs

-
Couchbase Slow Query Analysis Tool v3.29.3
+
Couchbase Slow Query Analysis Tool v3.29.4