angelxmoreno / angelxmoreno/bun-sqlite-orm

[Feature]: Add static update methods for efficient single-query updates

Open
#59 0 comments 0 reactions 0 assignees View on GitHub
developer-experience feature high-priority query-builder
Dominant language
TypeScript
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

## Problem Statement

Currently, bun-sqlite-orm only supports updates through instance methods, forcing an inefficient \"fetch-then-save\" pattern:

```typescript
// Current: Requires 2 queries (SELECT + UPDATE)
const user = await User.get(1);
user.name = 'Updated Name';
user.email = 'new@example.com';
await user.save();
```

This approach has several problems:
1. **Performance**: Requires 2 database queries instead of 1
2. **Memory overhead**: Must load entire entity just to update a few fields
3. **Race conditions**: Data could change between fetch and save
4. **Verbosity**: Simple updates require multiple lines of code
5. **API inconsistency**: Static methods exist for reads but not writes

## Proposed Solution

Add static update methods to `BaseEntity` that perform direct database updates without requiring entity instantiation.

### Method Signatures

```typescript
// Update records matching conditions
static async update(
this: new () => T,
conditions: Where,
data: Partial
): Promise

// Convenience method for updating by ID
static async updateById(
this: new () => T,
id: number | string,
data: Partial
): Promise

// Insert if not exists, update if exists
static async upsert(
this: new () => T,
conditions: Where,
data: Partial
): Promise
```

### Design Decisions

**Return Values**: All methods return `number` (count of affected rows)
- Consistent API across all update methods
- Efficient - no need to fetch updated entities
- Clear indication of operation success (0 = no matches, >0 = updated)

**Validation**: Run same validation as instance saves
- Maintains data integrity
- Consistent behavior with existing save operations
- Validation errors thrown before database operation

**Undefined Handling**: `undefined` values treated as `null`
- Clear, predictable behavior
- Matches SQL null semantics
- No ambiguity about field updates

**Error Handling**:
- No matching records: Return `0` (not an error)
- Constraint violations: Throw `ConstraintViolationError`
- Validation failures: Throw `ValidationError`

## Example Usage

### Basic Updates
```typescript
// Update single field by ID
const count = await User.updateById(1, { name: 'New Name' });
console.log(`Updated ${count} user(s)`); // \"Updated 1 user(s)\" or \"Updated 0 user(s)\"

// Update multiple fields
await User.updateById(1, {
name: 'John Doe',
email: 'john@example.com',
lastLoginAt: new Date()
});
```

### Conditional Updates
```typescript
// Update by conditions
const count = await User.update(
{ status: 'pending' },
{ status: 'approved', approvedAt: new Date() }
);
console.log(`Approved ${count} pending users`);

// Complex conditions (when issue #35 is complete)
await User.update(
{
createdAt: { lt: thirtyDaysAgo },
status: { in: ['pending', 'draft'] }
},
{ status: 'archived' }
);
```

### Upsert Operations
```typescript
// Insert or update user settings
await UserSettings.upsert(
{ userId: 123 },
{ theme: 'dark', notifications: true, language: 'en' }
);

// Upsert with complex data
await Analytics.upsert(
{ userId: 456, date: today },
{ pageViews: 1, sessionDuration: 300 }
);
```

### API Endpoints
```typescript
// RESTful PATCH endpoint
app.patch('/api/users/:id', async (req, res) => {
const count = await User.updateById(req.params.id, req.body);

if (count === 0) {
return res.status(404).json({ error: 'User not found' });
}

res.json({ message: 'User updated successfully' });
});

// Bulk update endpoint
app.patch('/api/users/bulk', async (req, res) => {
const { conditions, data } = req.body;
const count = await User.update(conditions, data);

res.json({
message: `Updated ${count} users`,
affectedRows: count
});
});
```

## Implementation Details

### SQL Generation
```typescript
// update() method generates:
UPDATE users
SET name = ?, email = ?, updated_at = ?
WHERE status = ?

// updateById() method generates:
UPDATE users
SET name = ?, email = ?, updated_at = ?
WHERE id = ?

// upsert() method generates:
INSERT INTO users (user_id, theme, notifications)
VALUES (?, ?, ?)
ON CONFLICT (user_id)
DO UPDATE SET theme = excluded.theme, notifications = excluded.notifications
```

### Validation Integration
```typescript
// Before SQL execution, validate the data partial
const entity = new this();
Object.assign(entity, data);
await entity.validate(); // Throws ValidationError if invalid
```

### Error Handling
```typescript
try {
const count = await User.updateById(1, { email: 'invalid-email' });
} catch (error) {
if (error instanceof ValidationError) {
// Handle validation errors
console.log('Validation failed:', error.errors);
} else if (error instanceof ConstraintViolationError) {
// Handle database constraint violations
console.log('Constraint violation:', error.constraintType);
}
}
```

## Performance Benefits

### Before (Instance Method)
```typescript
// 2 database queries + memory overhead
const user = await User.get(1); // SELECT query
user.name = 'Updated Name';
await user.save(); // UPDATE query
```

### After (Static Method)
```typescript
// 1 database query, no memory overhead
await User.updateById(1, { name: 'Updated Name' }); // UPDATE query only
```

### Bulk Operations
```typescript
// Before: N+2 queries for N records
const users = await User.find({ status: 'pending' }); // 1 SELECT
for (const user of users) {
user.status = 'approved';
await user.save(); // N UPDATE queries
}

// After: 1 query regardless of record count
await User.update({ status: 'pending' }, { status: 'approved' }); // 1 UPDATE
```

## Dependencies

- **Issue #35** - Advanced WHERE conditions: Will improve `conditions` parameter typing
- **Issue #57** - Enhanced error system: Will provide proper error types (`ConstraintViolationError`, etc.)

## Breaking Changes

None - these are new method additions.

## Alternatives Considered

1. **Return updated entities**: Would require additional SELECT query, defeating performance purpose
2. **Skip validation**: Would create inconsistency with instance save behavior
3. **Separate bulk methods**: `update()` already handles bulk operations efficiently
4. **Different naming**: Considered `modify()`, `patch()`, but `update()` is more standard

## Additional Context

### Integration with Existing Features
- Uses existing validation system for data integrity
- Integrates with enhanced error system (issue #57)
- Will benefit from typed conditions (issue #35)
- Follows same patterns as existing static methods

### Future Enhancements
- Entity lifecycle hooks (when implemented)
- Query result caching integration
- Batch update optimizations
- Transaction support integration

This feature addresses a major gap in the current API and would significantly improve performance for update operations while maintaining consistency with the existing codebase design.

Contributor guide

Open the contributing guide

Research direction

Start at BaseEntity and compare the proposed static update methods with the existing static read methods, then trace instance-save validation and SQL generation. Review dependencies on issues #35 and #57. Done means update, updateById, and upsert provide the stated return values, validation, undefined handling, and error behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, sqlite, typescript
Domain
backend, database
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.