Skip to content

perf(postgres): batch doBulk + named prepared statements#997

Open
SamTV12345 wants to merge 1 commit into
mainfrom
perf/postgres-driver
Open

perf(postgres): batch doBulk + named prepared statements#997
SamTV12345 wants to merge 1 commit into
mainfrom
perf/postgres-driver

Conversation

@SamTV12345
Copy link
Copy Markdown
Member

perf(postgres): PostgreSQL driver optimizations

Touches databases/postgres_db.ts only. Split out of the old combined MongoDB+PostgreSQL PR (#994) so each backend gets its own PR per AGENTS.md ("one backend per PR"). The MongoDB half stays in #994. The postgres_db.ts diff here is unchanged from the original combined PR.

  • doBulk collapses N round-trips into one on the native-upsert path via a single multi-row INSERT … VALUES ($1,$2),($3,$4),… ON CONFLICT DO UPDATE. The function-based fallback (old PG / CockroachDB without native upsert) is preserved for single-row and multi-row cases.
  • Named prepared statements on the three hottest single-row queries (get, set, remove) so PG parses + plans them once per connection.

🤖 Generated with Claude Code

@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

PostgreSQL driver optimizations: batch doBulk and named prepared statements

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Batch doBulk operations into single multi-row INSERT with ON CONFLICT for native upsert
• Add named prepared statements to three hottest queries (get, set, remove)
• Refactor doBulk to use async.parallel with cleaner task composition
• Preserve function-based fallback for old PostgreSQL/CockroachDB without native upsert
Diagram
flowchart LR
  A["doBulk Operations"] -->|Native Upsert| B["Single Multi-row INSERT"]
  A -->|Function-based| C["Per-row Upsert"]
  B --> D["ON CONFLICT DO UPDATE"]
  D --> E["Reduced Round-trips"]
  F["Single-row Queries"] -->|Named Statements| G["get/set/remove"]
  G --> H["Parse Once Per Connection"]

Loading

Grey Divider

File Changes

1. databases/postgres_db.ts ✨ Enhancement +62/-39

Batch operations and named prepared statements

• Converted get, set, and remove methods to use named prepared statements (ueberdb_get,
 ueberdb_set_native/ueberdb_set_function, ueberdb_remove)
• Refactored doBulk to collapse multiple set operations into a single multi-row INSERT with ON
 CONFLICT for native upsert path
• Implemented fallback to per-row upsert for function-based or single-row scenarios
• Restructured bulk delete to use parameterized placeholders and wrapped queries in async.parallel
 tasks with proper error handling

databases/postgres_db.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 30, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0)

Grey Divider


Action required

1. Write promise can hang 🐞 Bug ☼ Reliability
Description
In databases/postgres_db.ts, set() and doBulk() can return without calling the provided callback
when upsertStatement is null/undefined, which causes CacheAndBufferLayer’s util.promisify wrapper to
create a Promise that never settles and can deadlock buffered writes/flush.
Code

databases/postgres_db.ts[R190-210]

Evidence
The driver now has early-exit/no-op paths that skip the callback when upsertStatement is not ready,
but CacheAndBufferLayer promisifies these callback APIs and awaits them; promisify requires the
callback to be invoked to resolve/reject the Promise, so skipping it can hang flush/writes
indefinitely.

databases/postgres_db.ts[188-197]
databases/postgres_db.ts[207-210]
lib/CacheAndBufferLayer.ts[171-185]
lib/CacheAndBufferLayer.ts[217-220]
lib/CacheAndBufferLayer.ts[585-602]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`databases/postgres_db.ts` has code paths where `set()` / `doBulk()` return early (or do nothing) if `this.upsertStatement` is not set, **without invoking the callback**. Because `CacheAndBufferLayer` wraps legacy callback APIs with `node:util.promisify()` and then `await`s them, a missing callback causes an **unresolved Promise** and can deadlock writes/flush.

### Issue Context
- `CacheAndBufferLayer` promisifies non-async DB drivers and `await`s `doBulk()` during flush.
- `CacheAndBufferLayer` also starts its periodic `flush()` timer in the constructor, so a caller that performs writes before awaiting/finishing DB `init()` can hit these paths.

### Fix Focus Areas
- Ensure **every** `set()`/`doBulk()` invocation either:
 - calls the callback with an error (preferred: fail fast), or
 - calls the callback successfully (only if it is safe to drop work, which it likely is not).
- Concretely: if `this.upsertStatement` is null/undefined, call `callback(new Error('...not initialized...'))` (and for `set()`, also pass an appropriate dummy result if needed), instead of returning/do-nothing.

#### Code locations
- databases/postgres_db.ts[188-210]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread databases/postgres_db.ts Outdated
Comment on lines +190 to +210
const val = '' as any;
callback(Error('Your Key can only be 100 chars'), val);
} else if (this.upsertStatement != null) {
this.db.query(this.upsertStatement, [key, value], callback);
const name = this.upsertStatement.startsWith('INSERT INTO store(key, value) VALUES')
? 'ueberdb_set_native'
: 'ueberdb_set_function';
this.db.query({ name, text: this.upsertStatement, values: [key, value] }, callback);
}
}

remove(key:string, callback:()=>{}) {
this.db.query('DELETE FROM store WHERE key=$1', [key], callback);
remove(key: string, callback: () => {}) {
this.db.query(
{ name: 'ueberdb_remove', text: 'DELETE FROM store WHERE key=$1', values: [key] },
callback
);
}

doBulk(bulk:BulkObject[], callback:()=>{}) {
const replaceVALs = [];
let removeSQL = 'DELETE FROM store WHERE key IN (';
const removeVALs: string[] = [];
doBulk(bulk: BulkObject[], callback: () => {}) {
if (!this.upsertStatement) {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Write promise can hang 🐞 Bug ☼ Reliability

In databases/postgres_db.ts, set() and doBulk() can return without calling the provided callback
when upsertStatement is null/undefined, which causes CacheAndBufferLayer’s util.promisify wrapper to
create a Promise that never settles and can deadlock buffered writes/flush.
Agent Prompt
### Issue description
`databases/postgres_db.ts` has code paths where `set()` / `doBulk()` return early (or do nothing) if `this.upsertStatement` is not set, **without invoking the callback**. Because `CacheAndBufferLayer` wraps legacy callback APIs with `node:util.promisify()` and then `await`s them, a missing callback causes an **unresolved Promise** and can deadlock writes/flush.

### Issue Context
- `CacheAndBufferLayer` promisifies non-async DB drivers and `await`s `doBulk()` during flush.
- `CacheAndBufferLayer` also starts its periodic `flush()` timer in the constructor, so a caller that performs writes before awaiting/finishing DB `init()` can hit these paths.

### Fix Focus Areas
- Ensure **every** `set()`/`doBulk()` invocation either:
  - calls the callback with an error (preferred: fail fast), or
  - calls the callback successfully (only if it is safe to drop work, which it likely is not).
- Concretely: if `this.upsertStatement` is null/undefined, call `callback(new Error('...not initialized...'))` (and for `set()`, also pass an appropriate dummy result if needed), instead of returning/do-nothing.

#### Code locations
- databases/postgres_db.ts[188-210]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@SamTV12345 SamTV12345 force-pushed the perf/postgres-driver branch from 82d3968 to 0aa9514 Compare May 30, 2026 13:44
@SamTV12345 SamTV12345 requested a review from JohnMcLear May 30, 2026 13:51
@SamTV12345 SamTV12345 self-assigned this May 30, 2026
- doBulk collapses N round-trips into one on the native-upsert path via a
  single multi-row INSERT ... VALUES ($1,$2),($3,$4),... ON CONFLICT DO
  UPDATE. The function-based fallback (old PG / CockroachDB without native
  upsert) is preserved for single-row and multi-row cases.
- Named prepared statements on the three hottest single-row queries (get,
  set, remove) so PG parses + plans them once per connection.

Split out of #994 to keep one backend per PR (AGENTS.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@SamTV12345 SamTV12345 force-pushed the perf/postgres-driver branch from 0aa9514 to 39f6ecf Compare May 30, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant