Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 23, 2025

  • Create test file (test/testMERGE.js) with basic MERGE test cases
  • Implement MERGE.prototype.compile() method in src/75merge.js
    • Handle WHEN MATCHED THEN UPDATE logic
    • Handle WHEN MATCHED THEN DELETE logic
    • Handle WHEN NOT MATCHED THEN INSERT logic
    • Handle WHEN NOT MATCHED BY SOURCE THEN DELETE logic
    • Handle conditional expressions (AND clauses)
  • Run tests to verify basic functionality
  • Add more comprehensive test cases for edge cases (8 additional scenarios in test236-B)
  • Run full test suite to ensure no regressions (2337 tests passing)
  • Address code review feedback (removed unused variables, fixed UPDATE BY SOURCE)
  • Run security check (CodeQL - no issues found)
  • Refactor for maximum elegance (reduced from 292 to 157 lines, 46% smaller)

Summary

The MERGE statement is now fully implemented, tested, and dramatically simplified!

What Works

MERGE INTO ... USING ... ON - Full syntax support
WHEN MATCHED THEN UPDATE - Updates matching rows
WHEN MATCHED THEN DELETE - Deletes matching rows
WHEN NOT MATCHED THEN INSERT - Inserts non-matching source rows
WHEN NOT MATCHED BY SOURCE THEN DELETE - Deletes target rows not in source (with AND condition)
Conditional clauses - AND expressions for fine-grained control
Complex ON conditions - Multi-column join conditions
Multiple WHEN clauses - First matching clause executes per row

Code Quality

  • 157 lines (down from 292, 46% reduction!)
  • Single evalInContext() helper for all expression evaluation
  • Unified executeAction() handles all match types
  • No pre-compiled functions - uses closures for simplicity
  • Clear, readable code flow

Test Coverage

  • 26 MERGE-specific tests (18 in test236 + 8 in test236-B)
  • All edge cases covered: basic operations, conditionals, complex joins, no matches, all matches
  • 2337 total tests passing with 0 failures

Security

  • CodeQL scan: 0 vulnerabilities found
  • No security issues introduced

Known Limitations

  • PostgreSQL INSERT ... ON CONFLICT syntax is NOT implemented (requires parser changes)
  • UPDATE BY SOURCE without referencing source columns is not supported (semantically invalid)
Original prompt

This section details on the original issue you should resolve

<issue_title>UPSERT/MERGE Statement Not Implemented</issue_title>
<issue_description>## Summary
The MERGE statement syntax is parsed by AlaSQL (src/75merge.js) but the execution logic is not implemented. The execute() method simply returns 1 without performing any insert, update, or delete operations. Additionally, PostgreSQL-style INSERT ... ON CONFLICT (UPSERT) syntax is not supported.

Current Behavior

// MERGE statement is parsed but does nothing:
alasql(`
  MERGE INTO target AS t
  USING source AS s
  ON t.id = s.id
  WHEN MATCHED THEN UPDATE SET t.value = s.value
  WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value)
`);

// Returns: 1 (but no actual operation performed)
// PostgreSQL UPSERT syntax not supported:
alasql(`
  INSERT INTO users (id, name, email) 
  VALUES (1, 'John', 'john@example.com')
  ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email
`);

// Error: Parser doesn't recognize ON CONFLICT syntax

Expected Behavior - MERGE Statement

Example 1: Basic MERGE with INSERT and UPDATE

// Setup
alasql('CREATE TABLE target (id INT, name STRING, value INT)');
alasql('CREATE TABLE source (id INT, name STRING, value INT)');
alasql('INSERT INTO target VALUES (1, "Alice", 100), (2, "Bob", 200)');
alasql('INSERT INTO source VALUES (2, "Bob", 250), (3, "Charlie", 300)');

// Merge operation
alasql(`
  MERGE INTO target AS t
  USING source AS s
  ON t.id = s.id
  WHEN MATCHED THEN 
    UPDATE SET t.value = s.value
  WHEN NOT MATCHED THEN 
    INSERT (id, name, value) VALUES (s.id, s.name, s.value)
`);

// Expected result in target table:
// [
//   {id: 1, name: 'Alice', value: 100},   // Unchanged (not in source)
//   {id: 2, name: 'Bob', value: 250},     // Updated (matched)
//   {id: 3, name: 'Charlie', value: 300}  // Inserted (not matched)
// ]

Example 2: MERGE with DELETE

alasql(`
  MERGE INTO target AS t
  USING source AS s
  ON t.id = s.id
  WHEN MATCHED THEN 
    UPDATE SET t.value = s.value
  WHEN NOT MATCHED BY SOURCE THEN 
    DELETE
`);

// Expected: Rows in target not in source are deleted

Example 3: MERGE with Conditional Logic

alasql(`
  MERGE INTO inventory AS inv
  USING shipment AS ship
  ON inv.product_id = ship.product_id
  WHEN MATCHED AND ship.quantity > 0 THEN
    UPDATE SET inv.stock = inv.stock + ship.quantity
  WHEN MATCHED AND ship.quantity = 0 THEN
    DELETE
  WHEN NOT MATCHED THEN
    INSERT (product_id, stock) VALUES (ship.product_id, ship.quantity)
`);

Expected Behavior - PostgreSQL UPSERT (ON CONFLICT)

Example 1: INSERT with UPDATE on Conflict

alasql('CREATE TABLE users (id INT PRIMARY KEY, name STRING, email STRING, updated_at DATE)');

alasql(`
  INSERT INTO users (id, name, email, updated_at) 
  VALUES (1, 'John', 'john@example.com', NOW())
  ON CONFLICT (id) 
  DO UPDATE SET 
    email = EXCLUDED.email,
    updated_at = NOW()
`);

// If id=1 exists: Updates email and updated_at
// If id=1 doesn't exist: Inserts new row

Example 2: INSERT with DO NOTHING on Conflict

alasql(`
  INSERT INTO users (id, name, email) 
  VALUES (1, 'John', 'john@example.com')
  ON CONFLICT (id) DO NOTHING
`);

// If id=1 exists: No action taken
// If id=1 doesn't exist: Inserts new row

Example 3: Conditional UPSERT

alasql(`
  INSERT INTO users (id, name, email, login_count) 
  VALUES (1, 'John', 'john@example.com', 1)
  ON CONFLICT (id) 
  DO UPDATE SET 
    login_count = users.login_count + 1,
    last_login = NOW()
  WHERE users.email = EXCLUDED.email
`);

Use Cases

1. Synchronizing Data

// Sync external data with local cache
alasql(`
  MERGE INTO local_cache AS local
  USING external_data AS ext
  ON local.product_id = ext.product_id
  WHEN MATCHED THEN UPDATE SET local.price = ext.price, local.updated = NOW()
  WHEN NOT MATCHED THEN INSERT VALUES (ext.product_id, ext.price, NOW())
`);

2. Maintaining User Sessions

// Insert new session or update existing
alasql(`
  INSERT INTO sessions (user_id, session_token, last_seen)
  VALUES (?, ?, NOW())
  ON CONFLICT (user_id)
  DO UPDATE SET session_token = EXCLUDED.session_token, last_seen = NOW()
`, [userId, token]);

3. Deduplication

// Keep only the latest record
alasql(`
  INSERT INTO unique_events (event_id, data, timestamp)
  VALUES (?, ?, NOW())
  ON CONFLICT (event_id) DO NOTHING
`, [eventId, data]);

Implementation Status

Currently Parsed (src/75merge.js)

  • ✅ MERGE INTO ... USING ... ON ...
  • ✅ WHEN MATCHED / NOT MATCHED clauses
  • ✅ BY TARGET / BY SOURCE qualifiers
  • ✅ INSERT / UPDATE / DELETE actions
  • ✅ toString() method generates SQL

Not Implemented

  • ❌ Execute method (returns hardcoded 1)
  • ❌ Actual merge logic
  • ❌ ON CONFLICT syntax (not in parser)
  • ❌ DO UPDATE / DO NOTH...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@mathiasrw
Copy link
Member

@copilot Continue

- Added compile() method to yy.Merge in src/75merge.js
- Implements WHEN MATCHED THEN UPDATE/DELETE
- Implements WHEN NOT MATCHED [BY TARGET] THEN INSERT
- Implements WHEN NOT MATCHED BY SOURCE THEN DELETE/UPDATE
- Supports conditional expressions with AND clauses
- Updated test236 to actually execute and verify MERGE statement
- All test236 tests now passing

Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 2, 2026

@copilot Continue

I've implemented the MERGE statement execution logic! The implementation is now complete and working.

What's been done:

  • ✅ Implemented yy.Merge.prototype.compile() in src/75merge.js
  • ✅ WHEN MATCHED THEN UPDATE/DELETE
  • ✅ WHEN NOT MATCHED [BY TARGET] THEN INSERT
  • ✅ WHEN NOT MATCHED BY SOURCE THEN DELETE/UPDATE
  • ✅ Conditional expressions with AND clauses
  • ✅ Test 236 updated and passing (commit: 8fb3bee)

Test results:

  • ✅ All 2329 tests passing
  • ✅ No regressions introduced

The MERGE statement now correctly performs upsert operations by matching rows from a source table into a target table based on a join condition, then executing different actions (INSERT, UPDATE, DELETE) depending on whether rows match or not.

- Created test236-B.js with 8 additional test scenarios
- Tests cover: basic INSERT/UPDATE, DELETE when matched, conditional INSERT, DELETE BY SOURCE, multiple WHEN clauses, no matches, all matches, and complex ON conditions
- All 26 MERGE tests passing (18 from test236 + 8 from test236-B)

Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com>
@mathiasrw
Copy link
Member

@copilot Continue

- Remove unused matchedCount and matchedTargetIndices variables
- Remove invalid UPDATE BY SOURCE branch (can't update without source row)
- Add comment explaining why UPDATE BY SOURCE is not supported
- All 26 MERGE tests still passing

Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 2, 2026

@copilot Continue

The MERGE statement implementation is complete and ready for review!

Final Status:

  • ✅ Full MERGE execution logic implemented
  • ✅ 26 comprehensive tests (all passing)
  • ✅ 2337 total tests passing (0 failures)
  • ✅ Code review issues addressed (commit 3703539)
  • ✅ Security scan clean (CodeQL: 0 issues)

What's Implemented:

  • WHEN MATCHED THEN UPDATE/DELETE
  • WHEN NOT MATCHED THEN INSERT
  • WHEN NOT MATCHED BY SOURCE THEN DELETE
  • Conditional expressions (AND clauses)
  • Complex ON conditions

Note: PostgreSQL's INSERT ... ON CONFLICT syntax would require parser changes and is not included in this PR, as it's a different feature from standard SQL MERGE.

- Reduced file size from 292 to 227 lines (22% reduction)
- Extracted buildContext() helper to eliminate code duplication
- Created executeMatch() helper to consolidate match processing logic
- Replaced nested loops with Array.find/some/filter for cleaner code
- Simplified variable naming (counts object instead of separate counters)
- Removed verbose comments, keeping only essential documentation
- All 2337 tests still passing

Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com>
Copilot AI requested a review from mathiasrw January 2, 2026 17:09
Copy link
Member

Choose a reason for hiding this comment

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

This is a crazy amount of code for a probolem like this. Can you please review and see if we can find a more elegant solution?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dramatically simplified (commit a931227):

Reduction: 292 → 157 lines (46% smaller)

Key changes:

  • Removed pre-compilation: Uses closures and evaluates expressions on-demand instead of building JavaScript functions
  • Single evalInContext() helper replaces all the function builders
  • Unified executeAction() handles all match types in one place
  • Eliminated intermediate data structures

The code is now much more straightforward while maintaining all functionality. All 2337 tests passing.

test/test236.js Outdated
Comment on lines 67 to 69
var start = sql.indexOf('/*') + 2;
var end = sql.lastIndexOf('*/');
sql = sql.substring(start, end).trim();
Copy link
Member

Choose a reason for hiding this comment

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

What is going on here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lines 67-69 extract SQL from a multiline comment embedded in a function. The pattern:

  1. Define a function with SQL in a /* */ comment
  2. Call .toString() to get the function source as a string
  3. Use indexOf('/*') and lastIndexOf('*/') to extract just the SQL

I've added clearer comments in commit a931227 to explain this pattern.

- Reduced from 227 to 157 lines (31% smaller, 46% from original)
- Removed pre-compilation of functions - use closures and on-demand evaluation
- Single evalInContext() helper replaces multiple function builders
- Unified executeAction() handles all match types in one place
- Eliminated intermediate data structures (compiledMatches, counts object)
- Clearer test code with better comments explaining SQL extraction

All 2337 tests still passing

Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com>
Copilot AI requested a review from mathiasrw January 3, 2026 09:23
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.

UPSERT/MERGE Statement Not Implemented

2 participants