Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 181 additions & 4 deletions src/75merge.js
Comment thread
mathiasrw marked this conversation as resolved.
Comment thread
mathiasrw marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,186 @@ 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;
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 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;
});
}

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;
};

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);
}

var targetTable = db.tables[targettableid];
var sourceTable = db.tables[sourcetableid];
targetTable.dirty = true;

var counts = {insert: 0, update: 0, delete: 0};

// 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++;
}
}
}

// Process source rows (NOT MATCHED BY TARGET)
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 (alasql.options.autocommit && db.engineid) {
alasql.engines[db.engineid].saveTableData(databaseid, targettableid);
}

var res = counts.insert + counts.update + counts.delete;
if (cb) cb(res);
return res;
};
};

if (cb) res = cb(res);
return res;
yy.Merge.prototype.execute = function (databaseid, params, cb) {
return this.compile(databaseid)(params, cb);
};
26 changes: 22 additions & 4 deletions test/test236.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Copy Markdown
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
Copy Markdown
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.


// 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();
});

Expand Down
Loading