#Execution Contract
This document defines the behavioral contract for query execution in JustyBaseLite-netezza. It serves as the single source of truth for how queries flow through the system, what the result shapes look like, and how cancellation, retries, and streaming work.
#Shared lifecycle owner
@justybase/database-runtime/execution is the canonical orchestration layer.
It receives an ExecutionRequest plus an injected backend and owns execution
identity, statement attempts, timeout/cancellation, reconnect policy, event
sequence, resource cleanup and the terminal summary. Editor state, history,
authorization, credentials and transport messages remain in product adapters.
The ordered lifecycle is:
execution-started
→ statement-started
→ columns / rows / progress (zero or more)
→ statement-completed | statement-failed
→ retrying + statement-started ... (at most once when eligible)
→ execution-terminal
→ batch-completedEvery event has a sequence number increasing within one execution. Exactly one
execution-terminal and one compatibility batch-completed event are emitted,
both with the same immutable summary. A detached observer receives no later
callbacks but does not cancel execution or release its results. A failed or
cancelled statement may carry partial row/limit progress.
#Execution Modes
#1. Single Query (singleQueryExecutor.ts)
Entry points:
runQueryRaw(options)— returns structuredQueryResultrunQuery(...)— legacy JSON wrapper aroundrunQueryRawrunExplainQuery(...)— captures NOTICE messages for Netezza EXPLAIN outputrunQueryWithCatalog(...)— temporarily switches database catalog for cross-db queries
Flow:
runQueryRaw
→ resolveQueryVariables (${var..} substitution)
→ resolveConnectionName
→ clear stale document cancellation flag (document-bound execution only)
→ shared ExecutionOrchestrator
→ desktop backend acquires a persistent or transient connection
→ streamingManager.executeAndFetch
→ log to history
→ return QueryResultRetry logic: On isConnectionBrokenError (including Netezza's
Connection protocol is invalid; reconnect is required), the shared owner retries a
persistent execution
once only when both the original and fully expanded SQL contain one
allow-listed, call-free read-only statement. Writes, executable macros,
function/sequence expressions, multi-statement payloads, and ambiguous SQL fail
without replay and report that the database outcome may be unknown.
Cancellation note: single-query execution now clears stale StreamingManager cancellation state at the start of a new document-bound run so a previously cancelled execution does not poison the next single-statement run.
#2. Batch Sequential (batchQueryExecutor.ts → runQueriesSequentially)
Purpose: Execute multiple SQL statements one-at-a-time over a single connection.
Flow:
for each query:
→ shared statement-started → queryStartCallback
→ desktop backend → streamingManager.executeAndFetch
→ shared statement-completed/failed → queryEndCallback
→ logQueryToHistoryAsync
→ resultCallback (partial results to UI)Key behaviors:
- Cancellation is checked before each statement and after each execution
- On a broken connection, the shared orchestrator reconnects and repeats only the failed statement, and only for one proven safe read-only statement
- Cancellation observed during reconnect cleanup is terminal: the persistent
connection is not replayed, and the execution emits one
cancelledstatus. yieldAfterStatementpauses briefly after every 5th fast statement to prevent UI starvation- A single statement can emit multiple
QueryResultobjects whenexecuteAndFetch(...)returns multiple internal result sets - A reconnect keeps the same execution ID and emits
retryingbefore exactly one terminalsuccessorerror; it never emits a pre-retry terminal error
#3. Batch Streaming (batchQueryExecutor.ts → runQueriesWithStreaming)
Purpose: Execute multiple statements with progressive chunk delivery for real-time UI updates.
Flow: Same as sequential, but uses streamingManager.executeWithStreaming which delivers data via onChunk callback.
#StreamingManager (StreamingManager.ts)
An instance is created during extension activation and exposed through the
compatibility facade in queryCancellation.ts. It manages driver command
lifecycle, low-level cancellation and row delivery; it does not own the logical
execution lifecycle. Deactivation disposes command maps, pending-abort state and
cleanup timers.
#Command Registration
| Method | Purpose |
|---|---|
registerCommand(uri, cmd, sessionId) |
Track an executing command |
unregisterCommand(uri) |
Remove after completion |
isActive(uri) |
Check if a command is executing |
getCommand(uri) |
Retrieve the active command |
getActiveUris() |
List all active document URIs |
All URIs are normalized via normalizeUriKey for Windows drive-letter case insensitivity.
#Cancellation Protocol
User clicks "Cancel"
→ markCancelled(documentUri)
→ cancelledUris.add(normalizedKey)
→ state.isCancelled = true
During execution:
→ isCancelled(documentUri) checked on each row read
→ if cancelled: consumeRestAndCancel(reader, cmd, ...)
→ cancelFirst=true: cmd.cancel() + reader.close()
→ if close fails: fall back to drain loop
→ if drain times out (5s): prompt user for DROP SESSION
→ if user picks "Keep Waiting": extended drain (15s)
→ if extended drain fails: log warning, give up
→ cmd.cancel() called as final stepImportant: clearCancelled(uri) must be called at the start of each new document-bound execution sequence to reset stale cancellation flags.
#Row Limits
finalRowLimit = maxRows ?? limit (from getQueryConfig)When reached:
executeAndFetch: stops reading, callsconsumeRestAndCancel(cancelFirst=true), setslimitReached=trueexecuteWithStreaming: same behavior, sends partial chunk before stopping
#Long Query Alert
If netezza.longQueryAlertThreshold is > 0 (default: 10 minutes), a setTimeout warning is registered at execution start and cleared in the finally block.
#Result Shapes
#QueryResult (single query)
interface QueryResult {
columns: { name: string; type?: string; scale?: number }[];
data: unknown[][];
rowsAffected?: number;
limitReached?: boolean;
message?: string; // For DDL/DML with no result set
sql?: string; // The executed SQL
}Invariants:
columns.length === 0means DDL/DML (no result set) →messageis setdata.length === 0is valid (zero-row SELECT) →columnsis still populatedlimitReached === truemeans the server had more rows thanfinalRowLimitrowsAffectedcomes fromcmd._recordsAffected, -1 treated as "unknown"
#StreamingChunk (progressive delivery)
interface StreamingChunk {
columns: { name: string; type?: string; scale?: number }[];
rows: unknown[][];
isFirstChunk: boolean;
isLastChunk: boolean;
totalRowsSoFar: number;
limitReached: boolean;
}Invariants:
columnsis non-empty only onisFirstChunk === true- Final chunk always has
isLastChunk === true(even ifrowsis empty) - Zero-row results produce exactly 1 chunk:
{isFirstChunk: true, isLastChunk: true, rows: []} totalRowsSoFaris cumulative across all chunks- Streaming only covers the first result set for a statement; additional result sets are not progressively streamed
#InternalResultSet (multi-result-set)
interface InternalResultSet {
columns: ColumnDefinition[];
rows: unknown[][];
limitReached: boolean;
}executeAndFetchreturnsInternalResultSet[]— one per result setexecuteWithStreamingonly handles the first result set (Netezza convention)
#Type Detection (resultColumnMetadata.ts)
Column type resolution follows this priority:
- getDeclaredTypeName (if available) → normalize → return
- getColumnMetadata → check
declaredTypeName→ normalize → return - Character type enrichment (VARCHAR/NVARCHAR/CHAR/NCHAR):
- Extract base type from
getTypeName - Find length from: metadata → getTypeLength → schemaTable.ColumnSize → typeMod → declared name
- Format as
TYPE(length)
- Extract base type from
- Fallback:
getTypeName→ normalize
Numeric scale is only returned for numeric/decimal/integer/float types (via NUMERIC_SCALE_TYPE_ALIASES set). Sources: column metadata → schemaTable.NumericScale.
#Retry Protocol
Conditions for retry:
- Error matches
isConnectionBrokenError(TCP reset, ECONNRESET, or Netezza'sConnection protocol is invalid; reconnect is required, etc.) - No prior reconnect attempt in the logical execution
- Document has a persistent connection (
keepConnectionOpen) - Original and expanded SQL resolve to one conservatively allow-listed read-only statement
- Streaming has not delivered a chunk to its consumer
Retry flow:
- Close the persistent connection (
closeDocumentPersistentConnection) - Re-execute only the failed statement (which is the sole statement for single)
- Emit
retryingusing the original execution ID, then a newstatement-startedattempt - The logical execution emits exactly one terminal status; cancellation before
replay produces
cancelledand no second database execution
Writes, DDL, calls, executable macros, function/sequence expressions, multi-statement/ambiguous SQL, and streams with already delivered data are not replayed. Partial streamed rows stay visible; the terminal error explains why retry was suppressed. Cancellation and rejected safe-execution confirmation invoke the statement-failure hook so transaction-scoped metadata synchronization cannot retain stale state.
The dialect-neutral allow-list treats PostgreSQL positional parameters ($n),
JSON path operators (#>/#>>), and compact leading line comments as inert
syntax. A standalone # and an inline compact --text remain ambiguous and
therefore suppress automatic retry.
#Resource and error ownership
- Each execution owns a resource scope. Readers, commands, timers or other adapter resources registered there are disposed once in reverse order.
- Backend cleanup runs after the scope on success, error, cancellation and timeout. Repeated cancellation/disposal is idempotent.
- Cleanup failures are retained in
cleanupErrors. They turn an otherwise successful execution into an error but do not replace an earlier database failure or itscause. - Late driver callbacks are ignored after cancellation, terminal transition or observer detachment; late command handles are cancelled best-effort.
- Persistent reconnect closes only the target connection. Transient execution closes only the connection lease it acquired; there is no execution-ID based fallback that can close an unrelated connection.
- Mutable registries are instance-owned. Two orchestrators, extension activations or API server instances cannot cancel or dispose one another.
Desktop Result Panel append messages carry the stable result-set identity, an
authoritative row offset, and a monotonic chunk sequence. Duplicate or delayed
chunks are ignored. A gap, out-of-order delivery, or premature completion
causes one source-scoped requestResultSync; incremental delivery remains
blocked until an authoritative hydrate replaces the partial webview state.
Cancellation, source replacement, hydrate, and disposal reset the transport
cursor. Legacy unsequenced messages remain accepted for protocol compatibility.
#UI States
| State | Trigger | User Sees |
|---|---|---|
| Idle | No active execution | Empty result panel or previous results |
| Loading | executeReader pending |
Spinner/progress indicator |
| Partial Results | Streaming chunks arriving | Progressive row rendering |
| Complete | isLastChunk === true or executeAndFetch returns |
Full result grid |
| Limit Reached | limitReached === true |
Result grid + "limit reached" badge |
| Error | Exception during execution | Error message in panel |
| Cancelled | markCancelled + drain complete |
"Query cancelled" message |
| Retrying | Broken connection detected | "Reconnecting..." message |
#Result Panel Hydration and Export Contract
#Active-source streaming behavior
- First chunk for the active source causes a full hydrate so the webview receives complete result-set metadata.
- Subsequent chunks for the same active source are sent incrementally via
appendRows. - Last chunk for the active source is followed by
streamingComplete, includingtotalRowsandlimitReached. - Zero-row streamed results still hydrate as a real result set and still end with
streamingComplete.
#Inactive-source streaming behavior
- Background sources buffer streamed rows in
ResultStateManager. - Incremental
appendRowsandstreamingCompletemessages are not sent for inactive sources. - Buffered background results become visible on the next hydrate when the source becomes active.
#Cancellation and partial-result behavior
ResultStateManager.cancelExecution(sourceUri, currentRowCounts)marks result sets as cancelled and truncates buffered rows to the counts reported by the webview when provided.- Partial results kept after cancellation remain exportable.
- Export hydration must ignore stale row indices that no longer exist after truncation rather than throwing.
#Multi-result export behavior
- Excel multi-sheet export uses
ExportManager.hydrateExportData(...)as the authoritative hydration step. - Empty result sets are skipped.
- Result-set order, sheet names, and
isActiveflags are preserved for hydrated export items. - Column filtering and requested row ordering are preserved per sheet, while stale row indices are ignored.
#Hydration observability
- Full result-panel hydrate sends emit a structured
result_panel.hydrateperf event. - Event metadata includes hydrate reason, active source, result-set count, total row count, and executing-source count.
- Payload size is bucketed as
xs,s,m,l, orxlbased on serialized MessagePack bytes. - After the webview completes a hydrate render pass, it reports
result_panel.first_paintback to the host with duration, payload size, active source, row counts, and execution state. - The host keeps a rolling local sample window for
result_panel.first_paintso dogfooding sessions can be summarized without parsing raw logs. netezza.showResultPanelPerformanceStatsis the maintainer-facing entry point for reviewing that rolling first-paint baseline.netezza.clearResultPanelPerformanceStatsresets that local baseline before a fresh profiling session.- Large-payload regressions should be treated as a Phase 4 concern even if correctness tests still pass.
#Result-panel execution state UX
- The loading overlay remains the blocking affordance while the active source is executing.
- A lightweight status banner communicates non-blocking execution state for the active source when extra attention is needed: retrying, error, or cancelled. Successful completion should not add a redundant line above the grid.
- Cancelled executions should explicitly tell the user that partial results may still be available.
- Error state should remain visible even when the active result set is not the Logs tab.
- When a source is active but has no buffered results yet, the grid surface should render an explicit empty-state card instead of a synthetic placeholder log.
- Non-tabular completion states such as DDL success, empty result sets, and invalid render data should render recovery-oriented state cards with short next-step guidance.
- Banner copy should include the active source label where possible and distinguish between zero-row success, partial-error success, and cancellation with or without retained rows.
- Result-set tabs should expose lightweight status cues for error, cancelled-partial, and empty-result states so users can navigate to the right surface without relying on the grid banner.
- Error views should offer a direct recovery path back to
Logsin addition to Copilot/error details, so diagnostic flow does not depend on manual tab hunting. - Row View is a supported record/details surface and should stay reactive to selection changes plus active-result switches, rather than behaving like a static side panel.
- A dedicated Value Viewer should be available for individual cells with long or structured content, instead of forcing users to inspect everything through Row View or truncated grid cells.
#Disk-backed results (SQLite spill)
See SQL_RESULTS_FILTERING.md for how Loaded rows, All rows + LIMIT, and disk-backed filtering relate.
- Host spill triggers at
min(memoryRowThreshold, rowThreshold)(defaults: 25 000 / 500 000). This is independent of the webview stream cap (DISK_BACKED_WEBVIEW_STREAM_CAP= 250 000). - When spill activates during streaming, the host clears
ResultSet.data, subsequent chunks insert directly into SQLite, and the webview receivesdiskBackedActivatewith the stableresultSetId, a first page, and a ~600-row scroll window. - While still streaming above the webview cap but before/after spill, the webview may receive
rowCountUpdateinstead of fullappendRowspayloads. The message carries the optional stableresultSetIdwhen available; a mismatched identity is ignored and requests an authoritative sync, while legacy messages without an identity remain compatible. - After streaming completes, disk-backed sources must not be fully re-hydrated into the webview when
node:sqliteis available. - Filters, sort, global search, aggregations, and export operate on the full SQLite store via
DiskQuerySpec(not the visible window only). - Disk-backed grouping uses lazy SQL
GROUP BYtree expansion (queryDiskGroups); group/leaf pages load in 600-row windows with scroll-triggered pagination. - Optional idle spill (
idleSpillMinutes, default 0 = disabled) moves eligible in-memory result sets to SQLite after inactivity; hiding the panel can spill inactive sources immediately when enabled.
#Key Configuration
| Setting | Default | Purpose |
|---|---|---|
justybase.query.executionTimeout |
3600 | Query execution timeout in seconds |
justybase.query.rowLimit |
200000 | Maximum rows to fetch per execution |
justybase.results.diskBackedResults.memoryRowThreshold |
25000 | Host RAM spill trigger |
justybase.results.diskBackedResults.rowThreshold |
500000 | Hard upper spill bound |
justybase.results.diskBackedResults.idleSpillMinutes |
0 | Idle spill (0 = off) |
netezza.longQueryAlertThreshold |
10 | Minutes before showing "long query" warning |