From a7ffca994ed5afdfea2e4ffdeeff52d1721ab913 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 06:20:06 +0000 Subject: [PATCH 1/8] Initial plan From 8fb3bee4f6618ac7553a9f1fbc495849b82d2d0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:04:19 +0000 Subject: [PATCH 2/8] Implement MERGE statement execution logic - 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> --- src/75merge.js | 256 +++++++++++++++++++++++++++++++++++++++++++++++- test/test236.js | 26 ++++- 2 files changed, 274 insertions(+), 8 deletions(-) diff --git a/src/75merge.js b/src/75merge.js index a23ca15ca8..7235a40f35 100755 --- a/src/75merge.js +++ b/src/75merge.js @@ -42,9 +42,257 @@ yy.Merge.prototype.toString = function () { return s; }; -yy.Merge.prototype.execute = function (databaseid, params, cb) { - var res = 1; +yy.Merge.prototype.compile = function (databaseid) { + var self = this; + + // Get database and table IDs + databaseid = self.into.databaseid || databaseid; + var db = alasql.databases[databaseid]; + var targettableid = self.into.tableid; + var sourcetableid = self.using.tableid; + + var targetTable = db.tables[targettableid]; + var sourceTable = db.tables[sourcetableid]; + + if (!targetTable) { + throw new Error("Target table '" + targettableid + "' not found"); + } + if (!sourceTable) { + throw new Error("Source table '" + sourcetableid + "' not found"); + } + + // Compile the ON condition + if (self.exists) { + self.existsfn = self.exists.map(function (ex) { + var nq = ex.compile(databaseid); + nq.query.modifier = 'RECORDSET'; + return nq; + }); + } + if (self.queries) { + self.queriesfn = self.queries.map(function (q) { + var nq = q.compile(databaseid); + nq.query.modifier = 'RECORDSET'; + return nq; + }); + } + + // Create aliases mapping for ON condition evaluation + // The ON condition needs to compare target and source rows + var targetAlias = self.into.as || targettableid; + var sourceAlias = self.using.as || sourcetableid; + + // Build the ON condition function that takes both target and source rows + // We create a combined record with aliases as property objects + var onConditionJS = self.on.toJS('rec', ''); + var onConditionFnStr = 'var rec = {' + + '"' + targetAlias + '": targetRow, ' + + '"' + sourceAlias + '": sourceRow' + + '}; return ' + onConditionJS + ';'; + var onConditionFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + onConditionFnStr).bind(self); + + // Compile each match clause + var compiledMatches = self.matches.map(function (match) { + var result = { + matched: match.matched, + bytarget: match.bytarget, + bysource: match.bysource, + action: match.action + }; + + // Compile condition expression if present + if (match.expr) { + var exprJS = match.expr.toJS('rec', ''); + var exprFnStr = 'var rec = {'; + if (match.matched) { + // For MATCHED: have both target and source + exprFnStr += '"' + targetAlias + '": targetRow, "' + sourceAlias + '": sourceRow'; + } else if (match.bytarget) { + // For NOT MATCHED BY TARGET: only source + exprFnStr += '"' + sourceAlias + '": sourceRow'; + } else if (match.bysource) { + // For NOT MATCHED BY SOURCE: only target + exprFnStr += '"' + targetAlias + '": targetRow'; + } + exprFnStr += '}; return ' + exprJS + ';'; + result.exprFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + exprFnStr).bind(self); + } + + // Compile action (UPDATE, INSERT, or DELETE) + if (match.action.update) { + // Compile UPDATE SET clauses + var updateJS = 'var rec = {' + + '"' + targetAlias + '": targetRow, ' + + '"' + sourceAlias + '": sourceRow' + + '}; '; + match.action.update.forEach(function (setCol) { + var exprJS = setCol.expression.toJS('rec', ''); + updateJS += 'targetRow["' + setCol.column.columnid + '"] = ' + exprJS + '; '; + }); + result.updateFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + updateJS).bind(self); + } else if (match.action.insert) { + // Compile INSERT clause + var insertJS = 'var newRow = {}; '; + if (match.action.columns && match.action.values && match.action.values[0]) { + // INSERT with explicit columns + var values = match.action.values[0]; + insertJS += 'var rec = {"' + sourceAlias + '": sourceRow}; '; + match.action.columns.forEach(function (col, idx) { + if (values[idx]) { + var valueJS = values[idx].toJS('rec', ''); + insertJS += 'newRow["' + col.columnid + '"] = ' + valueJS + '; '; + } + }); + } else if (match.action.defaultvalues) { + // INSERT DEFAULT VALUES + insertJS += 'newRow = ' + (targetTable.defaultfns ? '{' + targetTable.defaultfns + '}' : '{}') + '; '; + } + result.insertFn = new Function('sourceRow', 'params', 'alasql', 'var y;' + insertJS + 'return newRow;').bind(self); + } + // DELETE doesn't need compilation, just a flag + + return result; + }); + + // Main execution statement + var statement = function (params, cb) { + var db = alasql.databases[databaseid]; + + if (alasql.options.autocommit && db.engineid) { + alasql.engines[db.engineid].loadTableData(databaseid, targettableid); + alasql.engines[db.engineid].loadTableData(databaseid, sourcetableid); + } + + var targetTable = db.tables[targettableid]; + var sourceTable = db.tables[sourcetableid]; + + targetTable.dirty = true; + + var matchedCount = 0; + var insertedCount = 0; + var updatedCount = 0; + var deletedCount = 0; + + // Track which target rows matched + var matchedTargetIndices = []; + + // Process WHEN MATCHED and WHEN NOT MATCHED BY SOURCE + for (var i = 0; i < targetTable.data.length; i++) { + var targetRow = targetTable.data[i]; + var matched = false; + var sourceRow = null; + + // Find matching source row + for (var j = 0; j < sourceTable.data.length; j++) { + if (onConditionFn(targetRow, sourceTable.data[j], params, alasql)) { + matched = true; + sourceRow = sourceTable.data[j]; + matchedTargetIndices.push(i); + break; + } + } + + if (matched) { + // Process WHEN MATCHED clauses + for (var m = 0; m < compiledMatches.length; m++) { + var match = compiledMatches[m]; + if (match.matched && !match.bysource) { + // Check additional condition if present + if (!match.exprFn || match.exprFn(targetRow, sourceRow, params, alasql)) { + if (match.action.update) { + match.updateFn(targetRow, sourceRow, params, alasql); + updatedCount++; + } else if (match.action.delete) { + targetTable.data.splice(i, 1); + i--; // Adjust index after deletion + deletedCount++; + } + break; // Only first matching clause executes + } + } + } + } else { + // Process WHEN NOT MATCHED BY SOURCE clauses + for (var m = 0; m < compiledMatches.length; m++) { + var match = compiledMatches[m]; + if (!match.matched && match.bysource) { + // Check additional condition if present + if (!match.exprFn || match.exprFn(targetRow, null, params, alasql)) { + if (match.action.delete) { + targetTable.data.splice(i, 1); + i--; // Adjust index after deletion + deletedCount++; + } else if (match.action.update) { + match.updateFn(targetRow, null, params, alasql); + updatedCount++; + } + break; // Only first matching clause executes + } + } + } + } + } + + // Process WHEN NOT MATCHED (BY TARGET) clauses + // These are source rows that didn't match any target row + for (var j = 0; j < sourceTable.data.length; j++) { + var sourceRow = sourceTable.data[j]; + var matched = false; + + // Check if this source row matched any target row + for (var i = 0; i < targetTable.data.length; i++) { + if (onConditionFn(targetTable.data[i], sourceRow, params, alasql)) { + matched = true; + break; + } + } + + if (!matched) { + // Process WHEN NOT MATCHED clauses + for (var m = 0; m < compiledMatches.length; m++) { + var match = compiledMatches[m]; + if (!match.matched && match.bytarget) { + // Check additional condition if present + if (!match.exprFn || match.exprFn(null, sourceRow, params, alasql)) { + if (match.action.insert) { + var newRow = match.insertFn(sourceRow, params, alasql); + + // Apply default values if needed + if (targetTable.defaultfns) { + var defaultfn = new Function('r,db,params,alasql', + 'var defaults={' + targetTable.defaultfns + '};' + + 'for(var key in defaults){if(!(key in r)){r[key]=defaults[key]}}return r'); + defaultfn(newRow, db, params, alasql); + } + + if (targetTable.insert) { + targetTable.insert(newRow, false, false); + } else { + targetTable.data.push(newRow); + } + insertedCount++; + } + break; // Only first matching clause executes + } + } + } + } + } + + if (alasql.options.autocommit && db.engineid) { + alasql.engines[db.engineid].saveTableData(databaseid, targettableid); + } + + // Return total number of rows affected + var res = insertedCount + updatedCount + deletedCount; + + if (cb) cb(res); + return res; + }; + + return statement; +}; - if (cb) res = cb(res); - return res; +yy.Merge.prototype.execute = function (databaseid, params, cb) { + return this.compile(databaseid)(params, cb); }; diff --git a/test/test236.js b/test/test236.js index 615a802222..3e05ecf153 100644 --- a/test/test236.js +++ b/test/test236.js @@ -61,11 +61,29 @@ describe('Test 236 MERGE', function () { */ } - .toString() - .slice(14, -3); - /// console.log(alasql.parse(sql).toString()); + .toString(); + + // Extract SQL from comment + var start = sql.indexOf('/*') + 2; + var end = sql.lastIndexOf('*/'); + sql = sql.substring(start, end).trim(); + + // Execute the MERGE + var res = alasql(sql); + + // Verify result count (3 rows affected: 1 insert + 2 deletes) + assert.equal(res, 3); + + // Verify final table state + var target = alasql('SELECT * FROM [Target] ORDER BY EmployeeID'); + assert.deepEqual(target, [ + {EmployeeID: 100, EmployeeName: 'Mary'}, // Unchanged (not in source, not matching S%) + // 101 'Sara' deleted (not in source, matches S%) + // 102 'Stefano' deleted (not in source, matches S%) + // 103 'Bob' not inserted (in source but doesn't match S%) + {EmployeeID: 104, EmployeeName: 'Steve'}, // Inserted (not in target, matches S%) + ]); - // console.log(res); done(); }); From 08919ab071735d9c08793c29bf51eea0d5acb4b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:08:55 +0000 Subject: [PATCH 3/8] Add comprehensive MERGE statement tests - 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> --- test/test236-B.js | 230 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 test/test236-B.js diff --git a/test/test236-B.js b/test/test236-B.js new file mode 100644 index 0000000000..b02ad35a7b --- /dev/null +++ b/test/test236-B.js @@ -0,0 +1,230 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('Test 236-B MERGE - Additional scenarios', function () { + beforeEach(function () { + alasql('CREATE DATABASE IF NOT EXISTS test236b'); + alasql('USE test236b'); + }); + + afterEach(function () { + alasql('DROP DATABASE IF EXISTS test236b'); + }); + + it('1. Basic MERGE with INSERT and UPDATE', function () { + // Setup + alasql('CREATE TABLE [Target1] (id INT, name STRING, val INT)'); + alasql('CREATE TABLE [Source1] (id INT, name STRING, val INT)'); + alasql('INSERT INTO [Target1] VALUES (1, "Alice", 100), (2, "Bob", 200)'); + alasql('INSERT INTO [Source1] VALUES (2, "Bob", 250), (3, "Charlie", 300)'); + + // Execute MERGE + var res = alasql(` + MERGE INTO [Target1] AS t + USING [Source1] AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET t.val = s.val + WHEN NOT MATCHED THEN + INSERT (id, name, val) VALUES (s.id, s.name, s.val) + `); + + // Verify: 1 update + 1 insert = 2 affected + assert.equal(res, 2); + + // Verify final state + var result = alasql('SELECT * FROM [Target1] ORDER BY id'); + assert.deepEqual(result, [ + {id: 1, name: 'Alice', val: 100}, // Unchanged + {id: 2, name: 'Bob', val: 250}, // Updated + {id: 3, name: 'Charlie', val: 300}, // Inserted + ]); + }); + + it('2. MERGE with DELETE when matched', function () { + // Setup + alasql('CREATE TABLE [Target2] (id INT, status STRING)'); + alasql('CREATE TABLE [Source2] (id INT, action STRING)'); + alasql('INSERT INTO [Target2] VALUES (1, "active"), (2, "active"), (3, "active")'); + alasql('INSERT INTO [Source2] VALUES (2, "delete")'); + + // Execute MERGE - delete matched rows + var res = alasql(` + MERGE INTO [Target2] AS t + USING [Source2] AS s + ON t.id = s.id + WHEN MATCHED THEN DELETE + `); + + assert.equal(res, 1); + + var result = alasql('SELECT * FROM [Target2] ORDER BY id'); + assert.deepEqual(result, [ + {id: 1, status: 'active'}, + {id: 3, status: 'active'}, + ]); + }); + + it('3. MERGE with conditional INSERT (AND clause)', function () { + // Setup + alasql('CREATE TABLE [Target3] (id INT, name STRING)'); + alasql('CREATE TABLE [Source3] (id INT, name STRING, priority INT)'); + alasql('INSERT INTO [Target3] VALUES (1, "Alice")'); + alasql('INSERT INTO [Source3] VALUES (2, "Bob", 1), (3, "Charlie", 5), (4, "David", 10)'); + + // Only insert if priority >= 5 + var res = alasql(` + MERGE INTO [Target3] AS t + USING [Source3] AS s + ON t.id = s.id + WHEN NOT MATCHED AND s.priority >= 5 THEN + INSERT (id, name) VALUES (s.id, s.name) + `); + + // Only 2 inserts (Charlie and David have priority >= 5) + assert.equal(res, 2); + + var result = alasql('SELECT * FROM [Target3] ORDER BY id'); + assert.deepEqual(result, [ + {id: 1, name: 'Alice'}, + {id: 3, name: 'Charlie'}, + {id: 4, name: 'David'}, + ]); + }); + + it('4. MERGE with DELETE BY SOURCE', function () { + // Setup + alasql('CREATE TABLE [Target4] (id INT, name STRING)'); + alasql('CREATE TABLE [Source4] (id INT, name STRING)'); + alasql('INSERT INTO [Target4] VALUES (1, "Alice"), (2, "Bob"), (3, "Charlie")'); + alasql('INSERT INTO [Source4] VALUES (2, "Bob")'); + + // Delete from target if not in source + // Note: BY SOURCE with DELETE requires an AND condition in the grammar + var res = alasql(` + MERGE INTO [Target4] AS t + USING [Source4] AS s + ON t.id = s.id + WHEN NOT MATCHED BY SOURCE AND t.id > 0 THEN DELETE + `); + + // 2 deletes (Alice and Charlie not in source) + assert.equal(res, 2); + + var result = alasql('SELECT * FROM [Target4] ORDER BY id'); + assert.deepEqual(result, [{id: 2, name: 'Bob'}]); + }); + + it('5. MERGE with multiple WHEN clauses', function () { + // Setup + alasql('CREATE TABLE [Inventory] (product_id INT, stock INT)'); + alasql('CREATE TABLE [Shipment] (product_id INT, quantity INT)'); + alasql('INSERT INTO [Inventory] VALUES (1, 100), (2, 50)'); + alasql('INSERT INTO [Shipment] VALUES (2, 25), (3, 75), (4, 0)'); + + // Complex merge with multiple conditions + var res = 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 NOT MATCHED AND ship.quantity > 0 THEN + INSERT (product_id, stock) VALUES (ship.product_id, ship.quantity) + `); + + // 1 update (product 2) + 1 insert (product 3) = 2 + // Product 4 not affected (quantity = 0) + assert.equal(res, 2); + + var result = alasql('SELECT * FROM [Inventory] ORDER BY product_id'); + assert.deepEqual(result, [ + {product_id: 1, stock: 100}, // Unchanged + {product_id: 2, stock: 75}, // Updated (50 + 25) + {product_id: 3, stock: 75}, // Inserted + ]); + }); + + it('6. MERGE with no matches', function () { + // Setup - no overlapping IDs + alasql('CREATE TABLE [Target6] (id INT, val INT)'); + alasql('CREATE TABLE [Source6] (id INT, val INT)'); + alasql('INSERT INTO [Target6] VALUES (1, 100), (2, 200)'); + alasql('INSERT INTO [Source6] VALUES (3, 300), (4, 400)'); + + var res = alasql(` + MERGE INTO [Target6] AS t + USING [Source6] AS s + ON t.id = s.id + WHEN MATCHED THEN UPDATE SET t.val = s.val + WHEN NOT MATCHED THEN INSERT (id, val) VALUES (s.id, s.val) + `); + + // 2 inserts, no updates + assert.equal(res, 2); + + var result = alasql('SELECT * FROM [Target6] ORDER BY id'); + assert.deepEqual(result, [ + {id: 1, val: 100}, + {id: 2, val: 200}, + {id: 3, val: 300}, + {id: 4, val: 400}, + ]); + }); + + it('7. MERGE with all matches', function () { + // Setup - all IDs match + alasql('CREATE TABLE [Target7] (id INT, val INT)'); + alasql('CREATE TABLE [Source7] (id INT, val INT)'); + alasql('INSERT INTO [Target7] VALUES (1, 100), (2, 200), (3, 300)'); + alasql('INSERT INTO [Source7] VALUES (1, 111), (2, 222), (3, 333)'); + + var res = alasql(` + MERGE INTO [Target7] AS t + USING [Source7] AS s + ON t.id = s.id + WHEN MATCHED THEN UPDATE SET t.val = s.val + WHEN NOT MATCHED THEN INSERT (id, val) VALUES (s.id, s.val) + `); + + // 3 updates, no inserts + assert.equal(res, 3); + + var result = alasql('SELECT * FROM [Target7] ORDER BY id'); + assert.deepEqual(result, [ + {id: 1, val: 111}, + {id: 2, val: 222}, + {id: 3, val: 333}, + ]); + }); + + it('8. MERGE with complex ON condition', function () { + // Setup + alasql('CREATE TABLE [Target8] (id INT, category STRING, val INT)'); + alasql('CREATE TABLE [Source8] (id INT, category STRING, val INT)'); + alasql('INSERT INTO [Target8] VALUES (1, "A", 100), (2, "B", 200)'); + alasql('INSERT INTO [Source8] VALUES (1, "A", 150), (1, "B", 175), (3, "C", 300)'); + + // Match on both id AND category + var res = alasql(` + MERGE INTO [Target8] AS t + USING [Source8] AS s + ON t.id = s.id AND t.category = s.category + WHEN MATCHED THEN UPDATE SET t.val = s.val + WHEN NOT MATCHED THEN INSERT (id, category, val) VALUES (s.id, s.category, s.val) + `); + + // 1 update (1, A) + 2 inserts (1, B) and (3, C) = 3 + assert.equal(res, 3); + + var result = alasql('SELECT * FROM [Target8] ORDER BY id, category'); + assert.deepEqual(result, [ + {id: 1, category: 'A', val: 150}, // Updated + {id: 1, category: 'B', val: 175}, // Inserted + {id: 2, category: 'B', val: 200}, // Unchanged + {id: 3, category: 'C', val: 300}, // Inserted + ]); + }); +}); From 37035390d69c5e680fedb94fa11dcc00f553d51b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:06:28 +0000 Subject: [PATCH 4/8] Fix code review issues in MERGE implementation - 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> --- src/75merge.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/75merge.js b/src/75merge.js index 7235a40f35..b155d22e34 100755 --- a/src/75merge.js +++ b/src/75merge.js @@ -168,14 +168,10 @@ yy.Merge.prototype.compile = function (databaseid) { targetTable.dirty = true; - var matchedCount = 0; var insertedCount = 0; var updatedCount = 0; var deletedCount = 0; - // Track which target rows matched - var matchedTargetIndices = []; - // Process WHEN MATCHED and WHEN NOT MATCHED BY SOURCE for (var i = 0; i < targetTable.data.length; i++) { var targetRow = targetTable.data[i]; @@ -187,7 +183,6 @@ yy.Merge.prototype.compile = function (databaseid) { if (onConditionFn(targetRow, sourceTable.data[j], params, alasql)) { matched = true; sourceRow = sourceTable.data[j]; - matchedTargetIndices.push(i); break; } } @@ -222,10 +217,9 @@ yy.Merge.prototype.compile = function (databaseid) { targetTable.data.splice(i, 1); i--; // Adjust index after deletion deletedCount++; - } else if (match.action.update) { - match.updateFn(targetRow, null, params, alasql); - updatedCount++; } + // Note: UPDATE BY SOURCE is not semantically valid since there's no source row to update from + // The grammar allows it with an AND condition, but it would require the UPDATE to not reference source columns break; // Only first matching clause executes } } From 1da241acbdac0a995d4ebc407ee077a6bbe4d80c Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Sat, 3 Jan 2026 04:04:30 +1100 Subject: [PATCH 5/8] Add test2407.js file --- test/{test236-B.js => test2407.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test236-B.js => test2407.js} (100%) diff --git a/test/test236-B.js b/test/test2407.js similarity index 100% rename from test/test236-B.js rename to test/test2407.js From ddd567ec679f937e74d1e6523027863bd6bbd6a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:09:01 +0000 Subject: [PATCH 6/8] Refactor MERGE implementation for elegance and brevity - 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> --- src/75merge.js | 233 ++++++++++++++++++------------------------------- 1 file changed, 84 insertions(+), 149 deletions(-) diff --git a/src/75merge.js b/src/75merge.js index b155d22e34..6251745ebb 100755 --- a/src/75merge.js +++ b/src/75merge.js @@ -44,24 +44,17 @@ yy.Merge.prototype.toString = function () { yy.Merge.prototype.compile = function (databaseid) { var self = this; - - // Get database and table IDs databaseid = self.into.databaseid || databaseid; var db = alasql.databases[databaseid]; var targettableid = self.into.tableid; var sourcetableid = self.using.tableid; - var targetTable = db.tables[targettableid]; var sourceTable = db.tables[sourcetableid]; - if (!targetTable) { - throw new Error("Target table '" + targettableid + "' not found"); - } - if (!sourceTable) { - throw new Error("Source table '" + sourcetableid + "' not found"); - } + if (!targetTable) throw new Error("Target table '" + targettableid + "' not found"); + if (!sourceTable) throw new Error("Source table '" + sourcetableid + "' not found"); - // Compile the ON condition + // Compile exists/queries if present if (self.exists) { self.existsfn = self.exists.map(function (ex) { var nq = ex.compile(databaseid); @@ -77,21 +70,22 @@ yy.Merge.prototype.compile = function (databaseid) { }); } - // Create aliases mapping for ON condition evaluation - // The ON condition needs to compare target and source rows var targetAlias = self.into.as || targettableid; var sourceAlias = self.using.as || sourcetableid; - // Build the ON condition function that takes both target and source rows - // We create a combined record with aliases as property objects - var onConditionJS = self.on.toJS('rec', ''); - var onConditionFnStr = 'var rec = {' + - '"' + targetAlias + '": targetRow, ' + - '"' + sourceAlias + '": sourceRow' + - '}; return ' + onConditionJS + ';'; - var onConditionFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + onConditionFnStr).bind(self); + // Helper to build context record + var buildContext = function (includeTarget, includeSource) { + var parts = []; + if (includeTarget) parts.push('"' + targetAlias + '": targetRow'); + if (includeSource) parts.push('"' + sourceAlias + '": sourceRow'); + return 'var rec = {' + parts.join(', ') + '};'; + }; + + // Compile ON condition + var onConditionFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', + 'var y;' + buildContext(true, true) + ' return ' + self.on.toJS('rec', '') + ';').bind(self); - // Compile each match clause + // Compile match clauses var compiledMatches = self.matches.map(function (match) { var result = { matched: match.matched, @@ -100,62 +94,57 @@ yy.Merge.prototype.compile = function (databaseid) { action: match.action }; - // Compile condition expression if present + // Compile condition expression if (match.expr) { - var exprJS = match.expr.toJS('rec', ''); - var exprFnStr = 'var rec = {'; - if (match.matched) { - // For MATCHED: have both target and source - exprFnStr += '"' + targetAlias + '": targetRow, "' + sourceAlias + '": sourceRow'; - } else if (match.bytarget) { - // For NOT MATCHED BY TARGET: only source - exprFnStr += '"' + sourceAlias + '": sourceRow'; - } else if (match.bysource) { - // For NOT MATCHED BY SOURCE: only target - exprFnStr += '"' + targetAlias + '": targetRow'; - } - exprFnStr += '}; return ' + exprJS + ';'; - result.exprFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + exprFnStr).bind(self); + var ctx = buildContext(match.matched || match.bysource, match.matched || match.bytarget); + result.exprFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', + 'var y;' + ctx + ' return ' + match.expr.toJS('rec', '') + ';').bind(self); } - // Compile action (UPDATE, INSERT, or DELETE) + // Compile actions if (match.action.update) { - // Compile UPDATE SET clauses - var updateJS = 'var rec = {' + - '"' + targetAlias + '": targetRow, ' + - '"' + sourceAlias + '": sourceRow' + - '}; '; + var updateJS = buildContext(true, true); match.action.update.forEach(function (setCol) { - var exprJS = setCol.expression.toJS('rec', ''); - updateJS += 'targetRow["' + setCol.column.columnid + '"] = ' + exprJS + '; '; + updateJS += 'targetRow["' + setCol.column.columnid + '"] = ' + setCol.expression.toJS('rec', '') + '; '; }); result.updateFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + updateJS).bind(self); } else if (match.action.insert) { - // Compile INSERT clause var insertJS = 'var newRow = {}; '; if (match.action.columns && match.action.values && match.action.values[0]) { - // INSERT with explicit columns - var values = match.action.values[0]; - insertJS += 'var rec = {"' + sourceAlias + '": sourceRow}; '; + insertJS += buildContext(false, true); match.action.columns.forEach(function (col, idx) { - if (values[idx]) { - var valueJS = values[idx].toJS('rec', ''); - insertJS += 'newRow["' + col.columnid + '"] = ' + valueJS + '; '; + if (match.action.values[0][idx]) { + insertJS += 'newRow["' + col.columnid + '"] = ' + match.action.values[0][idx].toJS('rec', '') + '; '; } }); } else if (match.action.defaultvalues) { - // INSERT DEFAULT VALUES insertJS += 'newRow = ' + (targetTable.defaultfns ? '{' + targetTable.defaultfns + '}' : '{}') + '; '; } result.insertFn = new Function('sourceRow', 'params', 'alasql', 'var y;' + insertJS + 'return newRow;').bind(self); } - // DELETE doesn't need compilation, just a flag return result; }); - // Main execution statement - var statement = function (params, cb) { + // Helper to execute first matching clause + var executeMatch = function (matches, targetRow, sourceRow, params) { + for (var m = 0; m < matches.length; m++) { + var match = matches[m]; + if (match.exprFn && !match.exprFn(targetRow, sourceRow, params, alasql)) continue; + + if (match.action.update) { + match.updateFn(targetRow, sourceRow, params, alasql); + return {type: 'update'}; + } else if (match.action.delete) { + return {type: 'delete'}; + } else if (match.action.insert) { + return {type: 'insert', row: match.insertFn(sourceRow, params, alasql)}; + } + } + return null; + }; + + return function (params, cb) { var db = alasql.databases[databaseid]; if (alasql.options.autocommit && db.engineid) { @@ -165,110 +154,60 @@ yy.Merge.prototype.compile = function (databaseid) { var targetTable = db.tables[targettableid]; var sourceTable = db.tables[sourcetableid]; - targetTable.dirty = true; - var insertedCount = 0; - var updatedCount = 0; - var deletedCount = 0; + var counts = {insert: 0, update: 0, delete: 0}; - // Process WHEN MATCHED and WHEN NOT MATCHED BY SOURCE + // Process target rows (MATCHED and NOT MATCHED BY SOURCE) for (var i = 0; i < targetTable.data.length; i++) { var targetRow = targetTable.data[i]; - var matched = false; - var sourceRow = null; + var sourceRow = sourceTable.data.find(function (s) { + return onConditionFn(targetRow, s, params, alasql); + }); - // Find matching source row - for (var j = 0; j < sourceTable.data.length; j++) { - if (onConditionFn(targetRow, sourceTable.data[j], params, alasql)) { - matched = true; - sourceRow = sourceTable.data[j]; - break; - } - } + var matchType = sourceRow ? 'matched' : 'bysource'; + var matches = compiledMatches.filter(function (m) { + return sourceRow ? (m.matched && !m.bysource) : (!m.matched && m.bysource); + }); - if (matched) { - // Process WHEN MATCHED clauses - for (var m = 0; m < compiledMatches.length; m++) { - var match = compiledMatches[m]; - if (match.matched && !match.bysource) { - // Check additional condition if present - if (!match.exprFn || match.exprFn(targetRow, sourceRow, params, alasql)) { - if (match.action.update) { - match.updateFn(targetRow, sourceRow, params, alasql); - updatedCount++; - } else if (match.action.delete) { - targetTable.data.splice(i, 1); - i--; // Adjust index after deletion - deletedCount++; - } - break; // Only first matching clause executes - } - } - } - } else { - // Process WHEN NOT MATCHED BY SOURCE clauses - for (var m = 0; m < compiledMatches.length; m++) { - var match = compiledMatches[m]; - if (!match.matched && match.bysource) { - // Check additional condition if present - if (!match.exprFn || match.exprFn(targetRow, null, params, alasql)) { - if (match.action.delete) { - targetTable.data.splice(i, 1); - i--; // Adjust index after deletion - deletedCount++; - } - // Note: UPDATE BY SOURCE is not semantically valid since there's no source row to update from - // The grammar allows it with an AND condition, but it would require the UPDATE to not reference source columns - break; // Only first matching clause executes - } - } + var result = executeMatch(matches, targetRow, sourceRow, params); + if (result) { + if (result.type === 'delete') { + targetTable.data.splice(i--, 1); + counts.delete++; + } else if (result.type === 'update') { + counts.update++; } } } - // Process WHEN NOT MATCHED (BY TARGET) clauses - // These are source rows that didn't match any target row + // Process source rows (NOT MATCHED BY TARGET) for (var j = 0; j < sourceTable.data.length; j++) { var sourceRow = sourceTable.data[j]; - var matched = false; - - // Check if this source row matched any target row - for (var i = 0; i < targetTable.data.length; i++) { - if (onConditionFn(targetTable.data[i], sourceRow, params, alasql)) { - matched = true; - break; - } - } + var hasMatch = targetTable.data.some(function (t) { + return onConditionFn(t, sourceRow, params, alasql); + }); - if (!matched) { - // Process WHEN NOT MATCHED clauses - for (var m = 0; m < compiledMatches.length; m++) { - var match = compiledMatches[m]; - if (!match.matched && match.bytarget) { - // Check additional condition if present - if (!match.exprFn || match.exprFn(null, sourceRow, params, alasql)) { - if (match.action.insert) { - var newRow = match.insertFn(sourceRow, params, alasql); - - // Apply default values if needed - if (targetTable.defaultfns) { - var defaultfn = new Function('r,db,params,alasql', - 'var defaults={' + targetTable.defaultfns + '};' + - 'for(var key in defaults){if(!(key in r)){r[key]=defaults[key]}}return r'); - defaultfn(newRow, db, params, alasql); - } - - if (targetTable.insert) { - targetTable.insert(newRow, false, false); - } else { - targetTable.data.push(newRow); - } - insertedCount++; - } - break; // Only first matching clause executes - } + if (!hasMatch) { + var matches = compiledMatches.filter(function (m) { + return !m.matched && m.bytarget; + }); + + var result = executeMatch(matches, null, sourceRow, params); + if (result && result.type === 'insert') { + var newRow = result.row; + if (targetTable.defaultfns) { + var defaults = new Function('r,db,params,alasql', + 'var defaults={' + targetTable.defaultfns + '};' + + 'for(var key in defaults){if(!(key in r)){r[key]=defaults[key]}}return r'); + defaults(newRow, db, params, alasql); } + if (targetTable.insert) { + targetTable.insert(newRow, false, false); + } else { + targetTable.data.push(newRow); + } + counts.insert++; } } } @@ -277,14 +216,10 @@ yy.Merge.prototype.compile = function (databaseid) { alasql.engines[db.engineid].saveTableData(databaseid, targettableid); } - // Return total number of rows affected - var res = insertedCount + updatedCount + deletedCount; - + var res = counts.insert + counts.update + counts.delete; if (cb) cb(res); return res; }; - - return statement; }; yy.Merge.prototype.execute = function (databaseid, params, cb) { From a931227658d15e974a3b1ace4e417aa5d903556b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 09:22:56 +0000 Subject: [PATCH 7/8] Dramatically simplify MERGE implementation - 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> --- src/75merge.js | 206 ++++++++++++++++-------------------------------- test/test236.js | 13 ++- 2 files changed, 74 insertions(+), 145 deletions(-) diff --git a/src/75merge.js b/src/75merge.js index 6251745ebb..ae4bb3c708 100755 --- a/src/75merge.js +++ b/src/75merge.js @@ -48,105 +48,30 @@ yy.Merge.prototype.compile = function (databaseid) { var db = alasql.databases[databaseid]; var targettableid = self.into.tableid; var sourcetableid = self.using.tableid; - var targetTable = db.tables[targettableid]; - var sourceTable = db.tables[sourcetableid]; - if (!targetTable) throw new Error("Target table '" + targettableid + "' not found"); - if (!sourceTable) throw new Error("Source table '" + sourcetableid + "' not found"); + if (!db.tables[targettableid]) throw new Error("Target table '" + targettableid + "' not found"); + if (!db.tables[sourcetableid]) throw new Error("Source table '" + sourcetableid + "' not found"); - // Compile exists/queries if present - if (self.exists) { - self.existsfn = self.exists.map(function (ex) { - var nq = ex.compile(databaseid); - nq.query.modifier = 'RECORDSET'; - return nq; - }); - } - if (self.queries) { - self.queriesfn = self.queries.map(function (q) { - var nq = q.compile(databaseid); - nq.query.modifier = 'RECORDSET'; - return nq; - }); - } + if (self.exists) self.existsfn = self.exists.map(function (ex) { + var nq = ex.compile(databaseid); nq.query.modifier = 'RECORDSET'; return nq; + }); + if (self.queries) self.queriesfn = self.queries.map(function (q) { + var nq = q.compile(databaseid); nq.query.modifier = 'RECORDSET'; return nq; + }); var targetAlias = self.into.as || targettableid; var sourceAlias = self.using.as || sourcetableid; - // Helper to build context record - var buildContext = function (includeTarget, includeSource) { - var parts = []; - if (includeTarget) parts.push('"' + targetAlias + '": targetRow'); - if (includeSource) parts.push('"' + sourceAlias + '": sourceRow'); - return 'var rec = {' + parts.join(', ') + '};'; - }; - - // Compile ON condition - var onConditionFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', - 'var y;' + buildContext(true, true) + ' return ' + self.on.toJS('rec', '') + ';').bind(self); - - // Compile match clauses - var compiledMatches = self.matches.map(function (match) { - var result = { - matched: match.matched, - bytarget: match.bytarget, - bysource: match.bysource, - action: match.action - }; - - // Compile condition expression - if (match.expr) { - var ctx = buildContext(match.matched || match.bysource, match.matched || match.bytarget); - result.exprFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', - 'var y;' + ctx + ' return ' + match.expr.toJS('rec', '') + ';').bind(self); - } - - // Compile actions - if (match.action.update) { - var updateJS = buildContext(true, true); - match.action.update.forEach(function (setCol) { - updateJS += 'targetRow["' + setCol.column.columnid + '"] = ' + setCol.expression.toJS('rec', '') + '; '; - }); - result.updateFn = new Function('targetRow', 'sourceRow', 'params', 'alasql', 'var y;' + updateJS).bind(self); - } else if (match.action.insert) { - var insertJS = 'var newRow = {}; '; - if (match.action.columns && match.action.values && match.action.values[0]) { - insertJS += buildContext(false, true); - match.action.columns.forEach(function (col, idx) { - if (match.action.values[0][idx]) { - insertJS += 'newRow["' + col.columnid + '"] = ' + match.action.values[0][idx].toJS('rec', '') + '; '; - } - }); - } else if (match.action.defaultvalues) { - insertJS += 'newRow = ' + (targetTable.defaultfns ? '{' + targetTable.defaultfns + '}' : '{}') + '; '; - } - result.insertFn = new Function('sourceRow', 'params', 'alasql', 'var y;' + insertJS + 'return newRow;').bind(self); - } - - return result; - }); - - // Helper to execute first matching clause - var executeMatch = function (matches, targetRow, sourceRow, params) { - for (var m = 0; m < matches.length; m++) { - var match = matches[m]; - if (match.exprFn && !match.exprFn(targetRow, sourceRow, params, alasql)) continue; - - if (match.action.update) { - match.updateFn(targetRow, sourceRow, params, alasql); - return {type: 'update'}; - } else if (match.action.delete) { - return {type: 'delete'}; - } else if (match.action.insert) { - return {type: 'insert', row: match.insertFn(sourceRow, params, alasql)}; - } - } - return null; + // Helper to evaluate expressions in context + var evalInContext = function (expr, targetRow, sourceRow, params) { + var rec = {}; + if (targetRow) rec[targetAlias] = targetRow; + if (sourceRow) rec[sourceAlias] = sourceRow; + return new Function('rec', 'params', 'alasql', 'var y; return ' + expr.toJS('rec', ''))(rec, params, alasql); }; return function (params, cb) { var db = alasql.databases[databaseid]; - if (alasql.options.autocommit && db.engineid) { alasql.engines[db.engineid].loadTableData(databaseid, targettableid); alasql.engines[db.engineid].loadTableData(databaseid, sourcetableid); @@ -155,60 +80,66 @@ yy.Merge.prototype.compile = function (databaseid) { var targetTable = db.tables[targettableid]; var sourceTable = db.tables[sourcetableid]; targetTable.dirty = true; + var count = 0; - var counts = {insert: 0, update: 0, delete: 0}; + // Check if target and source rows match + var rowsMatch = function (t, s) { + return evalInContext(self.on, t, s, params); + }; - // Process target rows (MATCHED and NOT MATCHED BY SOURCE) - for (var i = 0; i < targetTable.data.length; i++) { - var targetRow = targetTable.data[i]; - var sourceRow = sourceTable.data.find(function (s) { - return onConditionFn(targetRow, s, params, alasql); - }); - - var matchType = sourceRow ? 'matched' : 'bysource'; - var matches = compiledMatches.filter(function (m) { - return sourceRow ? (m.matched && !m.bysource) : (!m.matched && m.bysource); - }); - - var result = executeMatch(matches, targetRow, sourceRow, params); - if (result) { - if (result.type === 'delete') { - targetTable.data.splice(i--, 1); - counts.delete++; - } else if (result.type === 'update') { - counts.update++; + // Execute first applicable action for a row + var executeAction = function (targetRow, sourceRow, isMatched, isBySource) { + for (var m = 0; m < self.matches.length; m++) { + var match = self.matches[m]; + if (match.matched !== isMatched) continue; + if (isMatched && match.bysource) continue; + if (!isMatched && ((isBySource && !match.bysource) || (!isBySource && !match.bytarget))) continue; + if (match.expr && !evalInContext(match.expr, targetRow, sourceRow, params)) continue; + + var action = match.action; + if (action.delete) return 'delete'; + if (action.update) { + var rec = {}; rec[targetAlias] = targetRow; rec[sourceAlias] = sourceRow; + action.update.forEach(function (set) { + targetRow[set.column.columnid] = evalInContext(set.expression, targetRow, sourceRow, params); + }); + return 'update'; + } + if (action.insert) { + var newRow = {}; + if (action.columns && action.values && action.values[0]) { + action.columns.forEach(function (col, i) { + if (action.values[0][i]) newRow[col.columnid] = evalInContext(action.values[0][i], null, sourceRow, params); + }); + } else if (action.defaultvalues && targetTable.defaultfns) { + eval('newRow = {' + targetTable.defaultfns + '}'); + } + if (targetTable.defaultfns) { + eval('var defaults = {' + targetTable.defaultfns + '}'); + for (var k in defaults) if (!(k in newRow)) newRow[k] = defaults[k]; + } + if (targetTable.insert) targetTable.insert(newRow, false, false); + else targetTable.data.push(newRow); + return 'insert'; } } + return null; + }; + + // Process target rows + for (var i = 0; i < targetTable.data.length; i++) { + var targetRow = targetTable.data[i]; + var sourceRow = sourceTable.data.find(function (s) { return rowsMatch(targetRow, s); }); + var action = executeAction(targetRow, sourceRow, !!sourceRow, !sourceRow); + if (action === 'delete') { targetTable.data.splice(i--, 1); count++; } + else if (action === 'update') count++; } - // Process source rows (NOT MATCHED BY TARGET) + // Process unmatched source rows for (var j = 0; j < sourceTable.data.length; j++) { var sourceRow = sourceTable.data[j]; - var hasMatch = targetTable.data.some(function (t) { - return onConditionFn(t, sourceRow, params, alasql); - }); - - if (!hasMatch) { - var matches = compiledMatches.filter(function (m) { - return !m.matched && m.bytarget; - }); - - var result = executeMatch(matches, null, sourceRow, params); - if (result && result.type === 'insert') { - var newRow = result.row; - if (targetTable.defaultfns) { - var defaults = new Function('r,db,params,alasql', - 'var defaults={' + targetTable.defaultfns + '};' + - 'for(var key in defaults){if(!(key in r)){r[key]=defaults[key]}}return r'); - defaults(newRow, db, params, alasql); - } - if (targetTable.insert) { - targetTable.insert(newRow, false, false); - } else { - targetTable.data.push(newRow); - } - counts.insert++; - } + if (!targetTable.data.some(function (t) { return rowsMatch(t, sourceRow); })) { + if (executeAction(null, sourceRow, false, false) === 'insert') count++; } } @@ -216,9 +147,8 @@ yy.Merge.prototype.compile = function (databaseid) { alasql.engines[db.engineid].saveTableData(databaseid, targettableid); } - var res = counts.insert + counts.update + counts.delete; - if (cb) cb(res); - return res; + if (cb) cb(count); + return count; }; }; diff --git a/test/test236.js b/test/test236.js index 3e05ecf153..7fdacd8d04 100644 --- a/test/test236.js +++ b/test/test236.js @@ -46,7 +46,8 @@ describe('Test 236 MERGE', function () { }); it('2. Merge', function (done) { - var sql = function () { + // SQL is embedded in a multiline comment within a function + var sqlFunc = function () { /* MERGE [Target] AS T @@ -60,13 +61,11 @@ describe('Test 236 MERGE', function () { THEN DELETE */ - } - .toString(); + }; - // Extract SQL from comment - var start = sql.indexOf('/*') + 2; - var end = sql.lastIndexOf('*/'); - sql = sql.substring(start, end).trim(); + // Extract SQL from the function's comment block + var funcStr = sqlFunc.toString(); + var sql = funcStr.substring(funcStr.indexOf('/*') + 2, funcStr.lastIndexOf('*/')).trim(); // Execute the MERGE var res = alasql(sql); From 5d4b63b8cc891801f5fdc6f1d189b247a1cca17c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:57:32 +0000 Subject: [PATCH 8/8] Modernize test236.js to use template literals instead of ES3 function comments - Replace old function comment trick with modern backtick template strings - Simpler and more readable code - All 26 MERGE tests still passing Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com> --- test/test236.js | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/test/test236.js b/test/test236.js index 7fdacd8d04..aaa0aa0ecd 100644 --- a/test/test236.js +++ b/test/test236.js @@ -46,26 +46,17 @@ describe('Test 236 MERGE', function () { }); it('2. Merge', function (done) { - // SQL is embedded in a multiline comment within a function - var sqlFunc = function () { - /* - - MERGE [Target] AS T - USING [Source] AS S - ON (T.EmployeeID = S.EmployeeID) - WHEN NOT MATCHED BY TARGET AND S.EmployeeName LIKE 'S%' - THEN INSERT(EmployeeID, EmployeeName) VALUES(S.EmployeeID, S.EmployeeName) - WHEN MATCHED - THEN UPDATE SET T.EmployeeName = S.EmployeeName - WHEN NOT MATCHED BY SOURCE AND T.EmployeeName LIKE 'S%' - THEN DELETE - - */ - }; - - // Extract SQL from the function's comment block - var funcStr = sqlFunc.toString(); - var sql = funcStr.substring(funcStr.indexOf('/*') + 2, funcStr.lastIndexOf('*/')).trim(); + var sql = ` + MERGE [Target] AS T + USING [Source] AS S + ON (T.EmployeeID = S.EmployeeID) + WHEN NOT MATCHED BY TARGET AND S.EmployeeName LIKE 'S%' + THEN INSERT(EmployeeID, EmployeeName) VALUES(S.EmployeeID, S.EmployeeName) + WHEN MATCHED + THEN UPDATE SET T.EmployeeName = S.EmployeeName + WHEN NOT MATCHED BY SOURCE AND T.EmployeeName LIKE 'S%' + THEN DELETE + `; // Execute the MERGE var res = alasql(sql);