Skip to content

UPSERT/MERGE Statement Not Implemented #2407

@mathiasrw

Description

@mathiasrw

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', '[email protected]')
  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', '[email protected]', 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', '[email protected]')
  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', '[email protected]', 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 NOTHING actions
  • ❌ EXCLUDED table reference

Implementation Requirements

For MERGE Statement

1. Execute Logic in src/75merge.js

yy.Merge.prototype.execute = function(databaseid, params, cb) {
  // 1. Resolve target and source tables
  // 2. Execute join on ON condition
  // 3. Classify rows:
  //    - Matched: exists in both target and source
  //    - Not matched by target: in source only
  //    - Not matched by source: in target only
  // 4. For each classification, execute appropriate WHEN clause
  // 5. Return count of affected rows
};

2. Handle Multiple WHEN Clauses

  • Process in order specified
  • Stop at first matching WHEN clause per row
  • Support optional AND conditions

3. Action Execution

  • UPDATE: Modify matching rows in target table
  • INSERT: Add new rows to target table
  • DELETE: Remove rows from target table

For PostgreSQL UPSERT (ON CONFLICT)

1. Parser Updates (src/alasqlparser.jison)

InsertStatement:
  INSERT INTO table ...
  [ON CONFLICT (columns) DO UPDATE SET ... | DO NOTHING]

2. New Conflict Resolution Logic

  • Check for constraint violations (PRIMARY KEY, UNIQUE)
  • On conflict:
    • DO NOTHING: Skip insert
    • DO UPDATE: Execute UPDATE with EXCLUDED table reference
  • EXCLUDED table: Access to values from INSERT statement

3. Integration with INSERT (src/70insert.js)

  • Detect ON CONFLICT clause
  • Catch constraint violation
  • Execute conflict resolution action

Test Cases

MERGE Tests

describe('MERGE Statement', function() {
  beforeEach(function() {
    alasql('CREATE TABLE target (id INT PRIMARY KEY, value INT)');
    alasql('CREATE TABLE source (id INT, value INT)');
  });
  
  it('should insert non-matched rows', function(done) {
    alasql('INSERT INTO source VALUES (1, 100), (2, 200)');
    alasql(`
      MERGE INTO target AS t
      USING source AS s ON t.id = s.id
      WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.value)
    `);
    var res = alasql('SELECT * FROM target ORDER BY id');
    assert.deepEqual(res, [{id: 1, value: 100}, {id: 2, value: 200}]);
    done();
  });
  
  it('should update matched rows', function(done) {
    alasql('INSERT INTO target VALUES (1, 100)');
    alasql('INSERT INTO source VALUES (1, 200)');
    alasql(`
      MERGE INTO target AS t
      USING source AS s ON t.id = s.id
      WHEN MATCHED THEN UPDATE SET t.value = s.value
    `);
    var res = alasql('SELECT * FROM target');
    assert.equal(res[0].value, 200);
    done();
  });
  
  it('should handle both matched and not matched', function(done) {
    alasql('INSERT INTO target VALUES (1, 100)');
    alasql('INSERT INTO source VALUES (1, 200), (2, 300)');
    var affected = 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 VALUES (s.id, s.value)
    `);
    var res = alasql('SELECT * FROM target ORDER BY id');
    assert.deepEqual(res, [{id: 1, value: 200}, {id: 2, value: 300}]);
    assert.equal(affected, 2); // 1 update + 1 insert
    done();
  });
});

UPSERT Tests

describe('INSERT ... ON CONFLICT (UPSERT)', function() {
  beforeEach(function() {
    alasql('CREATE TABLE users (id INT PRIMARY KEY, name STRING, email STRING)');
  });
  
  it('should insert when no conflict', function(done) {
    alasql(`
      INSERT INTO users (id, name, email) VALUES (1, 'John', '[email protected]')
      ON CONFLICT (id) DO NOTHING
    `);
    var res = alasql('SELECT * FROM users');
    assert.equal(res.length, 1);
    assert.equal(res[0].name, 'John');
    done();
  });
  
  it('should do nothing on conflict with DO NOTHING', function(done) {
    alasql('INSERT INTO users VALUES (1, "John", "[email protected]")');
    alasql(`
      INSERT INTO users (id, name, email) VALUES (1, 'Jane', '[email protected]')
      ON CONFLICT (id) DO NOTHING
    `);
    var res = alasql('SELECT * FROM users');
    assert.equal(res.length, 1);
    assert.equal(res[0].name, 'John'); // Unchanged
    done();
  });
  
  it('should update on conflict with DO UPDATE', function(done) {
    alasql('INSERT INTO users VALUES (1, "John", "[email protected]")');
    alasql(`
      INSERT INTO users (id, name, email) VALUES (1, 'John', '[email protected]')
      ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email
    `);
    var res = alasql('SELECT * FROM users');
    assert.equal(res[0].email, '[email protected]'); // Updated
    done();
  });
});

Implementation Priority

Medium - While UPSERT is a common pattern, it can be worked around with explicit INSERT/UPDATE logic. However, having native support would significantly improve usability.

References

Workaround (Current)

Users can achieve UPSERT behavior using separate INSERT and UPDATE:

// Check if exists
var exists = alasql('SELECT * FROM users WHERE id = ?', [id]);
if (exists.length > 0) {
  // Update
  alasql('UPDATE users SET email = ? WHERE id = ?', [email, id]);
} else {
  // Insert
  alasql('INSERT INTO users VALUES (?, ?, ?)', [id, name, email]);
}

This workaround is verbose and not transaction-safe in concurrent scenarios.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions