Skip to content

feat: implement D1 upsert - #55

Closed
treeder wants to merge 1 commit into
mainfrom
feat-d1-upsert-5877242264351571818
Closed

feat: implement D1 upsert#55
treeder wants to merge 1 commit into
mainfrom
feat-d1-upsert-5877242264351571818

Conversation

@treeder

@treeder treeder commented May 1, 2026

Copy link
Copy Markdown
Owner

Adds upsert and upsertp methods to D1 in d1.js leveraging SQLite's INSERT ... ON CONFLICT(id) DO UPDATE SET ... syntax. Properly merges object properties via json_patch. Also adds a test file (test_upsert.js) to verify functionality.


PR created automatically by Jules for task 5877242264351571818 started by @treeder

Adds `upsert` and `upsertp` methods to `D1` in `d1.js` leveraging SQLite's `INSERT ... ON CONFLICT(id) DO UPDATE SET ...` syntax. Properly merges object properties via `json_patch`. Also adds a test file (`test_upsert.js`) to verify functionality.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces upsert functionality to the D1 library, including the core _upsertp, upsertp, and upsert methods, along with a new API endpoint and comprehensive tests. Feedback focuses on improving the robustness of the ON CONFLICT clause by using model-defined primary keys, refactoring duplicated logic between insertion and upsertion methods, and relaxing field name validation to allow underscores.

Comment thread d1.js
Comment on lines +536 to +538
s += ` ON CONFLICT(id) DO UPDATE SET ${updates.join(', ')}`
} else {
s += ` ON CONFLICT(id) DO NOTHING`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The ON CONFLICT clause is hardcoded to use id as the conflict target. While the library seems to favor id as the primary key, it would be more robust to use the primary key defined in the model if available, similar to how jsonObjectCol handles it.

Comment thread d1.js
Comment on lines +462 to +518
async _upsertp(table, obj, opts = {}) {
if (typeof table != 'string') {
opts.model = table
}
let ob = null
let fields = []
let values = []
if (obj instanceof Object && !Array.isArray(obj)) {
ob = obj
values = []
let f2 = []
for (const f in obj) {
f2.push(f)
values.push(obj[f])
}
fields = f2
} else {
// deprecated
fields = obj
values = opts
ob = {}
opts = {}
}
let id
if (!fields.includes('id')) {
id = nanoid()
fields.push('id')
values.push(id)
ob.id = id
} else {
id = values[fields.indexOf('id')]
if (!id) {
id = nanoid()
values[fields.indexOf('id')] = id
ob.id = id
}
}
let now = new Date()
if (!fields.includes('createdAt')) {
fields.push('createdAt')
values.push(now)
ob.createdAt = now
}
if (!fields.includes('updatedAt')) {
fields.push('updatedAt')
values.push(now)
ob.updatedAt = now
} else {
// If updating, we should probably set updatedAt to now anyway, or keep what they provided?
// Let's keep what they provided for consistency with insert, but typically upsert would update the date.
// Actually we'll let ON CONFLICT handle updating it to `excluded.updatedAt`.
}
for (let f of fields) {
if (!/^[a-zA-Z0-9]+$/.test(f)) {
throw new Error('Field must be alphanumeric')
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There is significant code duplication between _upsertp and _insertp (lines 374-426). Most of the logic for extracting fields, generating IDs, and setting timestamps is identical. Consider refactoring this shared logic into a private helper method to improve maintainability.

Comment thread d1.js
// Actually we'll let ON CONFLICT handle updating it to `excluded.updatedAt`.
}
for (let f of fields) {
if (!/^[a-zA-Z0-9]+$/.test(f)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The alphanumeric check !/^[a-zA-Z0-9]+$/.test(f) is quite restrictive as it prevents the use of underscores in field names, which is a common convention in SQL (e.g., user_id). While this matches the existing _insertp implementation, consider allowing underscores to support a wider range of database schemas.

Suggested change
if (!/^[a-zA-Z0-9]+$/.test(f)) {
if (!/^[a-zA-Z0-9_]+$/.test(f)) {

@treeder treeder closed this Aug 20, 2026
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