diff --git a/src/17alasql.js b/src/17alasql.js index 1f14e1bcd5..35950e2eef 100755 --- a/src/17alasql.js +++ b/src/17alasql.js @@ -129,6 +129,9 @@ alasql.options = { /** Maximum iterations for recursive CTEs to prevent infinite loops */ maxCteIterations: 1000, + /** Enable JavaScript property access via dot notation (e.g., column.length) */ + angularBrackets: false, + /** Whether GETDATE() and NOW() return dates as string. If false, then a Date object is returned */ dateAsString: true, }; @@ -272,9 +275,11 @@ alasql.dexec = function (databaseid, sql, params, cb, scope) { // if(db.databaseid != databaseid) console.trace('got!'); // console.log(3,db.databaseid,databaseid); - // Include joinstar option in cache key because it affects how SELECT * compiles - // Without this, changing joinstar would use stale cached queries compiled with old option - var hh = hash(sql + '|joinstar:' + alasql.options.joinstar); + // Include joinstar and angularBrackets options in cache key because they affect how queries compile + // Without this, changing these options would use stale cached queries compiled with old options + var hh = hash( + sql + '|joinstar:' + alasql.options.joinstar + '|angularBrackets:' + alasql.options.angularBrackets + ); // Create hash if (alasql.options.cache) { diff --git a/src/424select.js b/src/424select.js index c6bb630a6e..6de9e601f1 100755 --- a/src/424select.js +++ b/src/424select.js @@ -198,10 +198,29 @@ yy.Select.prototype.compileSelect1 = function (query, params) { var tbid = col.tableid; // console.log(query.sources); var dbid = col.databaseid || query.sources[0].databaseid || query.database.databaseid; - if (!tbid) tbid = query.defcols[col.columnid]; + + // Check if tableid is actually a column name (property access pattern like name.length) + // This handles cases where the parser sees "columnname.property" and interprets it as "table.column" + var isPropertyAccess = false; + var actualTableid; + if (alasql.options.angularBrackets && tbid) { + const tbidValue = query.defcols?.[tbid]; + if (tbidValue && tbidValue !== '-' && !query.defcols?.['.']?.[tbid]) { + // tbid is actually a column name (not a table name), so this is property access + isPropertyAccess = true; + actualTableid = tbidValue; + } + } + + if (!tbid) tbid = query.defcols?.[col.columnid]; if (!tbid) tbid = query.defaultTableid; if (col.columnid !== '_') { - if (false && tbid && !query.defcols['.'][col.tableid] && !query.defcols[col.columnid]) { + if ( + false && + tbid && + !query.defcols?.['.']?.[col.tableid] && + !query.defcols?.[col.columnid] + ) { ss.push( "'" + escapeq(col.as || col.columnid) + @@ -260,6 +279,20 @@ yy.Select.prototype.compileSelect1 = function (query, params) { .join(' ?? '); ss.push("'" + escapeq(col.as || col.columnid) + "':(" + searchExpr + ')'); + } else if (isPropertyAccess) { + // Property access pattern: columnname.property (e.g., name.length) + // Generate code to access the property on the column value + ss.push( + "'" + + escapeq(col.as || col.columnid) + + "':((p['" + + actualTableid + + "']['" + + col.tableid + + "'] || {})['" + + col.columnid + + "'])" + ); } else { ss.push( "'" + diff --git a/src/50expression.js b/src/50expression.js index 675fa32d05..7b607c6e86 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -782,6 +782,20 @@ } if (this.tableid) { + // Check if tableid is actually a column name (property access pattern like name.length) + // This handles cases where the parser sees "columnname.property" and interprets it as "table.column" + if (alasql.options.angularBrackets) { + const tableidValue = defcols?.[this.tableid]; + if (tableidValue && tableidValue !== '-' && !defcols?.['.']?.[this.tableid]) { + // tableid is actually a column name (not a table name), so this is property access + // Generate code to access the property on the column value + const actualTable = tableidValue; + if (this.columnid !== '_') { + return `((${context}['${actualTable}']['${this.tableid}'] || {})['${this.columnid}'])`; + } + } + } + // Otherwise, tableid is a table name (normal table.column access) return this.columnid !== '_' ? `${context}['${this.tableid}']['${this.columnid}']` : context === 'g' diff --git a/test/test341.js b/test/test341.js index b3efd03e86..4918b1d6d2 100644 --- a/test/test341.js +++ b/test/test341.js @@ -6,6 +6,15 @@ if (typeof exports === 'object') { } describe('Test 341 Intellectual DOT operator', function () { + // Enable JavaScript property access via dot notation for these tests + before(function () { + alasql.options.angularBrackets = true; + }); + + after(function () { + alasql.options.angularBrackets = false; + }); + it('1. CREATE DATABASE', function (done) { alasql('CREATE DATABASE test341;USE test341'); done(); @@ -35,18 +44,57 @@ describe('Test 341 Intellectual DOT operator', function () { done(); }); - it.skip('4. JavaScript way', function (done) { + it('4. JavaScript way', function (done) { + // Test SET statement with JavaScript property access + // SET returns the number of rows affected (1 for variable assignment) var res = alasql('SET @a = "who".length'); - assert.deepEqual(res, [6, 6, 7]); + assert.deepEqual(res, 1); + + // Verify the variable @a was set to the correct value (length of "who" = 3) + assert.deepEqual(alasql.vars.a, 3); + + // Verify we can use the variable in subsequent queries + var res2 = alasql('SELECT @a AS result'); + assert.deepEqual(res2, [{result: 3}]); done(); }); - it.skip('5. JavaScript way', function (done) { + it('5. JavaScript way', function (done) { var res = alasql('SELECT COLUMN name.length FROM persons'); assert.deepEqual(res, [6, 6, 7]); done(); }); + it('6. JavaScript way with table.column.length', function (done) { + var res = alasql('SELECT COLUMN persons.name.length FROM persons'); + assert.deepEqual(res, [6, 6, 7]); + done(); + }); + + it('7. Edge case: table and column with same name', function (done) { + // Create a table named "item" with a column named "item" + alasql('CREATE TABLE item (id INT, item STRING)'); + alasql('INSERT INTO item VALUES (1, "Alpha"), (2, "Beta")'); + + // Test 1: table.column access (item.item should get the column value) + var res1 = alasql('SELECT item.item FROM item'); + assert.deepEqual(res1, [{item: 'Alpha'}, {item: 'Beta'}]); + + // Test 2: table.column.property access (item.item.length should get the length) + var res2 = alasql('SELECT COLUMN item.item.length FROM item'); + assert.deepEqual(res2, [5, 4]); + + // Test 3: When only one column exists with unique name, property access works + alasql('CREATE TABLE products (title STRING)'); + alasql('INSERT INTO products VALUES ("Product"), ("Item")'); + var res3 = alasql('SELECT COLUMN title.length FROM products'); + assert.deepEqual(res3, [7, 4]); + + alasql('DROP TABLE item'); + alasql('DROP TABLE products'); + done(); + }); + it('5. FOREIGN KEY way', function (done) { var res = alasql('SELECT VALUE $0; SET $0 = 200; SELECT VALUE $0', [100]); assert.deepEqual(res, [100, 1, 200]);