From c9e138eb5f13ff961a7b70f4942898e0f59dcb4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:10:17 +0000 Subject: [PATCH 1/3] Initial plan From 61420d645f2f5642ef50a1c877eff3f13f3d9b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:17:07 +0000 Subject: [PATCH 2/3] Fix primary key enforcement for IndexedDB tables (issue #1292) Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com> --- src/91indexeddb.js | 42 ++++++++++++++++++++++++++++-- test/test1292.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 test/test1292.js diff --git a/src/91indexeddb.js b/src/91indexeddb.js index b779669366..ddbb4989f9 100755 --- a/src/91indexeddb.js +++ b/src/91indexeddb.js @@ -184,9 +184,26 @@ IDB.createTable = async function (databaseid, tableid, ifnotexists, cb) { throw err; } + // Get primary key information from the table definition + const table = alasql.databases[databaseid].tables[tableid]; + const pk = table && table.pk; + + // Build object store options + let storeOptions; + if (pk && pk.columns && pk.columns.length === 1) { + // Single-column primary key: use keyPath + storeOptions = {keyPath: pk.columns[0]}; + } else if (pk && pk.columns && pk.columns.length > 1) { + // Composite primary key: use array keyPath + storeOptions = {keyPath: pk.columns}; + } else { + // No primary key: use auto-increment + storeOptions = {autoIncrement: true}; + } + const request = indexedDB.open(ixdbid, found.version + 1); request.onupgradeneeded = function (event) { - request.result.createObjectStore(tableid, {autoIncrement: true}); + request.result.createObjectStore(tableid, storeOptions); }; request.onsuccess = function (event) { request.result.close(); @@ -300,7 +317,20 @@ IDB.intoTable = function (databaseid, tableid, value, columns, cb) { var tx = ixdb.transaction([tableid], 'readwrite'); var tb = tx.objectStore(tableid); for (var i = 0, ilen = value.length; i < ilen; i++) { - tb.add(value[i]); + var addRequest = tb.add(value[i]); + addRequest.onerror = function (evt) { + // Handle duplicate key errors + if (evt.target.error && evt.target.error.name === 'ConstraintError') { + evt.preventDefault(); + evt.stopPropagation(); + tx.abort(); + ixdb.close(); + var err = new Error( + 'Cannot insert record, because it already exists in primary key index' + ); + if (cb) cb(null, err); + } + }; } tx.oncomplete = function () { ixdb.close(); @@ -316,6 +346,14 @@ IDB.intoTable = function (databaseid, tableid, value, columns, cb) { } if (cb) cb(ilen); }; + tx.onerror = function (evt) { + ixdb.close(); + // Only report error if not already handled by addRequest.onerror + if (evt.target.error && evt.target.error.name === 'ConstraintError') { + var err = new Error('Cannot insert record, because it already exists in primary key index'); + if (cb) cb(null, err); + } + }; }; }; diff --git a/test/test1292.js b/test/test1292.js new file mode 100644 index 0000000000..3c017e5f8c --- /dev/null +++ b/test/test1292.js @@ -0,0 +1,64 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('Test 1292 - Primary key enforcement prevents duplicate values', function () { + const test = '1292'; + + before(function () { + alasql('create database test' + test); + alasql('use test' + test); + }); + + after(function () { + alasql('drop database test' + test); + }); + + it('A) Primary key column constraint prevents duplicates', function () { + alasql('CREATE TABLE settings (setting varchar(50) PRIMARY KEY, val varchar(300))'); + alasql("INSERT INTO settings (setting, val) values ('domain', 'http')"); + + // Inserting a duplicate primary key should throw an error + assert.throws(function () { + alasql("INSERT INTO settings (setting, val) values ('domain', 'https')"); + }, /already exists in primary key/); + + // Verify only one record exists + var res = alasql('SELECT * FROM settings'); + assert.deepEqual(res, [{setting: 'domain', val: 'http'}]); + + alasql('DROP TABLE settings'); + }); + + it('B) Primary key table constraint prevents duplicates', function () { + alasql('CREATE TABLE settings2 (setting varchar(50), val varchar(300), PRIMARY KEY (setting))'); + alasql("INSERT INTO settings2 (setting, val) values ('domain', 'http')"); + + // Inserting a duplicate primary key should throw an error + assert.throws(function () { + alasql("INSERT INTO settings2 (setting, val) values ('domain', 'https')"); + }, /already exists in primary key/); + + // Verify only one record exists + var res = alasql('SELECT * FROM settings2'); + assert.deepEqual(res, [{setting: 'domain', val: 'http'}]); + + alasql('DROP TABLE settings2'); + }); + + it('C) Primary key allows different key values', function () { + alasql('CREATE TABLE settings3 (setting varchar(50) PRIMARY KEY, val varchar(300))'); + alasql("INSERT INTO settings3 (setting, val) values ('domain', 'http')"); + alasql("INSERT INTO settings3 (setting, val) values ('port', '8080')"); + + // Verify both records exist + var res = alasql('SELECT * FROM settings3 ORDER BY setting'); + assert.deepEqual(res, [ + {setting: 'domain', val: 'http'}, + {setting: 'port', val: '8080'}, + ]); + + alasql('DROP TABLE settings3'); + }); +}); From 0d54b9a7a6095ad48e83e2ff848f851039c60226 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:19:41 +0000 Subject: [PATCH 3/3] Address code review feedback: prevent double callback on errors Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com> --- src/91indexeddb.js | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/91indexeddb.js b/src/91indexeddb.js index ddbb4989f9..4032b3bea4 100755 --- a/src/91indexeddb.js +++ b/src/91indexeddb.js @@ -186,16 +186,16 @@ IDB.createTable = async function (databaseid, tableid, ifnotexists, cb) { // Get primary key information from the table definition const table = alasql.databases[databaseid].tables[tableid]; - const pk = table && table.pk; + const pkColumns = table && table.pk && table.pk.columns; // Build object store options let storeOptions; - if (pk && pk.columns && pk.columns.length === 1) { + if (pkColumns && pkColumns.length === 1) { // Single-column primary key: use keyPath - storeOptions = {keyPath: pk.columns[0]}; - } else if (pk && pk.columns && pk.columns.length > 1) { + storeOptions = {keyPath: pkColumns[0]}; + } else if (pkColumns && pkColumns.length > 1) { // Composite primary key: use array keyPath - storeOptions = {keyPath: pk.columns}; + storeOptions = {keyPath: pkColumns}; } else { // No primary key: use auto-increment storeOptions = {autoIncrement: true}; @@ -316,11 +316,13 @@ IDB.intoTable = function (databaseid, tableid, value, columns, cb) { var ixdb = request.result; var tx = ixdb.transaction([tableid], 'readwrite'); var tb = tx.objectStore(tableid); + var errorHandled = false; for (var i = 0, ilen = value.length; i < ilen; i++) { var addRequest = tb.add(value[i]); addRequest.onerror = function (evt) { // Handle duplicate key errors - if (evt.target.error && evt.target.error.name === 'ConstraintError') { + if (!errorHandled && evt.target.error && evt.target.error.name === 'ConstraintError') { + errorHandled = true; evt.preventDefault(); evt.stopPropagation(); tx.abort(); @@ -346,14 +348,6 @@ IDB.intoTable = function (databaseid, tableid, value, columns, cb) { } if (cb) cb(ilen); }; - tx.onerror = function (evt) { - ixdb.close(); - // Only report error if not already handled by addRequest.onerror - if (evt.target.error && evt.target.error.name === 'ConstraintError') { - var err = new Error('Cannot insert record, because it already exists in primary key index'); - if (cb) cb(null, err); - } - }; }; };