diff --git a/benchmarks-website-v2/package-lock.json b/benchmarks-website-v2/package-lock.json index 36490839549..6fb85d6df00 100644 --- a/benchmarks-website-v2/package-lock.json +++ b/benchmarks-website-v2/package-lock.json @@ -22,7 +22,7 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "concurrently": "^8.2.2", - "vite": "^5.4.0" + "vite": "^7.3.1" }, "engines": { "node": ">=18.0.0" @@ -59,7 +59,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -322,9 +321,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -335,13 +334,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -352,13 +351,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -369,13 +368,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -386,13 +385,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -403,13 +402,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -420,13 +419,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -437,13 +436,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -454,13 +453,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -471,13 +470,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -488,13 +487,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -505,13 +504,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -522,13 +521,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -539,13 +538,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -556,13 +555,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -573,13 +572,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -590,13 +589,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -607,13 +606,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -624,13 +640,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -641,13 +674,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -658,13 +708,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -675,13 +725,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -692,13 +742,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -709,7 +759,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1196,7 +1246,6 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1289,7 +1338,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1360,7 +1408,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1514,9 +1561,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1524,32 +1571,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -1562,6 +1612,24 @@ "node": ">=6" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1736,6 +1804,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -1770,7 +1851,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1968,6 +2048,23 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2017,22 +2114,24 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2041,19 +2140,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -2074,6 +2179,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, diff --git a/benchmarks-website-v2/package.json b/benchmarks-website-v2/package.json index 0194ff7652b..f486d17242c 100644 --- a/benchmarks-website-v2/package.json +++ b/benchmarks-website-v2/package.json @@ -27,6 +27,6 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "concurrently": "^8.2.2", - "vite": "^5.4.0" + "vite": "^7.3.1" } } diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index 9ba20be3db3..ada0add1110 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -129,6 +129,11 @@ name = "expr_large_struct_pack" path = "benches/expr/large_struct_pack.rs" harness = false +[[bench]] +name = "expr_case_when" +path = "benches/expr/case_when_bench.rs" +harness = false + [[bench]] name = "chunked_dict_builder" harness = false diff --git a/vortex-array/benches/expr/case_when_bench.rs b/vortex-array/benches/expr/case_when_bench.rs new file mode 100644 index 00000000000..183b56c178c --- /dev/null +++ b/vortex-array/benches/expr/case_when_bench.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![allow(clippy::unwrap_used)] +#![allow(clippy::cast_possible_truncation)] + +use std::sync::LazyLock; + +use divan::Bencher; +use vortex_array::ArrayRef; +use vortex_array::Canonical; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::StructArray; +use vortex_array::expr::case_when; +use vortex_array::expr::get_item; +use vortex_array::expr::gt; +use vortex_array::expr::lit; +use vortex_array::expr::nested_case_when; +use vortex_array::expr::root; +use vortex_array::session::ArraySession; +use vortex_array::validity::Validity; +use vortex_buffer::Buffer; +use vortex_dtype::FieldNames; +use vortex_session::VortexSession; + +static SESSION: LazyLock = + LazyLock::new(|| VortexSession::empty().with::()); + +fn main() { + divan::main(); +} + +fn make_struct_array(size: usize) -> ArrayRef { + let data: Buffer = (0..size as i32).collect(); + let field = data.into_array(); + StructArray::try_new( + FieldNames::from(["value"]), + vec![field], + size, + Validity::NonNullable, + ) + .unwrap() + .into_array() +} + +/// Benchmark a simple binary CASE WHEN with varying array sizes. +#[divan::bench(args = [1000, 10000, 100000])] +fn case_when_simple(bencher: Bencher, size: usize) { + let array = make_struct_array(size); + + // CASE WHEN value > 500 THEN 100 ELSE 0 END + let expr = case_when( + gt(get_item("value", root()), lit(500i32)), + lit(100i32), + lit(0i32), + ); + + bencher + .with_inputs(|| (&expr, &array)) + .bench_refs(|(expr, array)| { + let mut ctx = SESSION.create_execution_ctx(); + array + .apply(expr) + .unwrap() + .execute::(&mut ctx) + .unwrap() + }); +} + +/// Benchmark nested CASE WHEN with multiple conditions. +#[divan::bench(args = [1000, 10000, 100000])] +fn case_when_nested_3_conditions(bencher: Bencher, size: usize) { + let array = make_struct_array(size); + + // CASE WHEN value > 750 THEN 3 WHEN value > 500 THEN 2 WHEN value > 250 THEN 1 ELSE 0 END + let expr = nested_case_when( + vec![ + (gt(get_item("value", root()), lit(750i32)), lit(3i32)), + (gt(get_item("value", root()), lit(500i32)), lit(2i32)), + (gt(get_item("value", root()), lit(250i32)), lit(1i32)), + ], + Some(lit(0i32)), + ); + + bencher + .with_inputs(|| (&expr, &array)) + .bench_refs(|(expr, array)| { + let mut ctx = SESSION.create_execution_ctx(); + array + .apply(expr) + .unwrap() + .execute::(&mut ctx) + .unwrap() + }); +} + +/// Benchmark CASE WHEN where all conditions are true (short-circuit path). +#[divan::bench(args = [1000, 10000, 100000])] +fn case_when_all_true(bencher: Bencher, size: usize) { + let array = make_struct_array(size); + + // CASE WHEN value >= 0 THEN 100 ELSE 0 END (always true for our data) + let expr = case_when( + gt(get_item("value", root()), lit(-1i32)), + lit(100i32), + lit(0i32), + ); + + bencher + .with_inputs(|| (&expr, &array)) + .bench_refs(|(expr, array)| { + let mut ctx = SESSION.create_execution_ctx(); + array + .apply(expr) + .unwrap() + .execute::(&mut ctx) + .unwrap() + }); +} + +/// Benchmark CASE WHEN where all conditions are false (short-circuit path). +#[divan::bench(args = [1000, 10000, 100000])] +fn case_when_all_false(bencher: Bencher, size: usize) { + let array = make_struct_array(size); + + // CASE WHEN value > 1000000 THEN 100 ELSE 0 END (always false for our data) + let expr = case_when( + gt(get_item("value", root()), lit(1_000_000i32)), + lit(100i32), + lit(0i32), + ); + + bencher + .with_inputs(|| (&expr, &array)) + .bench_refs(|(expr, array)| { + let mut ctx = SESSION.create_execution_ctx(); + array + .apply(expr) + .unwrap() + .execute::(&mut ctx) + .unwrap() + }); +} diff --git a/vortex-array/src/expr/exprs/case_when.rs b/vortex-array/src/expr/exprs/case_when.rs new file mode 100644 index 00000000000..faace3c9062 --- /dev/null +++ b/vortex-array/src/expr/exprs/case_when.rs @@ -0,0 +1,686 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Binary CASE WHEN expression for conditional value selection. +//! +//! This expression is a simple wrapper around the `zip` compute function: +//! `CASE WHEN condition THEN value ELSE else_value END` +//! +//! For n-ary CASE WHEN expressions (multiple WHEN clauses), use the +//! [`nested_case_when`] convenience function which converts to nested binary expressions: +//! `CASE WHEN a THEN x WHEN b THEN y ELSE z END` becomes +//! `CASE WHEN a THEN x ELSE (CASE WHEN b THEN y ELSE z END) END` + +use std::fmt; +use std::fmt::Formatter; +use std::hash::Hash; + +use prost::Message; +use vortex_dtype::DType; +use vortex_error::VortexResult; +use vortex_error::vortex_bail; +use vortex_error::vortex_panic; +use vortex_proto::expr as pb; +use vortex_scalar::Scalar; + +use crate::ArrayRef; +use crate::IntoArray; +use crate::arrays::BoolArray; +use crate::arrays::ConstantArray; +use crate::compute::zip; +use crate::expr::Arity; +use crate::expr::ChildName; +use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; +use crate::expr::ExprId; +use crate::expr::VTable; +use crate::expr::VTableExt; +use crate::expr::expression::Expression; + +/// Options for the binary CaseWhen expression. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CaseWhenOptions { + /// Whether an ELSE clause is present. + /// If false, unmatched rows return NULL. + pub has_else: bool, +} + +impl fmt::Display for CaseWhenOptions { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "case_when(else={})", self.has_else) + } +} + +/// A binary CASE WHEN expression. +/// +/// This is a simple conditional select: `CASE WHEN cond THEN value ELSE else_value END` +/// which is equivalent to `zip(value, else_value, cond)`. +/// +/// Children are always in order: [condition, then_value, else_value?] +pub struct CaseWhen; + +impl VTable for CaseWhen { + type Options = CaseWhenOptions; + + fn id(&self) -> ExprId { + ExprId::from("vortex.case_when") + } + + fn serialize(&self, options: &Self::Options) -> VortexResult>> { + Ok(Some( + pb::CaseWhenOpts { + // For backwards compatibility, binary is num_when_then_pairs=1 + num_when_then_pairs: 1, + has_else: options.has_else, + } + .encode_to_vec(), + )) + } + + fn deserialize(&self, metadata: &[u8]) -> VortexResult { + let opts = pb::CaseWhenOpts::decode(metadata)?; + // We only support binary (1 when/then pair) now + if opts.num_when_then_pairs != 1 { + vortex_bail!( + "CaseWhen only supports binary form (1 when/then pair), got {}", + opts.num_when_then_pairs + ); + } + Ok(CaseWhenOptions { + has_else: opts.has_else, + }) + } + + fn arity(&self, options: &Self::Options) -> Arity { + // Binary: condition + then + optional else + let num_children = 2 + if options.has_else { 1 } else { 0 }; + Arity::Exact(num_children) + } + + fn child_name(&self, options: &Self::Options, child_idx: usize) -> ChildName { + match child_idx { + 0 => ChildName::from("when"), + 1 => ChildName::from("then"), + 2 if options.has_else => ChildName::from("else"), + _ => unreachable!("Invalid child index {} for binary CaseWhen", child_idx), + } + } + + fn fmt_sql( + &self, + options: &Self::Options, + expr: &Expression, + f: &mut Formatter<'_>, + ) -> fmt::Result { + write!(f, "CASE WHEN {} THEN {}", expr.child(0), expr.child(1))?; + if options.has_else { + write!(f, " ELSE {}", expr.child(2))?; + } + write!(f, " END") + } + + fn return_dtype(&self, options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult { + // The return dtype is based on the THEN expression (index 1) + let then_dtype = &arg_dtypes[1]; + + // If there's no ELSE, the result is always nullable (unmatched rows are NULL) + if !options.has_else { + Ok(then_dtype.as_nullable()) + } else { + Ok(then_dtype.clone()) + } + } + + fn execute( + &self, + _options: &Self::Options, + args: ExecutionArgs, + ) -> VortexResult { + let row_count = args.row_count; + + // Extract inputs based on arity: [condition, then_value] or [condition, then_value, else_value] + let (condition, then_value, else_value) = match args.inputs.len() { + 2 => { + let [condition, then_value]: [ArrayRef; 2] = args + .inputs + .try_into() + .map_err(|_| vortex_error::vortex_err!("Expected 2 inputs"))?; + (condition, then_value, None) + } + 3 => { + let [condition, then_value, else_value]: [ArrayRef; 3] = args + .inputs + .try_into() + .map_err(|_| vortex_error::vortex_err!("Expected 3 inputs"))?; + (condition, then_value, Some(else_value)) + } + n => vortex_bail!("CaseWhen expects 2 or 3 inputs, got {}", n), + }; + + // Execute condition to get a BoolArray + let cond_bool = condition.execute::(args.ctx)?; + // SQL semantics: NULL condition is treated as FALSE (i.e., we take the ELSE branch) + let mask = cond_bool.to_mask_fill_null_false(); + + // Short-circuit: all true -> just return THEN value + if mask.all_true() { + return then_value.execute::(args.ctx); + } + + // Short-circuit: all false -> return ELSE value or NULL + if mask.all_false() { + return match else_value { + Some(else_value) => else_value.execute::(args.ctx), + None => { + // Create NULL constant of appropriate type + let then_dtype = then_value.dtype().as_nullable(); + Ok(ExecutionResult::constant( + Scalar::null(then_dtype), + row_count, + )) + } + }; + } + + // Get else value for zip (create NULL constant if no else clause) + let else_value = else_value.unwrap_or_else(|| { + let then_dtype = then_value.dtype().as_nullable(); + ConstantArray::new(Scalar::null(then_dtype), row_count).into_array() + }); + + // Use zip to select: where mask is true, take then_value; else take else_value + let result = zip(then_value.as_ref(), else_value.as_ref(), &mask)?; + + result.execute::(args.ctx) + } + + fn is_null_sensitive(&self, _options: &Self::Options) -> bool { + // CaseWhen is null-sensitive because NULL conditions are treated as false + true + } + + fn is_fallible(&self, _options: &Self::Options) -> bool { + false + } +} + +/// Creates a binary CASE WHEN expression with an ELSE clause. +/// +/// # Arguments +/// - `condition`: Boolean expression for the WHEN clause +/// - `then_value`: Value to return when condition is true +/// - `else_value`: Value to return when condition is false +/// +/// # Example +/// ```ignore +/// // CASE WHEN x > 0 THEN 'positive' ELSE 'non-positive' END +/// case_when(gt(col("x"), lit(0)), lit("positive"), lit("non-positive")) +/// ``` +pub fn case_when( + condition: Expression, + then_value: Expression, + else_value: Expression, +) -> Expression { + let options = CaseWhenOptions { has_else: true }; + CaseWhen.new_expr(options, [condition, then_value, else_value]) +} + +/// Creates a binary CASE WHEN expression without an ELSE clause. +/// +/// Returns NULL when the condition is false. +/// +/// # Arguments +/// - `condition`: Boolean expression for the WHEN clause +/// - `then_value`: Value to return when condition is true +/// +/// # Example +/// ```ignore +/// // CASE WHEN x > 0 THEN 'positive' END +/// case_when_no_else(gt(col("x"), lit(0)), lit("positive")) +/// ``` +pub fn case_when_no_else(condition: Expression, then_value: Expression) -> Expression { + let options = CaseWhenOptions { has_else: false }; + CaseWhen.new_expr(options, [condition, then_value]) +} + +/// Creates a nested CASE WHEN expression from multiple WHEN/THEN pairs. +/// +/// This is a convenience function that converts n-ary CASE WHEN to nested binary expressions: +/// `CASE WHEN a THEN x WHEN b THEN y ELSE z END` becomes +/// `CASE WHEN a THEN x ELSE (CASE WHEN b THEN y ELSE z END) END` +/// +/// # Arguments +/// - `when_then_pairs`: Vec of (condition, value) pairs +/// - `else_value`: Optional else expression (if None, unmatched rows return NULL) +/// +/// # Example +/// ```ignore +/// // CASE WHEN x > 10 THEN 'high' WHEN x > 5 THEN 'medium' ELSE 'low' END +/// nested_case_when( +/// vec![ +/// (gt(col("x"), lit(10)), lit("high")), +/// (gt(col("x"), lit(5)), lit("medium")), +/// ], +/// Some(lit("low")), +/// ) +/// ``` +pub fn nested_case_when( + when_then_pairs: Vec<(Expression, Expression)>, + else_value: Option, +) -> Expression { + assert!( + !when_then_pairs.is_empty(), + "nested_case_when requires at least one when/then pair" + ); + + // Build from right to left (innermost first) using rfold + when_then_pairs + .into_iter() + .rfold(else_value, |acc, (condition, then_value)| { + Some(match acc { + Some(else_expr) => case_when(condition, then_value, else_expr), + None => case_when_no_else(condition, then_value), + }) + }) + .unwrap_or_else(|| vortex_panic!("rfold on non-empty iterator always produces Some")) +} + +#[cfg(test)] +mod tests { + use std::sync::LazyLock; + + use vortex_buffer::buffer; + use vortex_dtype::DType; + use vortex_dtype::Nullability; + use vortex_dtype::PType; + use vortex_error::VortexExpect as _; + use vortex_scalar::Scalar; + use vortex_session::VortexSession; + + use super::*; + use crate::Canonical; + use crate::IntoArray; + use crate::ToCanonical; + use crate::VortexSessionExecute as _; + use crate::arrays::BoolArray; + use crate::arrays::PrimitiveArray; + use crate::arrays::StructArray; + use crate::expr::exprs::binary::eq; + use crate::expr::exprs::binary::gt; + use crate::expr::exprs::get_item::col; + use crate::expr::exprs::get_item::get_item; + use crate::expr::exprs::literal::lit; + use crate::expr::exprs::root::root; + use crate::expr::test_harness; + use crate::session::ArraySession; + + static SESSION: LazyLock = + LazyLock::new(|| VortexSession::empty().with::()); + + /// Helper to evaluate an expression using the apply+execute pattern + fn evaluate_expr(expr: &Expression, array: &ArrayRef) -> ArrayRef { + let mut ctx = SESSION.create_execution_ctx(); + array + .apply(expr) + .unwrap() + .execute::(&mut ctx) + .unwrap() + .into_array() + } + + // ==================== Serialization Tests ==================== + + #[test] + fn test_serialization_roundtrip() { + let options = CaseWhenOptions { has_else: true }; + let serialized = CaseWhen.serialize(&options).unwrap().unwrap(); + let deserialized = CaseWhen.deserialize(&serialized).unwrap(); + assert_eq!(options, deserialized); + } + + #[test] + fn test_serialization_no_else() { + let options = CaseWhenOptions { has_else: false }; + let serialized = CaseWhen.serialize(&options).unwrap().unwrap(); + let deserialized = CaseWhen.deserialize(&serialized).unwrap(); + assert_eq!(options, deserialized); + } + + // ==================== Display Tests ==================== + + #[test] + fn test_display_with_else() { + let expr = case_when(gt(col("value"), lit(0i32)), lit(100i32), lit(0i32)); + let display = format!("{}", expr); + assert!(display.contains("CASE")); + assert!(display.contains("WHEN")); + assert!(display.contains("THEN")); + assert!(display.contains("ELSE")); + assert!(display.contains("END")); + } + + #[test] + fn test_display_no_else() { + let expr = case_when_no_else(gt(col("value"), lit(0i32)), lit(100i32)); + let display = format!("{}", expr); + assert!(display.contains("CASE")); + assert!(display.contains("WHEN")); + assert!(display.contains("THEN")); + assert!(!display.contains("ELSE")); + assert!(display.contains("END")); + } + + #[test] + fn test_display_nested_nary() { + // CASE WHEN x > 10 THEN 'high' WHEN x > 5 THEN 'medium' ELSE 'low' END + // Becomes nested: CASE WHEN x>10 THEN 'high' ELSE (CASE WHEN x>5 THEN 'medium' ELSE 'low' END) END + let expr = nested_case_when( + vec![ + (gt(col("x"), lit(10i32)), lit("high")), + (gt(col("x"), lit(5i32)), lit("medium")), + ], + Some(lit("low")), + ); + let display = format!("{}", expr); + // Should contain nested CASE statements + assert_eq!(display.matches("CASE").count(), 2); + assert_eq!(display.matches("WHEN").count(), 2); + assert_eq!(display.matches("THEN").count(), 2); + } + + // ==================== DType Tests ==================== + + #[test] + fn test_return_dtype_with_else() { + let expr = case_when(lit(true), lit(100i32), lit(0i32)); + let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable); + let result_dtype = expr.return_dtype(&input_dtype).unwrap(); + assert_eq!( + result_dtype, + DType::Primitive(PType::I32, Nullability::NonNullable) + ); + } + + #[test] + fn test_return_dtype_without_else_is_nullable() { + let expr = case_when_no_else(lit(true), lit(100i32)); + let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable); + let result_dtype = expr.return_dtype(&input_dtype).unwrap(); + assert_eq!( + result_dtype, + DType::Primitive(PType::I32, Nullability::Nullable) + ); + } + + #[test] + fn test_return_dtype_with_struct_input() { + let dtype = test_harness::struct_dtype(); + let expr = case_when( + gt(get_item("col1", root()), lit(10u16)), + lit(100i32), + lit(0i32), + ); + let result_dtype = expr.return_dtype(&dtype).unwrap(); + assert_eq!( + result_dtype, + DType::Primitive(PType::I32, Nullability::NonNullable) + ); + } + + // ==================== Arity Tests ==================== + + #[test] + fn test_arity_with_else() { + let options = CaseWhenOptions { has_else: true }; + assert_eq!(CaseWhen.arity(&options), Arity::Exact(3)); + } + + #[test] + fn test_arity_without_else() { + let options = CaseWhenOptions { has_else: false }; + assert_eq!(CaseWhen.arity(&options), Arity::Exact(2)); + } + + // ==================== Child Name Tests ==================== + + #[test] + fn test_child_names() { + let options = CaseWhenOptions { has_else: true }; + assert_eq!(CaseWhen.child_name(&options, 0).to_string(), "when"); + assert_eq!(CaseWhen.child_name(&options, 1).to_string(), "then"); + assert_eq!(CaseWhen.child_name(&options, 2).to_string(), "else"); + } + + // ==================== Expression Manipulation Tests ==================== + + #[test] + fn test_replace_children() { + let expr = case_when(lit(true), lit(1i32), lit(0i32)); + expr.with_children([lit(false), lit(2i32), lit(3i32)]) + .vortex_expect("operation should succeed in test"); + } + + // ==================== Evaluate Tests ==================== + + #[test] + fn test_evaluate_simple_condition() { + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + let expr = case_when( + gt(get_item("value", root()), lit(2i32)), + lit(100i32), + lit(0i32), + ); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[0, 0, 100, 100, 100]); + } + + #[test] + fn test_evaluate_nary_multiple_conditions() { + // Test n-ary via nested_case_when + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + let expr = nested_case_when( + vec![ + (eq(get_item("value", root()), lit(1i32)), lit(10i32)), + (eq(get_item("value", root()), lit(3i32)), lit(30i32)), + ], + Some(lit(0i32)), + ); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[10, 0, 30, 0, 0]); + } + + #[test] + fn test_evaluate_nary_first_match_wins() { + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + // Both conditions match for values > 3, but first one wins + let expr = nested_case_when( + vec![ + (gt(get_item("value", root()), lit(2i32)), lit(100i32)), + (gt(get_item("value", root()), lit(3i32)), lit(200i32)), + ], + Some(lit(0i32)), + ); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[0, 0, 100, 100, 100]); + } + + #[test] + fn test_evaluate_no_else_returns_null() { + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + let expr = case_when_no_else(gt(get_item("value", root()), lit(3i32)), lit(100i32)); + + let result = evaluate_expr(&expr, &test_array); + assert!(result.dtype().is_nullable()); + + assert_eq!( + result.scalar_at(0).unwrap(), + Scalar::null(result.dtype().clone()) + ); + assert_eq!( + result.scalar_at(1).unwrap(), + Scalar::null(result.dtype().clone()) + ); + assert_eq!( + result.scalar_at(2).unwrap(), + Scalar::null(result.dtype().clone()) + ); + assert_eq!( + result.scalar_at(3).unwrap(), + Scalar::from(100i32).cast(result.dtype()).unwrap() + ); + assert_eq!( + result.scalar_at(4).unwrap(), + Scalar::from(100i32).cast(result.dtype()).unwrap() + ); + } + + #[test] + fn test_evaluate_all_conditions_false() { + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + let expr = case_when( + gt(get_item("value", root()), lit(100i32)), + lit(1i32), + lit(0i32), + ); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[0, 0, 0, 0, 0]); + } + + #[test] + fn test_evaluate_all_conditions_true() { + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + let expr = case_when( + gt(get_item("value", root()), lit(0i32)), + lit(100i32), + lit(0i32), + ); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[100, 100, 100, 100, 100]); + } + + #[test] + fn test_evaluate_with_literal_condition() { + let test_array = buffer![1i32, 2, 3].into_array(); + let expr = case_when(lit(true), lit(100i32), lit(0i32)); + let result = evaluate_expr(&expr, &test_array); + + if let Some(constant) = result.as_constant() { + assert_eq!(constant, Scalar::from(100i32)); + } else { + let prim = result.to_primitive(); + assert_eq!(prim.as_slice::(), &[100, 100, 100]); + } + } + + #[test] + fn test_evaluate_with_bool_column_result() { + let test_array = + StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())]) + .unwrap() + .into_array(); + + let expr = case_when( + gt(get_item("value", root()), lit(2i32)), + lit(true), + lit(false), + ); + + let result = evaluate_expr(&expr, &test_array).to_bool(); + assert_eq!( + result.to_bit_buffer().iter().collect::>(), + vec![false, false, true, true, true] + ); + } + + #[test] + fn test_evaluate_with_nullable_condition() { + let test_array = StructArray::from_fields(&[( + "cond", + BoolArray::from_iter([Some(true), None, Some(false), None, Some(true)]).into_array(), + )]) + .unwrap() + .into_array(); + + let expr = case_when(get_item("cond", root()), lit(100i32), lit(0i32)); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[100, 0, 0, 0, 100]); + } + + #[test] + fn test_evaluate_with_nullable_result_values() { + let test_array = StructArray::from_fields(&[ + ("value", buffer![1i32, 2, 3, 4, 5].into_array()), + ( + "result", + PrimitiveArray::from_option_iter([Some(10), None, Some(30), Some(40), Some(50)]) + .into_array(), + ), + ]) + .unwrap() + .into_array(); + + let expr = case_when( + gt(get_item("value", root()), lit(2i32)), + get_item("result", root()), + lit(0i32), + ); + + let result = evaluate_expr(&expr, &test_array); + let prim = result.to_primitive(); + assert_eq!(prim.as_slice::(), &[0, 0, 30, 40, 50]); + } + + #[test] + fn test_evaluate_with_all_null_condition() { + let test_array = StructArray::from_fields(&[( + "cond", + BoolArray::from_iter([None, None, None]).into_array(), + )]) + .unwrap() + .into_array(); + + let expr = case_when(get_item("cond", root()), lit(100i32), lit(0i32)); + + let result = evaluate_expr(&expr, &test_array).to_primitive(); + assert_eq!(result.as_slice::(), &[0, 0, 0]); + } + + // Note: Direct execute tests are covered through apply+execute tests above. + + // Note: The binary CASE WHEN implementation using `zip` does NOT provide + // short-circuit/lazy evaluation. All child expressions are evaluated first, + // then zip selects the result based on the condition. This means expressions + // like divide-by-zero will still fail even if protected by a condition. + // This is intentional - lazy evaluation would require a more complex + // implementation that filters the input before evaluating children. +} diff --git a/vortex-array/src/expr/exprs/mod.rs b/vortex-array/src/expr/exprs/mod.rs index 145d225bcae..fa3e8766f31 100644 --- a/vortex-array/src/expr/exprs/mod.rs +++ b/vortex-array/src/expr/exprs/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod between; pub(crate) mod binary; +pub(crate) mod case_when; pub(crate) mod cast; pub(crate) mod dynamic; pub(crate) mod get_item; @@ -19,6 +20,7 @@ pub(crate) mod root; pub(crate) mod select; pub use between::*; pub use binary::*; +pub use case_when::*; pub use cast::*; pub use dynamic::*; pub use get_item::*; diff --git a/vortex-array/src/expr/session.rs b/vortex-array/src/expr/session.rs index 13106c354c4..f46546481d7 100644 --- a/vortex-array/src/expr/session.rs +++ b/vortex-array/src/expr/session.rs @@ -8,6 +8,7 @@ use vortex_session::registry::Registry; use crate::expr::ExprVTable; use crate::expr::exprs::between::Between; use crate::expr::exprs::binary::Binary; +use crate::expr::exprs::case_when::CaseWhen; use crate::expr::exprs::cast::Cast; use crate::expr::exprs::get_item::GetItem; use crate::expr::exprs::is_null::IsNull; @@ -55,6 +56,7 @@ impl Default for ExprSession { for expr in [ ExprVTable::new_static(&Between), ExprVTable::new_static(&Binary), + ExprVTable::new_static(&CaseWhen), ExprVTable::new_static(&Cast), ExprVTable::new_static(&GetItem), ExprVTable::new_static(&IsNull), diff --git a/vortex-datafusion/src/convert/exprs.rs b/vortex-datafusion/src/convert/exprs.rs index 4ac557f00fd..2c83f476260 100644 --- a/vortex-datafusion/src/convert/exprs.rs +++ b/vortex-datafusion/src/convert/exprs.rs @@ -34,6 +34,7 @@ use vortex::expr::get_item; use vortex::expr::is_null; use vortex::expr::list_contains; use vortex::expr::lit; +use vortex::expr::nested_case_when; use vortex::expr::not; use vortex::expr::pack; use vortex::expr::root; @@ -114,6 +115,45 @@ impl DefaultExpressionConvertor { scalar_fn.name() )) } + + /// Attempts to convert a DataFusion CaseExpr to a Vortex expression. + fn try_convert_case_expr(&self, case_expr: &df_expr::CaseExpr) -> DFResult { + // DataFusion CaseExpr has: + // - expr(): Optional base expression (for "CASE expr WHEN ..." form) + // - when_then_expr(): Vec of (when, then) pairs + // - else_expr(): Optional else expression + + // We don't support the "CASE expr WHEN value1 THEN result1" form yet + if case_expr.expr().is_some() { + return Err(exec_datafusion_err!( + "CASE expr WHEN form is not yet supported, only searched CASE is supported" + )); + } + + let when_then_pairs = case_expr.when_then_expr(); + if when_then_pairs.is_empty() { + return Err(exec_datafusion_err!( + "CASE expression must have at least one WHEN clause" + )); + } + + // Convert all when/then pairs to (condition, value) tuples + let mut pairs = Vec::with_capacity(when_then_pairs.len()); + for (when_expr, then_expr) in when_then_pairs { + let condition = self.convert(when_expr.as_ref())?; + let value = self.convert(then_expr.as_ref())?; + pairs.push((condition, value)); + } + + // Convert optional else expression + let else_value = case_expr + .else_expr() + .map(|e| self.convert(e.as_ref())) + .transpose()?; + + // Use nested_case_when which converts to nested binary case_when expressions + Ok(nested_case_when(pairs, else_value)) + } } impl ExpressionConvertor for DefaultExpressionConvertor { @@ -205,6 +245,10 @@ impl ExpressionConvertor for DefaultExpressionConvertor { return self.try_convert_scalar_function(scalar_fn); } + if let Some(case_expr) = df.as_any().downcast_ref::() { + return self.try_convert_case_expr(case_expr); + } + Err(exec_datafusion_err!( "Couldn't convert DataFusion physical {df} expression to a vortex expression" )) @@ -350,10 +394,12 @@ fn can_be_pushed_down_impl(df_expr: &Arc, schema: &Schema) -> && can_be_pushed_down_impl(like.pattern(), schema) } else if let Some(lit) = expr.downcast_ref::() { supported_data_types(&lit.value().data_type()) - } else if expr.downcast_ref::().is_some() - || expr.downcast_ref::().is_some() - { - true + } else if let Some(cast_expr) = expr.downcast_ref::() { + // CastExpr child must be an expression type that convert() can handle + is_convertible_expr(cast_expr.expr()) + } else if let Some(cast_col_expr) = expr.downcast_ref::() { + // CastColumnExpr child must be an expression type that convert() can handle + is_convertible_expr(cast_col_expr.expr()) } else if let Some(is_null) = expr.downcast_ref::() { can_be_pushed_down_impl(is_null.arg(), schema) } else if let Some(is_not_null) = expr.downcast_ref::() { @@ -366,12 +412,39 @@ fn can_be_pushed_down_impl(df_expr: &Arc, schema: &Schema) -> .all(|e| can_be_pushed_down_impl(e, schema)) } else if let Some(scalar_fn) = expr.downcast_ref::() { can_scalar_fn_be_pushed_down(scalar_fn) + } else if let Some(case_expr) = expr.downcast_ref::() { + can_case_be_pushed_down(case_expr, schema) } else { tracing::debug!(%df_expr, "DataFusion expression can't be pushed down"); false } } +/// Checks if an expression type is one that convert() can handle. +/// This is less restrictive than can_be_pushed_down since it only checks +/// expression types, not data type support. +fn is_convertible_expr(df_expr: &Arc) -> bool { + let expr = df_expr.as_any(); + + // Expression types that convert() handles + expr.downcast_ref::().is_some() + || expr.downcast_ref::().is_some() + || expr.downcast_ref::().is_some() + || expr.downcast_ref::().is_some() + || expr + .downcast_ref::() + .is_some_and(|e| is_convertible_expr(e.expr())) + || expr + .downcast_ref::() + .is_some_and(|e| is_convertible_expr(e.expr())) + || expr.downcast_ref::().is_some() + || expr.downcast_ref::().is_some() + || expr.downcast_ref::().is_some() + || expr + .downcast_ref::() + .is_some_and(|sf| ScalarFunctionExpr::try_downcast_func::(sf).is_some()) +} + fn can_binary_be_pushed_down(binary: &df_expr::BinaryExpr, schema: &Schema) -> bool { let is_op_supported = try_operator_from_df(binary.op()).is_ok(); is_op_supported @@ -379,6 +452,32 @@ fn can_binary_be_pushed_down(binary: &df_expr::BinaryExpr, schema: &Schema) -> b && can_be_pushed_down_impl(binary.right(), schema) } +fn can_case_be_pushed_down(case_expr: &df_expr::CaseExpr, schema: &Schema) -> bool { + // We only support the "searched CASE" form (CASE WHEN cond THEN result ...) + // not the "simple CASE" form (CASE expr WHEN value THEN result ...) + if case_expr.expr().is_some() { + return false; + } + + // Check all when/then pairs + for (when_expr, then_expr) in case_expr.when_then_expr() { + if !can_be_pushed_down_impl(when_expr, schema) + || !can_be_pushed_down_impl(then_expr, schema) + { + return false; + } + } + + // Check the optional else clause + if let Some(else_expr) = case_expr.else_expr() + && !can_be_pushed_down_impl(else_expr, schema) + { + return false; + } + + true +} + fn supported_data_types(dt: &DataType) -> bool { use DataType::*; @@ -412,7 +511,8 @@ fn supported_data_types(dt: &DataType) -> bool { is_supported } -/// Checks if a GetField scalar function can be pushed down. +/// Checks if a scalar function can be pushed down. +/// Currently only GetFieldFunc is supported. fn can_scalar_fn_be_pushed_down(scalar_fn: &ScalarFunctionExpr) -> bool { ScalarFunctionExpr::try_downcast_func::(scalar_fn).is_some() } @@ -747,4 +847,96 @@ mod tests { assert!(!can_be_pushed_down_impl(&like_expr, &test_schema)); } + + /// Test that applying a CASE expression to an Arrow RecordBatch using DataFusion + /// matches the result of applying the converted Vortex expression. + #[test] + fn test_case_when_datafusion_vortex_equivalence() { + use datafusion::arrow::array::Int32Array; + use datafusion::arrow::array::RecordBatch; + use datafusion_physical_expr::expressions::CaseExpr; + use vortex::VortexSessionDefault; + use vortex::array::ArrayRef; + use vortex::array::Canonical; + use vortex::array::VortexSessionExecute as _; + use vortex::array::arrow::FromArrowArray; + use vortex::session::VortexSession; + + // Create test data + let values = Arc::new(Int32Array::from(vec![1, 5, 10, 15, 20])); + let schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Int32, + false, + )])); + let batch = RecordBatch::try_new(schema, vec![values]).unwrap(); + + // Build a DataFusion CASE expression: + // CASE WHEN value > 10 THEN 100 WHEN value > 5 THEN 50 ELSE 0 END + let col_value = Arc::new(df_expr::Column::new("value", 0)) as Arc; + let lit_10 = + Arc::new(df_expr::Literal::new(ScalarValue::Int32(Some(10)))) as Arc; + let lit_5 = + Arc::new(df_expr::Literal::new(ScalarValue::Int32(Some(5)))) as Arc; + let lit_100 = + Arc::new(df_expr::Literal::new(ScalarValue::Int32(Some(100)))) as Arc; + let lit_50 = + Arc::new(df_expr::Literal::new(ScalarValue::Int32(Some(50)))) as Arc; + let lit_0 = + Arc::new(df_expr::Literal::new(ScalarValue::Int32(Some(0)))) as Arc; + + // WHEN value > 10 THEN 100 + let when1 = Arc::new(df_expr::BinaryExpr::new( + col_value.clone(), + DFOperator::Gt, + lit_10, + )) as Arc; + // WHEN value > 5 THEN 50 + let when2 = Arc::new(df_expr::BinaryExpr::new(col_value, DFOperator::Gt, lit_5)) + as Arc; + + let case_expr = + CaseExpr::try_new(None, vec![(when1, lit_100), (when2, lit_50)], Some(lit_0)).unwrap(); + + // Apply DataFusion expression + let df_result = case_expr.evaluate(&batch).unwrap(); + let df_array = df_result.into_array(batch.num_rows()).unwrap(); + + // Convert to Vortex expression + let expr_convertor = DefaultExpressionConvertor::default(); + let vortex_expr = expr_convertor.try_convert_case_expr(&case_expr).unwrap(); + + // Convert batch to Vortex array + let vortex_array: ArrayRef = ArrayRef::from_arrow(&batch, false).unwrap(); + + // Apply Vortex expression + let session = VortexSession::default(); + let mut ctx = session.create_execution_ctx(); + let vortex_result = vortex_array + .apply(&vortex_expr) + .unwrap() + .execute::(&mut ctx) + .unwrap(); + + // Convert back to Arrow for comparison + let vortex_as_arrow = vortex_result.into_primitive().as_slice::().to_vec(); + + // Convert DataFusion result to Vec for comparison + let df_as_arrow: Vec = df_array + .as_any() + .downcast_ref::() + .unwrap() + .values() + .to_vec(); + + // Compare results + // Expected: [0, 0, 50, 100, 100] for values [1, 5, 10, 15, 20] + // value=1: not > 10, not > 5 -> ELSE 0 + // value=5: not > 10, not > 5 -> ELSE 0 + // value=10: not > 10, > 5 -> 50 + // value=15: > 10 -> 100 + // value=20: > 10 -> 100 + assert_eq!(df_as_arrow, vec![0, 0, 50, 100, 100]); + assert_eq!(vortex_as_arrow, df_as_arrow); + } } diff --git a/vortex-proto/proto/expr.proto b/vortex-proto/proto/expr.proto index 4540bce0a63..7d713a3afb3 100644 --- a/vortex-proto/proto/expr.proto +++ b/vortex-proto/proto/expr.proto @@ -80,3 +80,9 @@ message SelectOpts { FieldNames exclude = 2; } } + +// Options for `vortex.case_when` +message CaseWhenOpts { + uint32 num_when_then_pairs = 1; + bool has_else = 2; +} diff --git a/vortex-proto/src/generated/vortex.expr.rs b/vortex-proto/src/generated/vortex.expr.rs index f3b6d2cf624..180e693f269 100644 --- a/vortex-proto/src/generated/vortex.expr.rs +++ b/vortex-proto/src/generated/vortex.expr.rs @@ -145,3 +145,11 @@ pub mod select_opts { Exclude(super::FieldNames), } } +/// Options for `vortex.case_when` +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct CaseWhenOpts { + #[prost(uint32, tag = "1")] + pub num_when_then_pairs: u32, + #[prost(bool, tag = "2")] + pub has_else: bool, +}