@stacksjs/database 0.75.155 → 0.75.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{env as envVars}from"@stacksjs/env";import{log}from"@stacksjs/logging";import{parseCaptureBindings,parseSensitiveColumn,persistQueryLogValues,queryLogError}from"./query-log-bindings";import{normalizeQuery,parseQuery}from"./query-parser";import{db,getDatabaseDialect}from"./utils";import{isLibsqlDriver}from"./dialect";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));for(const records of recordsByShape.values())if(!await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1)))for(const record of records)await queryLogContext.run(!0,()=>storeQueryLogs([record]));for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,parameters}=extractQueryInfo(event);if(query)try{trackQuery(query,durationMs,config.database?.default||"unknown")}catch{}if(!config.database?.queryLogging?.enabled)return;const excludedQueries=config.database?.queryLogging?.excludedQueries;if(!query.trim()||isExcludedQuery(query,Array.isArray(excludedQueries)?excludedQueries:[]))return;const status=determineQueryStatus(durationMs,error),logRecord=await createQueryLogRecord(query,durationMs,status,error,parameters);if(config.database?.queryLogging?.analysis?.enabled&&(status==="slow"||config.database.queryLogging.analysis.analyzeAll))await enhanceWithQueryAnalysis(logRecord);await enqueueQueryLog(logRecord);if(status!=="completed")log[status==="failed"?"error":"warn"](`Query ${status}:`,{query:logRecord.query,duration:logRecord.duration,connection:logRecord.connection,...error&&{error}})}catch(err){log.error("Failed to log query:",err)}}export function isExcludedQuery(query,excludedQueries){const normalizedQuery=query.toLowerCase();return excludedQueries.some((pattern)=>{const normalizedPattern=pattern.trim().toLowerCase();return normalizedPattern.length>0&&normalizedQuery.includes(normalizedPattern)})}function queryText(sql){if(typeof sql==="string")return usableQueryText(sql);if(sql&&typeof sql==="object"&&typeof sql.then==="function")return"";if(sql&&typeof sql.toString==="function")return usableQueryText(String(sql));return""}function usableQueryText(text){return text.startsWith("[object ")?"":text}function extractQueryInfo(event){const query=queryText(event.query?.sql),durationMs=event.queryDurationMillis||0,error=event.error,parameters=event.query?.parameters;return{query,durationMs,error,parameters}}function determineQueryStatus(durationMs,error){const slowThreshold=config.database?.queryLogging?.slowThreshold||100;if(error)return"failed";if(durationMs>slowThreshold)return"slow";return"completed"}const warnedCaptureBindings=new Set;function capturesBindingValues(){const setting=config.database?.queryLogging?.captureBindings??envVars.DB_QUERY_LOGGING_CAPTURE_BINDINGS,capture=parseCaptureBindings(setting);if(capture===null){const shown=String(setting);if(!warnedCaptureBindings.has(shown)){warnedCaptureBindings.add(shown);console.warn(`[database] queryLogging.captureBindings (DB_QUERY_LOGGING_CAPTURE_BINDINGS) is "${shown}", which is not true, false, 1, 0, yes, no, on or off; query logs keep only the type of each binding.`)}return!1}if(capture!==void 0)return capture;const appEnv=config.app?.env;return appEnv!=="production"&&appEnv!=="prod"}const warnedSensitiveColumns=new Set;function sensitiveColumns(){const setting=config.database?.queryLogging?.sensitiveColumns;if(setting===void 0||setting===null)return[];const entries=Array.isArray(setting)?setting:[setting];for(const entry of entries){const shown=String(entry);if(parseSensitiveColumn(entry)===void 0&&!warnedSensitiveColumns.has(shown)){warnedSensitiveColumns.add(shown);console.warn(`[database] queryLogging.sensitiveColumns has "${shown}", which is not a column or table.column; it marks nothing as sensitive.`)}}return entries}async function createQueryLogRecord(query,durationMs,status,error,parameters){const connection=config.database.default||"unknown",normalizedQuery=normalizeQuery(query)||query,traceInfo=status==="completed"&&!config.database?.queryLogging?.captureAllTraces?void 0:extractTraceInfo(),errorText=error?String(error):void 0,persisted=parameters?persistQueryLogValues(query,parameters,errorText,{captureValues:capturesBindingValues(),sensitiveColumns:sensitiveColumns()}):{bindings:void 0,error:errorText===void 0?void 0:queryLogError(errorText,[],[])};return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:persisted.error,executed_at:sqlDateTime(),bindings:persisted.bindings,trace:traceInfo?.trace,...traceInfo?.caller??{},memory_usage:memoryUsage().heapUsed/1024/1024}}const SECRET_PATTERNS=[/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bAKIA[0-9A-Z]{16}/g,/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g];function sanitizeStackTrace(stack){let out=stack;for(const pattern of SECRET_PATTERNS)out=out.replace(pattern,"<redacted>");return out}let lastTraceInfo;function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"";if(lastTraceInfo?.trace===stack)return lastTraceInfo;const callerLine=stack.split(`
2
- `).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","turso","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?isLibsqlDriver(rawDriver)?"sqlite":rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords,libsql=isLibsqlDriver(config.database?.default),batch=libsql?db.sql?.batch:void 0;if(values.length>1&&getDatabaseDialect()==="sqlite"&&batch){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));await batch([{sql:statement,args:bindings}])}else if(values.length>1&&getDatabaseDialect()==="sqlite"&&!libsql){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));db.unsafe("SAVEPOINT stacks_query_log_batch").executeSync();try{db.unsafe(statement,bindings).executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync()}catch(error){db.unsafe("ROLLBACK TO SAVEPOINT stacks_query_log_batch").executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync();throw error}}else await db.insertInto("query_logs").values(values).execute();return!0}catch(error){if(reportFailure){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}return!1}}
1
+ import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{env as envVars}from"@stacksjs/env";import{log}from"@stacksjs/logging";import{parseCaptureBindings,parseSensitiveColumn,persistQueryLogValues,queryLogError}from"./query-log-bindings";import{normalizeQuery,parseQuery}from"./query-parser";import{db,getDatabaseDialect}from"./utils";import{isLibsqlDriver}from"./dialect";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1,queryLogTableMissing=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));if(queryLogTableMissing)queryLogTableMissing=!await queryLogContext.run(!0,queryLogTableExists);for(const records of recordsByShape.values()){if(queryLogTableMissing)break;if(await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1))==="failed"){for(const record of records)if(await queryLogContext.run(!0,()=>storeQueryLogs([record]))==="missing")break}}for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,parameters}=extractQueryInfo(event);if(query)try{trackQuery(query,durationMs,config.database?.default||"unknown")}catch{}if(!config.database?.queryLogging?.enabled)return;const excludedQueries=config.database?.queryLogging?.excludedQueries;if(!query.trim()||isExcludedQuery(query,Array.isArray(excludedQueries)?excludedQueries:[]))return;const status=determineQueryStatus(durationMs,error),logRecord=await createQueryLogRecord(query,durationMs,status,error,parameters);if(config.database?.queryLogging?.analysis?.enabled&&(status==="slow"||config.database.queryLogging.analysis.analyzeAll))await enhanceWithQueryAnalysis(logRecord);await enqueueQueryLog(logRecord);if(status!=="completed")log[status==="failed"?"error":"warn"](`Query ${status}:`,{query:logRecord.query,duration:logRecord.duration,connection:logRecord.connection,...error&&{error}})}catch(err){log.error("Failed to log query:",err)}}export function isExcludedQuery(query,excludedQueries){const normalizedQuery=query.toLowerCase();return excludedQueries.some((pattern)=>{const normalizedPattern=pattern.trim().toLowerCase();return normalizedPattern.length>0&&normalizedQuery.includes(normalizedPattern)})}function queryText(sql){if(typeof sql==="string")return usableQueryText(sql);if(sql&&typeof sql==="object"&&typeof sql.then==="function")return"";if(sql&&typeof sql.toString==="function")return usableQueryText(String(sql));return""}function usableQueryText(text){return text.startsWith("[object ")?"":text}function extractQueryInfo(event){const query=queryText(event.query?.sql),durationMs=event.queryDurationMillis||0,error=event.error,parameters=event.query?.parameters;return{query,durationMs,error,parameters}}function determineQueryStatus(durationMs,error){const slowThreshold=config.database?.queryLogging?.slowThreshold||100;if(error)return"failed";if(durationMs>slowThreshold)return"slow";return"completed"}const warnedCaptureBindings=new Set;function capturesBindingValues(){const setting=config.database?.queryLogging?.captureBindings??envVars.DB_QUERY_LOGGING_CAPTURE_BINDINGS,capture=parseCaptureBindings(setting);if(capture===null){const shown=String(setting);if(!warnedCaptureBindings.has(shown)){warnedCaptureBindings.add(shown);console.warn(`[database] queryLogging.captureBindings (DB_QUERY_LOGGING_CAPTURE_BINDINGS) is "${shown}", which is not true, false, 1, 0, yes, no, on or off; query logs keep only the type of each binding.`)}return!1}if(capture!==void 0)return capture;const appEnv=config.app?.env;return appEnv!=="production"&&appEnv!=="prod"}const warnedSensitiveColumns=new Set;function sensitiveColumns(){const setting=config.database?.queryLogging?.sensitiveColumns;if(setting===void 0||setting===null)return[];const entries=Array.isArray(setting)?setting:[setting];for(const entry of entries){const shown=String(entry);if(parseSensitiveColumn(entry)===void 0&&!warnedSensitiveColumns.has(shown)){warnedSensitiveColumns.add(shown);console.warn(`[database] queryLogging.sensitiveColumns has "${shown}", which is not a column or table.column; it marks nothing as sensitive.`)}}return entries}async function createQueryLogRecord(query,durationMs,status,error,parameters){const connection=config.database.default||"unknown",normalizedQuery=normalizeQuery(query)||query,traceInfo=status==="completed"&&!config.database?.queryLogging?.captureAllTraces?void 0:extractTraceInfo(),errorText=error?String(error):void 0,persisted=parameters?persistQueryLogValues(query,parameters,errorText,{captureValues:capturesBindingValues(),sensitiveColumns:sensitiveColumns()}):{bindings:void 0,error:errorText===void 0?void 0:queryLogError(errorText,[],[])};return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:persisted.error,executed_at:sqlDateTime(),bindings:persisted.bindings,trace:traceInfo?.trace,...traceInfo?.caller??{},memory_usage:memoryUsage().heapUsed/1024/1024}}const SECRET_PATTERNS=[/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bAKIA[0-9A-Z]{16}/g,/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g];function sanitizeStackTrace(stack){let out=stack;for(const pattern of SECRET_PATTERNS)out=out.replace(pattern,"<redacted>");return out}let lastTraceInfo;function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"";if(lastTraceInfo?.trace===stack)return lastTraceInfo;const callerLine=stack.split(`
2
+ `).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","turso","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?isLibsqlDriver(rawDriver)?"sqlite":rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function queryLogTableExists(){try{const dialect=getDatabaseDialect(),sql=dialect==="postgres"?"SELECT 1 AS present WHERE to_regclass('query_logs') IS NOT NULL":dialect==="mysql"?"SELECT 1 AS present FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'query_logs'":"SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'query_logs'";return(await db.unsafe(sql).execute()).length>0}catch{return!1}}async function storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords,libsql=isLibsqlDriver(config.database?.default),batch=libsql?db.sql?.batch:void 0;if(values.length>1&&getDatabaseDialect()==="sqlite"&&batch){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));await batch([{sql:statement,args:bindings}])}else if(values.length>1&&getDatabaseDialect()==="sqlite"&&!libsql){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));db.unsafe("SAVEPOINT stacks_query_log_batch").executeSync();try{db.unsafe(statement,bindings).executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync()}catch(error){db.unsafe("ROLLBACK TO SAVEPOINT stacks_query_log_batch").executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync();throw error}}else await db.insertInto("query_logs").values(values).execute();return"stored"}catch(error){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message)){if(!queryLogTableMissing)log.debug("Query logging will start after the query_logs table is migrated.");queryLogTableMissing=!0;return"missing"}if(reportFailure)log.error("Failed to store query log:",error);return"failed"}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.155",
5
+ "version": "0.75.157",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -66,26 +66,26 @@
66
66
  "prepublishOnly": "bun run build"
67
67
  },
68
68
  "dependencies": {
69
- "@stacksjs/config": "0.75.155",
70
- "@stacksjs/env": "0.75.155",
71
- "@stacksjs/error-handling": "0.75.155",
72
- "@stacksjs/faker": "0.75.155",
73
- "@stacksjs/features": "0.75.155",
74
- "@stacksjs/logging": "0.75.155",
75
- "@stacksjs/model-meta": "0.75.155",
76
- "@stacksjs/path": "0.75.155",
77
- "@stacksjs/query-builder": "0.75.155",
78
- "@stacksjs/security": "0.75.155",
79
- "@stacksjs/storage": "0.75.155",
80
- "@stacksjs/strings": "0.75.155",
69
+ "@stacksjs/config": "0.75.157",
70
+ "@stacksjs/env": "0.75.157",
71
+ "@stacksjs/error-handling": "0.75.157",
72
+ "@stacksjs/faker": "0.75.157",
73
+ "@stacksjs/features": "0.75.157",
74
+ "@stacksjs/logging": "0.75.157",
75
+ "@stacksjs/model-meta": "0.75.157",
76
+ "@stacksjs/path": "0.75.157",
77
+ "@stacksjs/query-builder": "0.75.157",
78
+ "@stacksjs/security": "0.75.157",
79
+ "@stacksjs/storage": "0.75.157",
80
+ "@stacksjs/strings": "0.75.157",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
82
  "bun-query-builder": "^0.3.8",
83
83
  "dynamodb-tooling": "^0.3.6"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.155",
87
- "@stacksjs/router": "0.75.155",
88
- "@stacksjs/utils": "0.75.155",
86
+ "@stacksjs/cli": "0.75.157",
87
+ "@stacksjs/router": "0.75.157",
88
+ "@stacksjs/utils": "0.75.157",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }