diff --git a/src/workerd/api/sql-test.js b/src/workerd/api/sql-test.js index 24de66804da..7decafc0aa9 100644 --- a/src/workerd/api/sql-test.js +++ b/src/workerd/api/sql-test.js @@ -504,6 +504,41 @@ async function test(storage) { assert.equal(getI(), 2); } + // Test joining two tables with overlapping names + { + sql.exec(`CREATE TABLE abc (a INT, b INT, c INT);`) + sql.exec(`CREATE TABLE cde (c INT, d INT, e INT);`) + sql.exec(`INSERT INTO abc VALUES (1,2,3),(4,5,6);`) + sql.exec(`INSERT INTO cde VALUES (7,8,9),(1,2,3);`) + + const stmt = sql.prepare(`SELECT * FROM abc, cde`) + + // In normal iteration, data is lost + const objResults = Array.from(stmt()) + assert.equal(Object.values(objResults[0]).length, 5) // duplicate column 'c' dropped + assert.equal(Object.values(objResults[1]).length, 5) // duplicate column 'c' dropped + assert.equal(Object.values(objResults[2]).length, 5) // duplicate column 'c' dropped + assert.equal(Object.values(objResults[3]).length, 5) // duplicate column 'c' dropped + + assert.equal(objResults[0].c, 7) // Value of 'c' is the second in the join + assert.equal(objResults[1].c, 1) // Value of 'c' is the second in the join + assert.equal(objResults[2].c, 7) // Value of 'c' is the second in the join + assert.equal(objResults[3].c, 1) // Value of 'c' is the second in the join + + // Iterator has a 'columnNames' property, with .raw() that lets us get the full data + const iterator = stmt(); + assert.deepEqual(iterator.columnNames, ["a","b","c","c","d","e"]) + const rawResults = Array.from(iterator.raw()) + assert.equal(rawResults.length, 4) + assert.deepEqual(rawResults[0], [1,2,3,7,8,9]) + assert.deepEqual(rawResults[1], [1,2,3,1,2,3]) + assert.deepEqual(rawResults[2], [4,5,6,7,8,9]) + assert.deepEqual(rawResults[3], [4,5,6,1,2,3]) + + // Once an iterator is consumed, it can no longer access the columnNames. + assert.deepEqual(iterator.columnNames, []) + } + await scheduler.wait(1); // Test for bug where a cursor constructed from a prepared statement didn't have a strong ref diff --git a/src/workerd/api/sql.c++ b/src/workerd/api/sql.c++ index b99a2d39d93..e59e818d47e 100644 --- a/src/workerd/api/sql.c++ +++ b/src/workerd/api/sql.c++ @@ -118,6 +118,17 @@ jsg::Ref SqlStorage::Cursor::raw(jsg::Lock&) { return jsg::alloc(JSG_THIS); } +kj::Array> SqlStorage::Cursor::getColumnNames(jsg::Lock& js) { + KJ_IF_MAYBE(s, state) { + cachedColumnNames.ensureInitialized(js, (*s)->query); + return KJ_MAP(name, this->cachedColumnNames.get()) { + return name.addRef(js); + }; + } else { + return kj::Array>(); + } +} + kj::Maybe> SqlStorage::Cursor::rawIteratorNext( jsg::Lock& js, jsg::Ref& obj) { return iteratorImpl(js, obj, diff --git a/src/workerd/api/sql.h b/src/workerd/api/sql.h index dd2ffe7aeef..3044e651fb1 100644 --- a/src/workerd/api/sql.h +++ b/src/workerd/api/sql.h @@ -97,11 +97,14 @@ class SqlStorage::Cursor final: public jsg::Object { cachedColumnNames(cachedColumnNames) {} ~Cursor() noexcept(false); + kj::Array> getColumnNames(jsg::Lock& js); JSG_RESOURCE_TYPE(Cursor, CompatibilityFlags::Reader flags) { JSG_ITERABLE(rows); JSG_METHOD(raw); + JSG_READONLY_PROTOTYPE_PROPERTY(columnNames, getColumnNames); } + using Value = kj::Maybe, kj::StringPtr, double>>; // One value returned from SQL. Note that we intentionally return StringPtr instead of String // because we know that the underlying buffer returned by SQLite will be valid long enough to be