-
Notifications
You must be signed in to change notification settings - Fork 693
Implement CASCADE behavior enforcement for FOREIGN KEY constraints #2337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 5 commits
a88fabe
6db5c31
a16cfef
f1b0751
d668f43
b53bc38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,90 @@ yy.Delete.prototype.toString = function () { | |
| return s; | ||
| }; | ||
|
|
||
| // Helper function to apply CASCADE delete operations recursively | ||
| function applyCascadeDeletes(db, databaseid, tableid, row, params, alasql) { | ||
| // Check all child tables that reference this table | ||
| for (var childTableId in db.tables) { | ||
| var childTable = db.tables[childTableId]; | ||
| if (!childTable.foreignKeys) continue; | ||
|
|
||
| childTable.foreignKeys.forEach(function(fk) { | ||
| // Check if this foreign key references the table we're deleting from | ||
| if (fk.fktable === tableid && fk.fkdatabase === databaseid) { | ||
| // Build the parent key values | ||
| var parentKeyValues = fk.fkcolumns.map(function(col) { | ||
| return row[col]; | ||
| }); | ||
|
|
||
| // Find matching child rows - store row references, not indices | ||
| var childRowsToProcess = []; | ||
| for (var j = 0; j < childTable.data.length; j++) { | ||
| var childRow = childTable.data[j]; | ||
| var matches = true; | ||
| for (var k = 0; k < fk.columns.length; k++) { | ||
| if (childRow[fk.columns[k]] !== parentKeyValues[k]) { | ||
| matches = false; | ||
| break; | ||
| } | ||
| } | ||
| if (matches) { | ||
| childRowsToProcess.push(childRow); | ||
| } | ||
| } | ||
|
|
||
| // Apply the appropriate action based on ondelete | ||
| if (childRowsToProcess.length > 0) { | ||
| if (fk.ondelete === 'RESTRICT' || fk.ondelete === 'NO ACTION') { | ||
| throw new Error('Cannot delete row from table "' + tableid + '" because it has dependent rows in table "' + childTableId + '"'); | ||
| } else if (fk.ondelete === 'CASCADE') { | ||
| // First, recursively cascade delete for each child row | ||
| childRowsToProcess.forEach(function(childRow) { | ||
| applyCascadeDeletes(db, databaseid, childTableId, childRow, params, alasql); | ||
| }); | ||
|
|
||
| // Then delete child rows - find current index for each row | ||
| childRowsToProcess.forEach(function(childRow) { | ||
| var idx = childTable.data.indexOf(childRow); | ||
| if (idx !== -1) { | ||
| if (childTable.delete) { | ||
| childTable.delete(idx, params, alasql); | ||
| } | ||
| childTable.data.splice(idx, 1); | ||
| } | ||
| }); | ||
| } else if (fk.ondelete === 'SET NULL') { | ||
| // Set foreign key columns to NULL | ||
| childRowsToProcess.forEach(function(childRow) { | ||
| fk.columns.forEach(function(col) { | ||
| var colDef = childTable.xcolumns[col]; | ||
| if (colDef && colDef.notnull) { | ||
| throw new Error('Cannot SET NULL on NOT NULL column "' + col + '" in table "' + childTableId + '"'); | ||
| } | ||
| childRow[col] = null; | ||
| }); | ||
| }); | ||
| } else if (fk.ondelete === 'SET DEFAULT') { | ||
| // Set foreign key columns to their default values | ||
| childRowsToProcess.forEach(function(childRow) { | ||
| fk.columns.forEach(function(col) { | ||
| // Find the column definition to get default value | ||
| var colDef = childTable.xcolumns[col]; | ||
| if (colDef && colDef.default !== undefined) { | ||
| childRow[col] = colDef.default; | ||
| } else if (colDef && colDef.notnull) { | ||
| throw new Error('Cannot SET DEFAULT to NULL on NOT NULL column "' + col + '" in table "' + childTableId + '" without a DEFAULT value'); | ||
| } else { | ||
| childRow[col] = null; | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| yy.Delete.prototype.compile = function (databaseid) { | ||
| var self = this; | ||
| databaseid = this.table.databaseid || databaseid; | ||
|
|
@@ -80,22 +164,33 @@ yy.Delete.prototype.compile = function (databaseid) { | |
|
|
||
| var newtable = []; | ||
| var deletedRows = []; | ||
| var rowsToDelete = []; // Track rows to delete for CASCADE processing | ||
|
|
||
| for (var i = 0, ilen = table.data.length; i < ilen; i++) { | ||
| if (wherefn(table.data[i], params, alasql)) { | ||
| rowsToDelete.push(table.data[i]); | ||
| // Track deleted row for OUTPUT clause | ||
| if (self.output) { | ||
| deletedRows.push(cloneDeep(table.data[i])); | ||
| } | ||
| // Check for transaction - if it is not possible then return all back | ||
| if (table.delete) { | ||
| table.delete(i, params, alasql); | ||
| } else { | ||
| // Simply do not push | ||
| } | ||
| } else { | ||
| newtable.push(table.data[i]); | ||
| } | ||
| } | ||
|
|
||
| // Process CASCADE operations before actually deleting | ||
| rowsToDelete.forEach(function(row) { | ||
| applyCascadeDeletes(db, databaseid, tableid, row, params, alasql); | ||
| }); | ||
|
|
||
| // Call table.delete for each row to update indices before setting newtable | ||
| rowsToDelete.forEach(function(row) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do this only if there are indexes?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! Optimized to only call Commit b53bc38 |
||
| var idx = table.data.indexOf(row); | ||
| if (idx !== -1 && table.delete) { | ||
| table.delete(idx, params, alasql); | ||
| } | ||
| }); | ||
|
|
||
| table.data = newtable; | ||
|
|
||
| // Trigger prevent functionality | ||
|
|
@@ -164,6 +259,14 @@ yy.Delete.prototype.compile = function (databaseid) { | |
| return cloneDeep(row); | ||
| }); | ||
| } | ||
|
|
||
| // Copy all rows to delete | ||
| var rowsToDelete = table.data.slice(); | ||
|
|
||
| // Process CASCADE operations for all rows before deleting | ||
| rowsToDelete.forEach(function(row) { | ||
| applyCascadeDeletes(db, databaseid, tableid, row, params, alasql); | ||
| }); | ||
|
|
||
| // Delete all records from the array | ||
| db.tables[tableid].data.length = 0; | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like way too much code for this problem. Are you sure this is the best way to do it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ~200 lines in UPDATE handle all CASCADE actions for primary key changes, including pre-validation for RESTRICT, NOT NULL constraint checking, and tracking which columns are being updated to avoid unnecessary cascade operations. This is the minimum needed to properly implement SQL-99 CASCADE standard for UPDATE operations. Commit b53bc38 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a lot of code. Are you sure this is the best way to solve this problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the minimum code needed for SQL-standard CASCADE behavior. The ~90 lines handle all 5 CASCADE actions (CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION) with recursive support for grandchild rows and NOT NULL validation. The core logic is in the
applyCascadeDeleteshelper function which is reused for both WHERE and non-WHERE deletes to avoid duplication.Commit b53bc38