{"_id":"@aeye/query","_rev":"11-1f96dff9cf63b61c1c3cac50750e5af1","name":"@aeye/query","dist-tags":{"latest":"0.6.6"},"versions":{"0.3.9":{"name":"@aeye/query","version":"0.3.9","license":"GPL-3.0","_id":"@aeye/query@0.3.9","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"ec8c8eebbdc07f4f1d78ba1a72cceaf4b66306c7","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.3.9.tgz","fileCount":4,"integrity":"sha512-+LnrJKHvncuvAajtUvwTIWF+KosbVvItttjzQ16CU5Ngbg+nCRxhbiGjMWNJpx0HmUer5AmQrckCPnN1otdObw==","signatures":[{"sig":"MEUCIQCApxhwzJOJohDfSOWWMq3kAXs6z5Bz7KHhW0DoMGAB3AIgCfGqkMPBDrQ1qRRN9didBYNIwCApNa1MCqh+2V693N4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":764648},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"7df3bae899c5a30cf17f4925fb7057e9a61e7332","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.3.9_1783858178816_0.5169536550120966","host":"s3://npm-registry-packages-npm-production"}},"0.3.10":{"name":"@aeye/query","version":"0.3.10","license":"GPL-3.0","_id":"@aeye/query@0.3.10","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"edda700c2c3247f6a66b222fbf3ff6e4d4b7667a","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.3.10.tgz","fileCount":5,"integrity":"sha512-jaSiJxFJa5FJRZEaCKFd3FS8zEWClLArT7T9YV818bUOOsbtHjv1XmEx/beluATP9SCloZaewj7bj6KC0506wg==","signatures":[{"sig":"MEUCIQCWnn8xoswD0PEJQFNqMzEKVQG8PRSyHj5LX5S0wklzYwIgMe2a8SiV1BzAI7eJ78PKAWgkMJXuk911EJKbX08XjEI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1290577},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"0616ffebc1a2007753e4c05d8917a6ab9f5bf2f3","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.3.10_1784260982337_0.8838438303927263","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@aeye/query","version":"0.4.0","license":"GPL-3.0","_id":"@aeye/query@0.4.0","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"d459e06885c1ca1420b2f535ff02279e64d9f5d5","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.4.0.tgz","fileCount":5,"integrity":"sha512-sZkOrjg2wLD5IOHYyLrKhsQAC8RK3I0aQ2dfUL0vwzSxscsOxCT36ukIbpvMBfMWBgxkDAJ1oBsvV4j4lS8dHg==","signatures":[{"sig":"MEUCIQDLLK9rclp8ZADirktSIMzZTx+crocTDWcJJ6gZMNHY0AIgcOkWHeMf27Of30rtI0yjxcksjgIqOBGYo1PnKxRpiC4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1309227},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"16d78d116c267851bd22509d86d243bf4ac56cab","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.4.0_1784324106889_0.04702562647586306","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@aeye/query","version":"0.5.0","license":"GPL-3.0","_id":"@aeye/query@0.5.0","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"c213c6fb46386cb35037d928496c98f4296e3bed","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.5.0.tgz","fileCount":5,"integrity":"sha512-KSzaiIQqkU+wbnWU/K6H7Q8EKPAsSlfKsVVMjZfehHokCNLsbLZiwk3v3EYXTdHBUOuOaR8H7r+U4l5UThIZ7Q==","signatures":[{"sig":"MEUCIQC9xBcryoRxAGvwtGLjRdH9jW5FZp5cchsCb2OQOMvAugIgU5uvSVX4J61mibbPxmyIpxoVT+b98AV/V34TK/QZpsU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1329692},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"6f422ae6ede3e2482d6ee09af859a4118d7b943d","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.5.0_1785166821860_0.09455285405915492","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@aeye/query","version":"0.6.0","license":"GPL-3.0","_id":"@aeye/query@0.6.0","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"0449a2ef157357c1fa3ca8d4cca40b1600a35e65","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.0.tgz","fileCount":5,"integrity":"sha512-M+xXGFUAXX54eERuYtplcsJ29+J3nYyE2mVFa6FbxmQygLfLxvPCoew5DQDe4/tCMZyFnzaqol5IdUZpWLFpdA==","signatures":[{"sig":"MEQCIEeTfi/YgvLRn48kNEmm1mAOcY98AC5ub/XkkESJ+i7sAiAD81zSnVoWtWUEud+COalMx+bcIGdt//H+MGJ3KovP3Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1374357},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"769cacbbcad067e575ba20b6ae7dd280bb39760a","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.6.0_1785674920471_0.7829325736905979","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@aeye/query","version":"0.6.1","license":"GPL-3.0","_id":"@aeye/query@0.6.1","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"64e50b8dc9a5a3bb867594186502c7ae77efdf1c","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.1.tgz","fileCount":5,"integrity":"sha512-aMxXPIyZfZ2oR85H9qTh5869m4+glLMDvPvrfCNlfF6bjPy1Vbez1pdaaOODnAbSLxmORX4zzWzefxzoj4upKw==","signatures":[{"sig":"MEUCICJQK/BCOIgalccIU4KDZM3pMjeLiAsIJ9y19tCv5imOAiEA1jLSoqeFGKfYOwS4CwVHyIfoL7UT/dH95bNTfLSiXeo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1393194},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"3e32c16123e1c176fefcd0ed543d71bac0b0946d","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.6.1_1785689989984_0.014923038765740415","host":"s3://npm-registry-packages-npm-production"}},"0.6.2":{"name":"@aeye/query","version":"0.6.2","license":"GPL-3.0","_id":"@aeye/query@0.6.2","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"09cab0393c00c39d754cc801058821224be4f72b","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.2.tgz","fileCount":5,"integrity":"sha512-qh996E6wYs1y4hzqQQX77n4oDvI3QuBsDOJLTllgG/yUJ+5riLWaa9BFinjME1zrKGp/yLPjVjnkOBs7MrqtnQ==","signatures":[{"sig":"MEUCIQD0k4TPdmr5BkQ1qJE/N5TPOSHpoer8Em6ylzQ2wdGyfgIgeZm75q/uyPn/AZB8CEXOTl8d5KGPbCW2nW/kyANeFSs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1398854},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"7cd7d03029f2d40e78e46e7759af716ff8cd3902","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.6.2_1786021144458_0.606199384890161","host":"s3://npm-registry-packages-npm-production"}},"0.6.3":{"name":"@aeye/query","version":"0.6.3","license":"GPL-3.0","_id":"@aeye/query@0.6.3","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"a6753658fd3104a1713fba74439d38e23f324d81","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.3.tgz","fileCount":5,"integrity":"sha512-dtoA65NxhFFhehxK2L3dxw2tIPCa7WG2L6pknntkCNhzNSMsjBK3wmV+9ShFlseCNY3XJCePWUAZvy7qKo7YKg==","signatures":[{"sig":"MEUCICoDRuxTd4zNKg7UMxszDN/7HbrQvCAPOWzHAC28vcpnAiEAzEQypVg1Z40RbASetbirUME0b1nQwiEYBybXLY0tUDE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1408358},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"5c2e0bb3b9df195e5ccb687573df26a63326e9ef","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.6.3_1786149188190_0.46165597730247576","host":"s3://npm-registry-packages-npm-production"}},"0.6.4":{"name":"@aeye/query","version":"0.6.4","license":"GPL-3.0","_id":"@aeye/query@0.6.4","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"4cf4657845539a2249ac367b2292ebeb7e18e1e0","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.4.tgz","fileCount":5,"integrity":"sha512-ZbFNE0Zg77E0NrGuvh9B2gNk+FYnhEzxwKXLud0SVIawTYRzsFRZp4cM1YKIYSAemi5UjyfeEJ6Lxr6S9bK34Q==","signatures":[{"sig":"MEUCIBcNevaUqDBu3Q/kSiBw12Kki/hWgfx+6CgjJf22uZCHAiEAu2uqbek2n+ekoLhsPQZCKYXlpvKAknPsueoyXC3YtNE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1411166},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"ba19fe8e8b03856e1dce8a61f7458ac7a4d89d13","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.6.4_1786202271105_0.9650344407669631","host":"s3://npm-registry-packages-npm-production"}},"0.6.5":{"name":"@aeye/query","version":"0.6.5","license":"GPL-3.0","_id":"@aeye/query@0.6.5","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"dist":{"shasum":"bfe0a2c0546cddf037f14968d289626e1206cbff","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.5.tgz","fileCount":5,"integrity":"sha512-lNWsxeAYq05oUbedleIs9DzT3lIFOUyaw1pKvgWJTEHbNQ+BqI2hrReXdXIFHgVfywkx23aWfcGkk/14VBLcQQ==","signatures":[{"sig":"MEQCIDkid4OVTTjhxGasY2RxZpfIHySH9QJhdh+WOFl/Vuy8AiBuVf3n1pwzNw07G2JfOiP+NIxq4QFeF4t65onoBZ58AQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1448380},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"source":"./src/index.ts","default":"./dist/index.js"}},"gitHead":"30df6fd8644fbe03132cd3b2b96a683cea54dc1e","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"tmp":"tmp/query_0.6.5_1786297590059_0.6341720754674831","host":"s3://npm-registry-packages-npm-production"}},"0.6.6":{"_id":"@aeye/query@0.6.6","dist":{"shasum":"3b4980c488a15d4dc722aa2b0148d20d21f96254","tarball":"https://registry.npmjs.org/@aeye/query/-/query-0.6.6.tgz","fileCount":5,"integrity":"sha512-n6IYRTNi3TsC3DLyiKbNtDgyuGNMhLuzc33eWmWqka6+PO3JNQ7UwyytevInhDz9bdfpJwNqE3uS/AWEHJRObQ==","signatures":[{"sig":"MEUCIGJ9OBDa9VbInnEqzeAE1YG74yMSnJ+CPflWlHKYXz8kAiEA2Ig/7gwEEooh8v/8DBvnwBSK9vA1+Ow5K0IXIuRA5mE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCXlSPq/0jtWV1MKcok6JB7+fvwxN1sw9SWMcVfZnlUngIgRFz06IZdcxf2cOMU8qBzIWOWWAe1GUIZP10c5W5Nzi0="}],"unpackedSize":1880215},"main":"dist/index.js","name":"@aeye/query","type":"module","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js"},"./conformance":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js"}},"gitHead":"b684796e9cc434f39425d3b2d5abc8ba65a86d8c","license":"GPL-3.0","scripts":{"cli":"tsx examples/cli.ts","test":"vitest run","build":"tsup src/index.ts --format esm --dts && tsx scripts/check-dist.mjs","clean":"rimraf dist","examples":"tsx examples/examples.ts","typecheck":"tsc --noEmit","test:watch":"vitest","integration":"tsx integration/run.ts","test:coverage":"vitest run --coverage","integration:check":"tsx integration/run.ts --check"},"version":"0.6.6","_npmUser":{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"},"_npmVersion":"10.9.8","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","directories":{},"maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.1.12","@aeye/core":"^0.3.9"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","tsup":"^8.0.0","vitest":"^3.0.0","@aeye/ai":"^0.3.9","@aeye/aws":"^0.3.9","typescript":"^5.9.3","@aeye/models":"^0.3.9","@aeye/openai":"^0.3.9","@aeye/openrouter":"^0.3.9","@vitest/coverage-v8":"^3.2.4"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/query_0.6.6_1787273440817_0.2487147107478207"}}},"time":{"created":"2026-07-12T12:09:38.692Z","modified":"2026-08-21T00:50:41.136Z","0.3.9":"2026-07-12T12:09:39.019Z","0.3.10":"2026-07-17T04:03:02.542Z","0.4.0":"2026-07-17T21:35:07.116Z","0.5.0":"2026-07-27T15:40:22.041Z","0.6.0":"2026-08-02T12:48:40.844Z","0.6.1":"2026-08-02T16:59:50.143Z","0.6.2":"2026-08-06T12:59:04.623Z","0.6.3":"2026-08-08T00:33:08.379Z","0.6.4":"2026-08-08T15:17:51.251Z","0.6.5":"2026-08-09T17:46:30.223Z","0.6.6":"2026-08-21T00:50:40.963Z"},"license":"GPL-3.0","description":"Query - An LLM-friendly relational query language, runtime & SQL converter","maintainers":[{"name":"philip.diffenderfer","email":"pdiffenderfer@gmail.com"}],"readme":"# @aeye/query\r\n\r\nAn **LLM-friendly relational query language**, in-memory runtime, and SQL\r\nconverter. You define *Types* (type-like entities) with *Fields*; from that an\r\nLLM (or a developer) can build a **typed, validated, runnable** query — a\r\n`select` / `insert` / `update` / `delete` / set-operation / CTE / single\r\nexpression. A built query resolves to an output type, has typed bind params,\r\ncan be cost-bounded, run in-memory, converted to SQL (base + Postgres),\r\nauto-paginated, and **drilled down** (aggregate un-ravelling).\r\n\r\nIt is fully standalone (only depends on `zod`) and obsessively type-safe: no\r\n`any`, no `unknown` in public APIs, no casts; every polymorphic node is a\r\ndiscriminated union so handling is exhaustively checkable.\r\n\r\n```bash\r\nnpm install        # from the monorepo root (workspace)\r\nnpm run typecheck  # tsc --noEmit\r\nnpm test           # vitest\r\nnpm run examples   # runnable, end-to-end tour (examples/)\r\n```\r\n\r\n## The type / field model\r\n\r\nA `Type` is a named collection of `Field`s plus index + cardinality estimates.\r\nEach field has a `FieldType` (one of `number`, `text`, `money`, `bool`,\r\n`relation`, `date`, `timestamp`, `json`, `array`); nullability lives on the\r\n*field*, not the field type.\r\n\r\nAn `array` field is an ordered collection. It carries optional `minItems` /\r\n`maxItems` element-count bounds and an optional `item` element field type\r\n(omit `item` for heterogeneous / unknown elements). Because `item` is itself a\r\nfield type, arrays nest (`array<array<number>>`). `inferType` detects arrays\r\nfrom sampled rows and infers the element type from homogeneous scalars.\r\n\r\n```ts\r\n// array of text tags, 0–8 items\r\n{ name: 'tags', type: { kind: 'array', item: { kind: 'text' }, maxItems: 8 }, nullable: true }\r\n```\r\n\r\nRelations carry a target Type and a cardinality `count`: a relation field's\r\n**name is the join key** for all purposes — `count === 1` is belongs-to (the FK\r\nlives on this type), `count > 1` is has-many. There are no explicit FK fields.\r\nA belongs-to relation may set `inverseRelation` to have its target Type\r\nautomatically gain the matching has-many field pointing back. The join key on\r\neither side resolves through each Type's *identity field* (the field of its\r\nfirst unique single-field index, else the field named `id`).\r\n\r\nA relation field can be **compared directly to a value** in `= <> in notIn`,\r\nwhere the value is an object keyed by the target's **primary key** (single or\r\ncomposite) — `{ kind: 'comparison', op: '=', left: relRef, right: { kind:\r\n'param', name: 'u' } }` with `:u = { id: 5 }` (a single-key relation also\r\naccepts a bare scalar). A **belongs-to** matches its FK columns; a **has-many**\r\nmatches by **membership** — `= value` is a correlated `EXISTS` testing that the\r\nvalue's key is in the related set (`<>` / `notIn` → `NOT EXISTS`). Two relations\r\nof the same target compare by their FK key (`order.customer = invoice.customer`);\r\na has-many may not be compared to another relation. Ordering / LIKE on a relation\r\nis rejected — a relation compares by identity, not order.\r\n\r\nIndexes are **composite**: an ordered list of parts, each with a prefix\r\ndistinct-row `count` (non-increasing); the index is unique iff its last part's\r\n`count === 1`. Text matching is governed by a text field's `casing`, else the engine's\r\n`textCasing` default (`'fold'` unless you set it):\r\n\r\n| `casing`     | means                                          | SQL for `a = b`       | in-memory runtime |\r\n|--------------|------------------------------------------------|-----------------------|-------------------|\r\n| `'fold'`     | case-INSENSITIVE, folded by the query           | `LOWER(a) = LOWER(b)` | folds             |\r\n| `'collated'` | case-INSENSITIVE, folded by the COLUMN's collation | `a = b`            | folds             |\r\n| `'exact'`    | case-SENSITIVE                                  | `a = b`               | compares as-is    |\r\n\r\n`LOWER(col)` is not sargable, so `'fold'` costs every predicate over the column\r\nits index — and on Postgres a column modelled as `text` may physically be a\r\n`uuid`, where `LOWER()` does not exist at all. So set `textCasing: 'exact'` on\r\nthe engine for a schema of identifiers / codes / enums (`'collated'` when the\r\ncolumns really do carry a case-insensitive collation, e.g. `citext`), and let\r\nthe individual columns that DO want folding say `casing: 'fold'` — a field's own\r\ndeclaration always wins over the engine default. A Type may also be\r\nflagged `semantic` / `search` to make it eligible for embedding similarity /\r\nfull-text search even when no individual field is flagged.\r\n\r\n```ts\r\nimport { createRegistry, QueryEngine, arrayExecutor, type TypeDef } from '@aeye/query';\r\n\r\nconst userDef: TypeDef = {\r\n  name: 'user',\r\n  fields: [\r\n    { name: 'id', type: { kind: 'number', whole: true } },\r\n    { name: 'name', type: { kind: 'text' } },\r\n    { name: 'age', type: { kind: 'number', whole: true }, nullable: true },\r\n  ],\r\n  // unique single-field index on `id` ⇒ `id` is the identity / join key.\r\n  indexes: [{ exprs: [{ expr: { kind: 'field-ref', source: 'user', field: 'id' }, count: 1 }] }],\r\n  count: 1000,\r\n  bytes: 64,\r\n};\r\n\r\nconst orderDef: TypeDef = {\r\n  name: 'order',\r\n  fields: [\r\n    { name: 'id', type: { kind: 'number', whole: true } },\r\n    // belongs-to user; materializes `user.orders` (has-many) pointing back.\r\n    { name: 'userId', type: { kind: 'relation', to: 'user', count: 1, inverseRelation: 'orders' } },\r\n  ],\r\n  indexes: [{ exprs: [{ expr: { kind: 'field-ref', source: 'order', field: 'id' }, count: 1 }] }],\r\n  count: 5000,\r\n  bytes: 48,\r\n};\r\n\r\nconst registry = createRegistry();\r\nconst user = registry.parseType(userDef);\r\nconst order = registry.parseType(orderDef);\r\nregistry.registerType(user);\r\nregistry.registerType(order);\r\n\r\nconst engine = new QueryEngine(registry, {\r\n  executors: { user: arrayExecutor(userRows) }, // wire data for in-memory runs\r\n  textCasing: 'exact',                          // default case policy for text columns that declare none\r\n});\r\n```\r\n\r\nYou can also **infer** a `TypeDef` straight from raw JSON rows:\r\n\r\n```ts\r\nimport { inferType } from '@aeye/query';\r\nconst def = inferType('user', userRows); // field types + nullability inferred\r\n```\r\n\r\n## Build, validate, and run a query\r\n\r\nA query is plain JSON (a `QueryDef`). Validate it to get LLM-friendly\r\n`Problems`, then run it in-memory.\r\n\r\n```ts\r\nconst select = {\r\n  kind: 'select',\r\n  fields: [{ expr: { kind: 'field-ref', source: 'user', field: 'name' } }],\r\n  from: { kind: 'type', type: 'user' },\r\n  where: [{\r\n    kind: 'comparison', op: '>',\r\n    left: { kind: 'field-ref', source: 'user', field: 'age' },\r\n    right: { kind: 'literal', value: 30 },\r\n  }],\r\n} as const;\r\n\r\nconst problems = engine.validateQuery(select);   // structure + params + per-Type hooks\r\nif (!problems.hasErrors) {\r\n  const result = await engine.run(select);        // { rows, fields, outputType }\r\n}\r\n```\r\n\r\n### Building expressions with `e.*`\r\n\r\nHand-writing the raw `ExprDef` JSON above gets verbose fast. The **`e.*`\r\nbuilder** composes the same trees with terse, fully-typed function calls — and\r\neach `e.*` returns a **real `Expr` instance** (the exact subclass), so it is\r\nstrictly more capable than a def factory:\r\n\r\n```ts\r\nimport { e } from '@aeye/query';\r\n\r\n// e.eq(...) is a ComparisonExpr, e.and(...) a LogicalExpr, e.ref(...) a FieldRefExpr, …\r\nconst cond = e.and(\r\n  e.eq(e.ref('task', 'done'), e.value(true)),\r\n  e.gt(e.ref('task', 'hours'), e.value(0)),\r\n);\r\n```\r\n\r\nThere is **one builder per expression kind**, grouped as: leaves (`value`/`lit`,\r\n`param`, `ref`, `path`, `output`, `excluded`, `filters`), arithmetic\r\n(`add`/`sub`/`mul`/`div`/`mod`, `neg`/`pos`), comparison\r\n(`eq`/`neq`/`lt`/`lte`/`gt`/`gte`/`like`/`notLike`/`ilike`), logical\r\n(`and`/`or`/`not`), predicates (`isNull`/`notNull`, `between`/`notBetween`,\r\n`inList`/`notInList`, `inSubquery`/`notInSubquery`, `exists`/`notExists`), array\r\nops (`contains`/`containsAny`/`containsAll`/`isEmpty`/`notEmpty`), `case`/`when`,\r\ncalls (`fn`, `agg`/`count`/`countStar`/`sum`/`avg`/`min`/`max`, `window`,\r\n`tableFn`), `subquery`, and search (`textSearch`, `semantic`). Every function is\r\nalso a named export (`import { eq, and, ref } from '@aeye/query'`).\r\n\r\n**Run or emit a built expr standalone** — the engine normalizes either an `Expr`\r\nor a raw `ExprDef`:\r\n\r\n```ts\r\n// Evaluate against a row (defaults to an empty row for constant predicates):\r\nconst v = await engine.evaluateExpr(e.gt(e.ref('task', 'hours'), e.value(0)), {\r\n  task: { hours: 5 },\r\n});                                         // Value(true)\r\n\r\n// Emit SQL + ordered bind params for a dialect (params never interpolated):\r\nconst { sql, params } = engine.exprToSQL(cond, 'postgres');\r\n// sql:    (\"task\".\"done\" = $1 AND \"task\".\"hours\" > $2)\r\n// params: [true, 0]\r\n```\r\n\r\n**Embed a built expr into a query** via `.toJSON()` — a query def's `where` /\r\n`order` / field slots are `ExprDef`, and `.toJSON()` is the free wire form:\r\n\r\n```ts\r\nconst select = {\r\n  kind: 'select',\r\n  fields: [{ expr: e.ref('user', 'name').toJSON() }],\r\n  from: { kind: 'type', type: 'user' },\r\n  where: [e.gt(e.ref('user', 'age'), e.value(30)).toJSON()],\r\n} as const;\r\n```\r\n\r\n`registry.parseExpr` is a **pass-through** for an already-built `Expr`, so built\r\nand parsed exprs compose freely.\r\n\r\n### Sources & aliasing\r\n\r\nEvery source is referenced by its **type name** — there is no alias to invent or\r\nkeep in sync:\r\n\r\n- **FROM.** `from: { kind: 'type', type: 'user' }` binds under `source: 'user'`.\r\n- **Joins.** A join crosses a **single relation field** — `on` is a\r\n  `{ kind: 'relation', source, field, as }` ref\r\n  (`{ on: { kind: 'relation', source: 'user', field: 'orders', as: 'order' } }`):\r\n  the bound source to join FROM, its relation field, and the **required alias**\r\n  the joined rows bind under (field-refs into them then use `source: 'order'`).\r\n  **Multi-hop** joins are expressed as **chained** single-hop joins, and reading a\r\n  value across a relation is a join + a plain `{ source, field }` field-ref (there\r\n  is no separate relation-path expr). The relation key is synthesized — you never\r\n  write ON. A join can also add a **fresh source** — `on` may instead be a\r\n  `type` / `aliased` / `subquery` / `function` source, with `and` as its ON (a\r\n  manual join). `joinType` (freeing `type` for the Type-name rule) defaults to\r\n  `left`.\r\n- **DML.** `update` / `delete` / `insert` target a type by name (`type` / `from`\r\n  / `into`) and bind it under that name — DML targets take no alias.\r\n\r\nWhen you need a distinct binding — a **self-join**, or **two instances of the\r\nsame Type** — reach for the `aliased` escape hatch on a FROM source\r\n(`{ kind: 'aliased', type: 'user', as: 'u1' }`) or set `as` on a join to override\r\nthe bound name of its hop (`{ on: { source: 'u1', field: 'orders' }, as: 'o1' }`).\r\nThe `as` on a join is the **collision-breaker**.\r\n\r\n> **The `type` vs `source` rule.** `type` is used only where the value MUST be a\r\n> registered Type name (FROM `type`, DML `into` / `type` / `from`, relation `to`,\r\n> a semantic query's `{ type, field }`); `source` is a **bound** name in the\r\n> query's scope (a Type name, a join alias, a CTE, an aliased source) — used by\r\n> `field-ref`, `semantic` / `text-search` / `filters`, and a join's `on.source`.\r\n\r\nIf two sources end up bound under the same name — two joins landing on one target\r\ntype, or a join hop rebinding the FROM / DML target type — the engine reports a\r\n`source.duplicate` validation error pointing you at the `aliased` form (or a join\r\n`as`) to disambiguate.\r\n\r\n> **Known limitation.** A self-referential DML that would need two instances of\r\n> its target type (e.g. an `UPDATE user` joined back to `user` via its relations,\r\n> whose hop rebinds `user`) currently errors with `source.duplicate`; an\r\n> aliased-DML target mechanism is a deferred follow-up.\r\n\r\n### Output references (`groupBy` / `orderBy` / `having`)\r\n\r\nA SELECT's `groupBy`, `order`, and `having` can reference a **projected output\r\nfield by name** instead of repeating its expression — via\r\n`{ kind: 'output', name }`. The `name` is the output's `as`, or its natural\r\nderived name (a field-ref's field, an aggregate's function name). The reference **EXPANDS to** (delegates to) the\r\nreferenced select item's expression: the SQL emits the target's SQL (portable\r\nacross dialects, in every clause), and the runtime re-evaluates the target — so\r\na group key re-computes over the source row while an ORDER BY / HAVING ref\r\nre-computes over the group (including an aggregate target). This keeps queries\r\nsmaller and removes a whole class of GROUP BY / ORDER BY mismatches.\r\n\r\n```ts\r\n// Revenue per user, grouped + ordered by output name — the `sum` / `userId`\r\n// expressions are written ONCE, in `fields`.\r\nconst revenuePerUser = {\r\n  kind: 'select',\r\n  fields: [\r\n    { expr: { kind: 'field-ref', source: 'order', field: 'userId' }, as: 'userId' },\r\n    { expr: { kind: 'aggregate', function: 'sum', args: { value: { kind: 'field-ref', source: 'order', field: 'total' } } }, as: 'revenue' },\r\n  ],\r\n  from: { kind: 'type', type: 'order' },\r\n  groupBy: [{ kind: 'output', name: 'userId' }],   // ← by output name, not the expr\r\n  having:  [{ kind: 'comparison', op: '>', left: { kind: 'output', name: 'revenue' }, right: { kind: 'literal', value: 100 } }],\r\n  order:   [{ expr: { kind: 'output', name: 'revenue' }, dir: 'desc' }],\r\n} as const;\r\n```\r\n\r\nIt is valid **only** in those three clause positions — in WHERE, a join `on`, or\r\nany general expression argument (where no outputs are bound) it fails validation\r\nwith `output.not-available`. An unknown name reports `output.unknown`, and using\r\none as a GROUP BY key whose target is an aggregate reports `output.aggregate`\r\n(you cannot group BY an aggregate). The LLM schema offers `output` in exactly\r\nthose `groupBy` / `orderBy` / `having` positions and nowhere else. `drillDown`\r\nexpands any `output` references against the original projection before it\r\nun-ravels the aggregates, so a drilled query never dangles.\r\n\r\n## Write model & permissions\r\n\r\nTypes and fields declare **what write operations are possible**, and that flows\r\ninto BOTH validation AND the LLM-facing schema — so the generated schema never\r\noffers a write the engine would reject.\r\n\r\n- **Type permissions.** `insertable` / `updatable` / `deletable` on a `TypeDef`\r\n  (each default **true**). A restricted Type is rejected by validation\r\n  (`insert.type-readonly` / `update.type-readonly` / `delete.type-readonly`), the\r\n  schema **drops the DML kind** when no Type permits it, and each DML's\r\n  target-name enum is filtered (`into` → insertable, `update.type` → updatable,\r\n  `delete.from` → deletable).\r\n- **Field permissions.** `insertable` / `updatable` on a `FieldDef` (default\r\n  **true**). A **computed** field (`FieldBacking.compute`) defaults to\r\n  `insertable:false, updatable:false` (override with an explicit flag).\r\n  Validation rejects a listed non-insertable field (`insert.field-readonly`) / an\r\n  assigned non-updatable field (`update.field-readonly`); the paired schema\r\n  offers only insertable `fields` / updatable `set` fields.\r\n- **Insert-requiredness (one rule).** A field is REQUIRED on insert iff it is\r\n  **insertable AND non-nullable AND has no default AND is not computed** —\r\n  otherwise optional or excluded. A shared `requiredOnInsert` helper drives both\r\n  the schema (required-vs-optional in paired mode) and validation\r\n  (`insert.missing-required`, listing the missing names).\r\n- **Defaults live on the backing.** `FieldBacking.default` is a `Value` or a\r\n  factory `() => Value | Promise<Value>` — its presence alone makes the field\r\n  optional-on-insert (no `hasDefault` flag). At **runtime** an omitted defaulted\r\n  field is materialized (value evaluated / factory awaited, per row) into the\r\n  record; in **SQL** the column is left out of the INSERT so the DB's own column\r\n  `DEFAULT` fills it (a JS-factory default is runtime-only).\r\n- **Per-field expr restrictions.** `FieldDef.exprs` = `{ not: ExprKind[] }` or\r\n  `{ only: ExprKind[] }` NARROWS which expr kinds may target the field (never\r\n  enables one the field TYPE disallows). `field.allowsExpr(kind)` respects both.\r\n  Validation reports `field.expr-denied` at the use site — a standalone\r\n  `field-ref`, a gating operator's DIRECT field-ref operand (`comparison` /\r\n  `between` / `in` / `is-null` / `array-op`), and the field-naming exprs\r\n  (`text-search` / `text-score` / `semantic` / `filters`). The paired schema\r\n  omits an excluded field from the relevant enum, and gates a kind away entirely\r\n  when every candidate field excludes it.\r\n\r\n```ts\r\nconst doc: TypeDef = {\r\n  name: 'doc', count: 1000, bytes: 256,\r\n  fields: [\r\n    { name: 'id',    type: { kind: 'text' } },                      // required on insert\r\n    { name: 'title', type: { kind: 'text' } },                      // required\r\n    { name: 'views', type: { kind: 'number' }, updatable: false },  // write-once\r\n    { name: 'notes', type: { kind: 'text' }, nullable: true },      // optional\r\n  ],\r\n};\r\n// `createdAt` is optional-on-insert (has a default) and materialized at runtime.\r\nconst backing: TypeBacking = {\r\n  fields: { createdAt: { default: () => Value.of(new Date().toISOString()) } },\r\n};\r\n```\r\n\r\n## Execution model\r\n\r\nThere is ONE execution contract: **run a query, optionally with param values\r\nand filters, and get back `{ rows, fields, total }`**.\r\n\r\n```ts\r\nconst result = await engine.run(query, { params, filters, includeTotal });\r\n//   result.rows   — the output rows (objects; pass { rows: 'array' } for arrays)\r\n//   result.fields — resolved output fields (name + type + summary metadata)\r\n//   result.total  — pre-limit row count, when run with `includeTotal: true`\r\n```\r\n\r\nEach field's `type` is the full `ResolvedType`. A **computed** one carries\r\n`aggregate` (does a group collapse happen anywhere in this expression?) and\r\n`aggregateFn` — the APPLIED aggregate's name, present exactly when the value IS\r\none aggregate call. `sum(hours) as total_hours` reports `aggregateFn: 'sum'`\r\nunder any alias; `max(a) - min(b)` reports `aggregate: true` with NO\r\n`aggregateFn` (it contains aggregates but is none); a window over an\r\naggregate-shaped function reports neither. Read it rather than inferring the\r\nfunction from the output column NAME, which cannot see through an alias and\r\nmistakes `hours * 2 as count` for an aggregate.\r\n\r\nEverything composes around that one call:\r\n\r\n- **Params.** A `param` (`{ kind: 'param', name }`) infers its type from how it\r\n  is used and is bound at run time via `options.params`. Introspect what a built\r\n  query expects with `query.params(engine)` → `ParamDef[]` (name + inferred\r\n  type).\r\n\r\n  It reports every param the statement binds **at any depth**, so what it\r\n  declares is always enough to bind the emitted SQL. That includes a\r\n  `limit` / `offset` bound, which lives outside the walked expr tree: on the\r\n  statement itself, on a **`cte`'s `final`** (where `autoPaginate` puts it), on a\r\n  set-operation **arm** or its set-level bound, and inside a FROM / `in` /\r\n  `exists` subquery or an `insert … select` source. An under-reported bound is\r\n  not a cosmetic gap — the SQL still emits `LIMIT ?` and a caller that binds the\r\n  declared signature leaves it NULL, which Postgres reads as *no limit*.\r\n\r\n  A param's type is the **meet** of every use — the most specific type\r\n  compatible with all of them, folded through `FieldType.meet`. Compared against\r\n  an `enum` in one place and plain `text` in another it is the **enum**;\r\n  `text{minLength:5}` beside `text{maxLength:10}` carries **both** bounds;\r\n  `number` beside `money` is `money`. The meet is commutative, associative,\r\n  idempotent and **sound** (it accepts nothing that both operands do not), so the\r\n  answer never depends on where in the JSON each use sits. Where the uses have no\r\n  meet the param is in **conflict** (`param.conflict`) and `params()` omits it.\r\n\r\n  It is the *greatest* lower bound for every type built from a **def** —\r\n  `x ⊓ ⊤ = x` holds for anything `parseType` / `parseFieldType` can produce. A\r\n  closed set IS the value schema, so a meet narrows a merged set by the merged\r\n  scalar constraints, and for a self-inconsistent type\r\n  (`text{values:['ab'], minLength:5}`, whose own bound rejects its own member)\r\n  even `x ⊓ text` narrows or conflicts. Since `0.6.6` that declaration is\r\n  **refused at parse time** (`field-type.bad-values`) rather than tolerated, so\r\n  the exception is gone from the def road. The public **constructors** do not\r\n  validate — `new TextFieldType({ values: [{ value: 'ab' }], minLength: 5 })` is\r\n  still buildable, and for one of those the meet is a lower bound only, exactly\r\n  as an uncompilable `pattern` is still constructible by hand. Soundness is kept\r\n  unconditionally on both roads, because it is the law a validator actually\r\n  depends on.\r\n\r\n  `engine.parameters(query)` → `ParamInfo[]` is the detailed view: per param,\r\n  every `use` (`{ at, type, category, field? }` — path, required type, category\r\n  summary, and the column the requirement came from), the merged `type`, and the\r\n  `conflict` when there is none. `engine.checkParams(query, params)` →\r\n  `Problems` checks SUPPLIED values against that merged type before execution\r\n  (`param.value` / `param.missing` / `param.unknown`); `null` always passes, and\r\n  the same check rides on `validateQuery(query, _, _, { params })`.\r\n- **Filters.** `options.filters` is a `Record<source, ExprDef | Expr | null>` —\r\n  a single **boolean Expr** per source (or `null` / absent for none). The\r\n  `filters` EXPR in the query is only a placeholder (`{ source, fields? }`); the\r\n  predicate is supplied here, keyed by source, and the placeholder evaluates /\r\n  emits it (vacuously `TRUE` when none is supplied). Introspect which sources a\r\n  built query exposes — and the fields each offers — with\r\n  `query.filters(engine)` → `Record<source, { fields: QueryField[] }>` (each\r\n  field is name + resolved type + nullability + field-type kind, restricted to\r\n  the placeholder's `fields` allowlist when it sets one). Build the per-source\r\n  bool Expr however you like — e.g. a `comparison` / `logical` `ExprDef`, or one\r\n  produced by your own filter-builder UI. `query.filterSources()` still lists the\r\n  sources a filter may target; an unknown source or field is a `QueryTypeError`.\r\n- **Total count.** `includeTotal: true` is an **execution-time** option (NOT a\r\n  `SelectDef` field): `run` captures the pre-limit count into `result.total`,\r\n  and `toSQL(query, dialect, { includeTotal: true })` emits\r\n  `COUNT(*) OVER () AS \"$total\"`.\r\n\r\n  It applies to the **ENTRY query only** — never a CTE body, a set-operation\r\n  arm, or a FROM subquery. `$total` is a PROJECTED column, so an arm that\r\n  carried it would take part in the set comparison and change the **rows**:\r\n  `UNION` would stop de-duplicating, and `INTERSECT` / `EXCEPT` would compare\r\n  per-arm counts they were never meant to see. A query whose entry is a **set\r\n  operation therefore reports no total at all** (`result.total` is `undefined`;\r\n  the SQL carries no `$total`) rather than a wrong one — both engines agree.\r\n  To page a set operation *and* count it, wrap it in a SELECT and count there:\r\n\r\n  ```ts\r\n  const counted = {\r\n    kind: 'select',\r\n    fields: [{ expr: { kind: 'field-ref', source: 's', field: 'id' } }],\r\n    from: { kind: 'subquery', as: 's', query: theUnion },\r\n    limit: 20,\r\n  } satisfies SelectDef;\r\n  await engine.run(counted, { includeTotal: true }); // → { rows, total }\r\n  ```\r\n- **Pagination.** `autoPaginate` adds `limit` / `offset` as bind PARAMS, so\r\n  pagination is just supplying their values:\r\n\r\n  ```ts\r\n  import { autoPaginate } from '@aeye/query';\r\n  const paged = autoPaginate(select); // adds { limit: param('limit'), offset: param('offset') }\r\n  paged.params(engine);               // → [{ name:'limit', type:{kind:'number'} }, { name:'offset', … }]\r\n  await engine.run(paged, { params: { limit: 10, offset: 0 } });\r\n  ```\r\n\r\n  It pages exactly the kinds that HAVE a row bound: a **`select`** (its own\r\n  LIMIT / OFFSET), a **set operation** (`union` / `intersect` / `except` — the\r\n  SET-LEVEL bound over the combined rows, never an arm's, since paging an arm\r\n  would change which rows the set operation compares), and a **`cte`**, which is\r\n  paged through its `final` query (a CTE body is an intermediate result). Every\r\n  other kind — `insert` / `update` / `delete` / `expr` — has no bound to bind and\r\n  **throws** `QueryTypeError` with code `paginate.unsupported-kind`. Ask first\r\n  with `canAutoPaginate(query)` when you hold an arbitrary `QueryDef`.\r\n\r\n- **Drill-down.** `drillDownInto` rebuilds the underlying-rows query and extracts\r\n  the drill PARAMS from a chosen aggregated row — then it is the same `run` call\r\n  with those params (see [Drill-down](#drill-down)).\r\n\r\n`toSQL` accepts the same `params` and `filters` options and emits them\r\nidentically, so emitted SQL matches what would run.\r\n\r\n## SQL conversion\r\n\r\nThe same query emits SQL for any registered dialect. The base dialect uses `?`\r\nplaceholders; Postgres uses `$1`, `$2`, … . Relation joins synthesize their ON\r\nclause from the relation key — you never write it.\r\n\r\n```ts\r\nconst base = engine.toSQL(select, 'base');         // { sql, params }\r\nconst pg   = engine.toSQL(select, 'postgres', { params: { minTotal: 50 } });\r\n```\r\n\r\n## Type backing & Access / Computed\r\n\r\nThe conceptual model the LLM sees — a `TypeDef`'s flat list of fields — can be\r\n**arbitrarily richer behind the scenes**. A `TypeBacking` is plain dev-side\r\nTypeScript you register *alongside* the Type (`registry.registerType(type,\r\nbacking)`, or `new QueryEngine(registry, { backings })`); the JSON `TypeDef` /\r\n`FieldDef` are **never touched**, so the schema stays minimal. A backing can\r\nremap the real source table, compute fields, auto-join other Types, gate\r\nrows / fields, and point full-text / semantic search at hidden physical fields —\r\nall resolved IDENTICALLY in `engine.run` and `engine.toSQL`.\r\n\r\nTwo primitives compose everything. Each offers a dual `expr` path plus per-mode\r\noverrides — **SQL** resolves `sql` then `expr`; the **runtime** resolves `run`\r\nthen `expr`. Every factory (`expr` / `sql` / `run`) is handed the **`alias` the\r\nType is bound under for this occurrence** and MUST use it for every reference\r\n(never hardcode the Type name) — so when a Type is aliased (multiple joins to the\r\nsame Type, a self-join, an `{kind:'aliased'}` FROM) the references resolve to the\r\ncorrect source:\r\n\r\n- **`Access`** — a security *predicate*. It resolves to a predicate `Expr`\r\n  (apply it), `true` (visible), `false` (denied), or `undefined` (no-op).\r\n- **`Computed`** — a field *value* producer (replaces the stored column). It\r\n  always yields a value.\r\n\r\n```ts\r\nconst backing: TypeBacking = {\r\n  name: 'projects',                       // real table ⇒ FROM \"projects\" AS \"project\"\r\n  access: { /* RLS — see below */ },\r\n  joins: { /* named hidden joins — see below */ },\r\n  fields: { /* per-field compute / access / remap */ },\r\n};\r\nregistry.registerType(project, backing);  // the TypeDef the LLM sees is unchanged\r\n```\r\n\r\n`examples/11-computed-fields.ts` is the end-to-end demo: one simple `project`\r\nType backed by `projects` + `users` + `tasks`, run in-memory AND emitted to SQL.\r\n\r\n## Computed fields\r\n\r\nA `FieldBacking.compute` supplies a field's value. The primary path is a dual\r\n`expr` (one `Expr` both emitted to SQL and evaluated in memory); `sql` / `run`\r\noverride per mode. A bare `name` just remaps the stored column.\r\n\r\n```ts\r\nfields: {\r\n  // dual expr: the auto-joined owner's name (one definition, both modes). Read\r\n  // the named join off the BOUND `alias` with `e.ref`, never a literal type name.\r\n  ownerName: { joins: ['owner'], compute: { expr: (alias) => e.ref(joinAlias(alias, 'owner'), 'name') } },\r\n\r\n  // per-mode override: format money in SQL one way, in memory another. Both use\r\n  // the bound `alias` (`row[alias]`), never a hardcoded key.\r\n  budgetLabel: { compute: {\r\n    sql: (alias, ctx) => SqlText.concat([SqlText.raw(\"'$' || \"), ctx.dialect.field(alias, 'budget')]),\r\n    run: (alias, row) => Value.of(`$${row[alias]?.['budget'] ?? 0}`),\r\n  } },\r\n\r\n  legacyNote: { name: 'note' },           // remap: read the stored `note` column\r\n}\r\n```\r\n\r\nCompute / access exprs that reach into other sources flow through the join\r\nplanner, so two fields reading the **same** auto-join collapse to ONE join.\r\n\r\n## RLS & FLS\r\n\r\nSame `Access` primitive, two scopes — both apply in `run` AND `toSQL`:\r\n\r\n- **RLS** (`TypeBacking.access`) — a row filter for every occurrence of the\r\n  Type. A predicate is ANDed into the SQL `WHERE` and filters executor rows on\r\n  load; `false` ⇒ no rows (`WHERE FALSE`); `true` / `undefined` ⇒ no filter.\r\n  Combines (AND) with any `RlsProvider` passed to `run` / `toSQL`.\r\n- **FLS** (`FieldBacking.access`) — a per-field gate. A predicate emits\r\n  `CASE WHEN <pred> THEN <value> ELSE NULL END` (nulled in memory when false);\r\n  `false` ⇒ a constant `NULL`; `true` / `undefined` ⇒ the plain value.\r\n\r\n```ts\r\n// RLS: only the current org's rows (orgId is NOT a conceptual field — pure backing).\r\naccess: { expr: (alias) => e.eq(e.ref(alias, 'orgId'), e.value(currentOrg)) },\r\n\r\n// FLS: `secretField` is visible only for active projects, reading stored `secret`.\r\nsecretField: { name: 'secret', access: { expr: (alias) => e.eq(e.ref(alias, 'status'), e.value('active')) } },\r\n```\r\n\r\n## Default conditions (soft scope)\r\n\r\n`TypeBacking.defaultConditions` is a **soft, suppressible** default scope —\r\narchived / soft-delete filtering the query can **reveal past**, unlike RLS. Each\r\n`DefaultCondition` is `{ where, without?, ops?, description? }`:\r\n\r\n- **`where`** — an `Access` predicate (dual `{ expr }` / `sql` / `run`, resolved\r\n  exactly like RLS: `false` ⇒ no rows, `true` / `undefined` ⇒ no filter, else\r\n  ANDed) applied while the condition is **active**, per bound occurrence.\r\n- **`without`** — referencing any of these fields **on that source** in a\r\n  **condition position** (the query's `where` / `having`, or a JOIN's `and`)\r\n  **lifts** the scope for that source. A reference in a select item / ORDER BY /\r\n  GROUP BY does **not** lift it, and each bound alias (incl. a self-join) is\r\n  decided independently. Omitted ⇒ derived from the fields `where.expr` reads (a\r\n  `sql`/`run`-only `where` with no `without` is then **always-on** — set it\r\n  explicitly to make it liftable).\r\n- **`ops`** — which row-filtering ops it scopes (default\r\n  `['select', 'update', 'delete']`; **INSERT is never scoped**).\r\n- **`description`** — an optional terse LLM-facing note (else auto-summarized in\r\n  `describeType`).\r\n\r\nRLS still always applies and is **never** suppressed; a default condition ANDs in\r\nalongside it.\r\n\r\n```ts\r\n// Archived files: every query is scoped to `archivedAt IS NULL`…\r\ndefaultConditions: [{ where: { expr: (alias) => e.isNull(e.ref(alias, 'archivedAt')) } }],\r\n\r\n// …until a query FILTERS on `archivedAt` (e.g. WHERE archivedAt IS NOT NULL),\r\n// which lifts the scope for that source and reveals the archived rows.\r\n```\r\n\r\n## Default ordering\r\n\r\n`TypeBacking.defaultOrder` declares a Type's **natural sort** — the `ORDER BY` a\r\nSELECT gets when it specifies **none** (and ordering is meaningful). A\r\n`DefaultOrder` is `{ by: DefaultOrderTerm[]; applyTo? }`; each `DefaultOrderTerm`\r\nis `{ by: Computed; dir?; nulls? }` whose `by` is the sort **key** — the same\r\ndual `{ expr }` / `sql` / `run` `Computed` computed fields use, so one key\r\n**emits to SQL and sorts in memory identically** (`dir` default `'asc'`; `nulls`\r\nelse direction-based — asc ⇒ nulls first, desc ⇒ last — matching an explicit\r\nORDER BY).\r\n\r\nIt applies only when the **FROM** binds the backed Type (joins never contribute\r\ntheir default order), the query has **no explicit `order`**, and it is **not\r\naggregated** (no `groupBy`, no bare aggregate) and **not `DISTINCT`** — both are\r\nskipped (a base-field order is meaningless post-aggregation; a non-selected\r\nDISTINCT key is illegal SQL).\r\n\r\n`applyTo` scopes **which** selects receive it:\r\n\r\n- **`'result'`** (default) — the **root** query being run/emitted, **or** any\r\n  `LIMIT`/`OFFSET` select.\r\n- **`'paginated'`** — only a `LIMIT`/`OFFSET` select.\r\n- **`'all'`** — every eligible select over the Type (incl. subqueries / CTEs).\r\n\r\nThe root is tracked by an `isRoot` marker threaded from `engine.run` /\r\n`engine.toSQL` onto the runtime / SQL context; nested queries (a subquery /\r\nEXISTS / IN subquery, a FROM subquery, a CTE body, a set-op branch) run and emit\r\n**non-root**. SELECT-only — DML is never reordered.\r\n\r\n```ts\r\n// Newest-first by default: an unsorted SELECT over the Type gets\r\n// `ORDER BY \"t\".\"createdAt\" DESC`.\r\ndefaultOrder: { by: [{ by: { expr: (alias) => e.ref(alias, 'createdAt') }, dir: 'desc' }] },\r\n```\r\n\r\n## Named joins & LATERAL\r\n\r\n`TypeBacking.joins` declares **named, hidden** joins; a field opts in via\r\n`FieldBacking.joins: [name]`. Each join is added to a query **once, only if a\r\nreferencing field is emitted**, and deduped by name — so N fields sharing one\r\njoin collapse to a single planned join (its alias is `joinAlias(source, name)`).\r\nA `JoinSpec` is either a `relation` (reuses the shared relation-join machinery)\r\nor a `lateral` (a correlated sub-select):\r\n\r\n```ts\r\njoins: {\r\n  // a belongs-to relation auto-join (shared by every field reading the owner).\r\n  owner: { expr: (alias) => ({ kind: 'relation', source: alias, relation: 'owner' }) },\r\n\r\n  // a LATERAL aggregate over a has-many — `taskCount` + `totalHours` share it.\r\n  // The lateral correlates via `outer` (the planner passes this backed Type's\r\n  // bound alias to `query`); the inner FROM (`task`) is its own scope.\r\n  taskStats: { expr: (alias) => ({ kind: 'lateral', joinType: 'left',\r\n    query: (outer) => ({ kind: 'select',\r\n      fields: [\r\n        { expr: e.countStar().toJSON(), as: 'cnt' },\r\n        { expr: e.sum(e.ref('task', 'hours')).toJSON(), as: 'hrs' },\r\n      ],\r\n      from: { kind: 'type', type: 'task' },\r\n      where: [e.eq(e.ref('task', 'projectId'), e.ref(outer, 'id')).toJSON()] }) }) },\r\n}\r\n```\r\n\r\nA `lateral`'s `pick` names the column a no-`compute` field defaults to. Postgres\r\nemits `LEFT JOIN LATERAL (…) ON true`; the portable base dialect degrades to\r\n`LEFT JOIN LATERAL (…) ON 1 = 1` (a documented correlated-subquery fallback) and\r\nthe runtime evaluates the sub-select per outer row.\r\n\r\n## Relation-join backing (physical FK columns / custom ON)\r\n\r\nBy default a relation join's `ON` is synthesized from the relation field's\r\n**name** convention (`source.<local> = target.<foreign>`). `FieldBacking.relation`\r\n(on a relation-typed field only) overrides that with **explicit, LLM-hidden\r\nphysical foreign-key columns** — the conceptual `FieldDef` / `TypeDef` the model\r\nsees never change.\r\n\r\n```ts\r\nconst backing: TypeBacking = {\r\n  fields: {\r\n    // `comment_rating.user` belongs-to `user`; the hidden physical FK is `user_id`.\r\n    user: { relation: { keys: [{ local: 'user_id', foreign: 'id' }] } },\r\n  },\r\n};\r\n// A user's ratings (the materialized inverse has-many) then emits\r\n//   … LEFT JOIN \"comment_rating\" ON \"user\".\"id\" = \"comment_rating\".\"user_id\"\r\n// and the belongs-to direction emits the mirror `ON \"comment_rating\".\"user_id\" = \"user\".\"id\"`.\r\n```\r\n\r\n- **`keys: [{ local, foreign? }]`** — physical key-column pairs, **all ANDed**\r\n  (composite FKs: `keys: [{ local: 'a_id', foreign: 'a' }, { local: 'b_id',\r\n  foreign: 'b' }]` ⇒ `ON src.a_id = tgt.a AND src.b_id = tgt.b`). `local` is the\r\n  column on the side that **declares** the relation; `foreign` is the column on\r\n  the **target** and defaults to the target's identity field.\r\n- **The backing lives on the owning belongs-to relation.** A materialized inverse\r\n  has-many **reuses the same FK** (its forward relation's backing, orientation\r\n  swapped) — you declare it once.\r\n- **`on`** — a fully custom, alias-correct `ON` (overrides `keys`). `{ expr }` is\r\n  the dual path (one predicate emitted to SQL **and** evaluated in memory); `sql`\r\n  / `run` are per-mode overrides. Each factory receives the two **bound aliases**\r\n  (`localAlias` = the declaring side, `joinedAlias` = the target), so\r\n  aliased / self-joins resolve.\r\n\r\nEvery ON site honors the backing — authored relation joins (value + at runtime),\r\nthe `TypeBacking.joins` relation spec, and joined UPDATE/DELETE — so SQL and the\r\nin-memory runtime always agree. `JoinDef.and`\r\nis still ANDed onto whatever `ON` the backing produces. With no backing the\r\nconvention is used unchanged (fully backward-compatible).\r\n\r\n## Filters and params\r\n\r\nA `filters` expression is an **LLM-opaque placeholder** bound to a source, with\r\nan optional `fields` allowlist — `{ kind: 'filters', source, fields? }`. The LLM\r\nnever authors the predicate. At **execution time** the developer supplies a\r\nsingle **boolean `Expr` / `ExprDef`** per source (keyed by source); the\r\nplaceholder evaluates / emits it, vacuously `TRUE` when none is supplied.\r\n\r\nIntrospect what a built query exposes with `query.filters(engine)`: it returns\r\n`Record<source, { fields: QueryField[] }>` — for every `filters` placeholder,\r\nits bound source mapped to the fields available on it (each with name, resolved\r\ntype, nullability, and field-type kind), restricted to the placeholder's\r\n`fields` allowlist when set. A UI renders controls from that, then supplies the\r\nresulting bool `Expr` at run time.\r\n\r\n```ts\r\n// In the query: just a placeholder + an allowlist (no predicate).\r\nwhere: [{ kind: 'filters', source: 'product', fields: ['category', 'price'] }]\r\n\r\n// Introspect the exposed source → fields.\r\nconst exposed = engine.registry.parseQuery(select).filters(engine);\r\n// → { product: { fields: [{ name:'category', … }, { name:'price', … }] } }\r\n\r\n// At run time: supply ONE bool ExprDef per source (built however you like — here\r\n// with the `e.*` builder, whose `.toJSON()` is the wire `ExprDef`).\r\nconst productFilter: ExprDef = e.and(\r\n  e.eq(e.ref('product', 'category'), e.value('hardware')),\r\n  e.gte(e.ref('product', 'price'), e.value(30)),\r\n).toJSON();\r\nawait engine.run(select, { filters: { product: productFilter } });\r\n```\r\n\r\n## Semantic & text search\r\n\r\nBoth are bound to a **source** with an OPTIONAL `field` (omit to target the\r\nwhole source):\r\n\r\n- **`semantic`** — `{ kind: 'semantic', source, field?, query }` scores a row's\r\n  embedding against `query`, which is a literal string, a `param`, a\r\n  `{ source, field }` ref to ANOTHER **bound** source + semantic field (the\r\n  cross-source **pairing** form), or a `{ type, field }` ref that resolves to the\r\n  single bound source of that Type. The source/field must be semantic-eligible (a\r\n  Type flagged `semantic`, or a `semantic`/`search` text field). Requires an\r\n  embedder.\r\n- **`text-search`** — `{ kind: 'text-search', source, field?, query }` is a\r\n  full-text **predicate** (a boolean); `query` is a literal string or a `param`.\r\n  Whole-source search needs a searchable Type; a narrowed `field` must be a text\r\n  field.\r\n- **`text-score`** — `{ kind: 'text-score', source, field?, query }` is the\r\n  numeric **relevance** counterpart of `text-search` (same eligibility). It\r\n  resolves to a **number** — usable in SELECT + ORDER BY — so \"top N by text\r\n  relevance\" works. Postgres emits `ts_rank`; the base (ANSI) dialect degrades to\r\n  a numeric `0/1` match. Build it with `e.textScore(source, query, field?)`.\r\n\r\n```ts\r\n{ kind: 'semantic', source: 'doc', field: 'body', query: 'quarterly revenue' }\r\n{ kind: 'semantic', source: 'doc', query: { source: 'topic', field: 'label' } } // pairing\r\n{ kind: 'text-search', source: 'user', field: 'email', query: 'ada' }\r\n{ kind: 'text-score',  source: 'doc',  field: 'body',  query: 'revenue' }\r\n```\r\n\r\nIn the LLM schema these participate in **depth** like field-refs: at `paired`\r\nthe `source` is a Type and the `field` enum is restricted to that Type's\r\nsemantic / text fields.\r\n\r\n### Scoring & ranking — pairing + text relevance\r\n\r\nBoth `semantic` (pairing) and `text-score` produce a **number** you can put in\r\nSELECT and `ORDER BY … DESC LIMIT N`, so a query returns the top-N by relevance.\r\n\r\n- **Cross-Type semantic pairing.** Join (or cross-join) two Types so BOTH are\r\n  bound, then score one against the other's embedding. `toSQL` emits the\r\n  dialect's `similarity` over BOTH bound aliases' vectors (each side's hidden\r\n  `SemanticBacking.vectorField` if backed, else the default `<alias>.\"embedding\"`\r\n  fragment), so a self-pairing of two aliases of ONE Type works too:\r\n\r\n  ```sql\r\n  -- FROM paper JOIN topic … , fields: [ id, semantic(paper, {source:'topic',field:'label'}) as score ]\r\n  SELECT \"paper\".\"id\", (1 - (\"paper\".\"emb\" <=> \"topic\".\"embedding\")) AS \"score\"\r\n  FROM \"paper\" … INNER JOIN \"topic\" … ORDER BY \"score\" DESC LIMIT 10\r\n  ```\r\n\r\n  Validation requires BOTH sides be bound and semantic-eligible: an unbound\r\n  reference is `semantic.query-unbound`; a `{ type }` bound more than once is\r\n  `semantic.query-ambiguous` (use the `{ source }` form to disambiguate). The\r\n  base dialect degrades similarity to `0` (never throws).\r\n\r\n- **Numeric text score.** `text-score` ranks by full-text relevance:\r\n  `ts_rank(to_tsvector(col), plainto_tsquery(query))` in Postgres (honoring a\r\n  `SearchBacking`'s hidden `vectorField` / `language` / boolean `sql` override,\r\n  the last lifted to a numeric `0/1`); the base dialect degrades to\r\n  `CASE WHEN <LIKE> THEN 1 ELSE 0 END`. In memory it is a deterministic\r\n  token-overlap fraction (honoring `SearchBacking.run`). See\r\n  `examples/13-scoring-ranking.ts`.\r\n\r\n### Search & semantic backing\r\n\r\nA Type / field flagged `search` / `semantic` in the (unchanged, minimal) schema\r\nvery often has a **physical field hidden from the type system** that already\r\nholds a precomputed `tsvector` (full-text) or `pgvector` embedding. A backing\r\nsays **how** search / similarity runs per Type or field — most importantly by\r\npointing at that hidden field. Both `TypeBacking` (whole-type) and `FieldBacking`\r\n(per-field) take an optional `search?: SearchBacking` and `semantic?:\r\nSemanticBacking`; a **field-level** backing overrides the **type-level** one.\r\n\r\n```ts\r\nconst backing: TypeBacking = {\r\n  // WHOLE-TYPE: point at hidden precomputed fields (NOT conceptual fields).\r\n  search:   { vectorField: 'search_tsv', language: 'english' }, // a tsvector field\r\n  semantic: { vectorField: 'embedding' },                       // a pgvector field\r\n  fields: {\r\n    // FIELD-LEVEL override wins for a field-narrowed `text-search` / `semantic`.\r\n    title: { search: { vectorField: 'title_tsv' }, semantic: { vectorField: 'title_vec' } },\r\n  },\r\n};\r\n```\r\n\r\n**Knobs** (each factory takes the bound `alias` **first** and must reference it,\r\nso aliased / self-joined sources resolve correctly):\r\n\r\n- `vectorField` — the hidden physical field, referenced as `<alias>.\"<field>\"`.\r\n  In Postgres a `SearchBacking.vectorField` emits the precomputed-tsvector\r\n  predicate `<alias>.\"f\" @@ plainto_tsquery('<language>', $n)` (**not** re-wrapped\r\n  in `to_tsvector`); a `SemanticBacking.vectorField` is the left operand of the\r\n  dialect's `similarity`, with the query vector bound as a `$n::vector` param.\r\n- `language` — the text-search config for `plainto_tsquery` (default `'english'`).\r\n- `sql` — a full SQL override → a **boolean** predicate (search) / **numeric**\r\n  score (semantic). Given `(alias, query|queryVector, ctx)`.\r\n- `run` — a full runtime override → `boolean` (search) / `number` (semantic).\r\n- `SemanticBacking.vector` — where the row's embedding comes from at runtime\r\n  (an alternative to `vectorField`), returning `number[]` or `null`.\r\n- `SemanticBacking.embedder` — a per-Type / per-field embedder for the **query**\r\n  text (else the run / engine embedder).\r\n\r\n**Precedence** (both modes): a full `sql` / `run` override wins; else the hidden\r\n`vectorField` (or `vector` producer) is used; else today's default (the dialect's\r\n`textSearch` / `similarity` over the conceptual text fields, or an in-memory\r\ntoken match / embed-the-row-text). `toSQL` stays synchronous — the async embedder\r\nis **never** called there; the query vector is a bound param.\r\n\r\nThe **base (ANSI) dialect degrades gracefully** and never throws: a tsvector\r\nfield falls back to a case-insensitive `LIKE`, and vector similarity to a\r\nconstant `0`. See `examples/12-search-backing.ts`.\r\n\r\n### Array fields and operations\r\n\r\nAn `array` field is queried with the `array-op` predicate expression: `contains`\r\n(a single element is present), `containsAny` / `containsAll` (overlap / superset\r\nagainst an element list), and `isEmpty` / `notEmpty`. Element count is a\r\n`comparison` over the builtin `arrayLength(field)` scalar function. (Element matching follows the ELEMENT type's `casing`, else the engine's\r\n`textCasing` default — the same rule a scalar text comparison uses.)\r\n\r\n```ts\r\n// containment — a single element is present:\r\n{ kind: 'array-op', op: 'contains', target: { kind: 'field-ref', source: 'user', field: 'tags' },\r\n  value: { kind: 'literal', value: 'beta' } }\r\n\r\n// overlap against an element list:\r\n{ kind: 'array-op', op: 'containsAny', target: { kind: 'field-ref', source: 'user', field: 'tags' },\r\n  value: [{ kind: 'literal', value: 'admin' }, { kind: 'literal', value: 'beta' }] }\r\n\r\n// element count ≥ 2 via the builtin arrayLength function:\r\n{ kind: 'comparison', op: '>=',\r\n  left: { kind: 'function-call', function: 'arrayLength', args: { arr: { kind: 'field-ref', source: 'user', field: 'tags' } } },\r\n  right: { kind: 'literal', value: 2 } }\r\n```\r\n\r\n> Build these with the `e.*` array builders (`e.contains` / `e.containsAny` /\r\n> `e.containsAll` / `e.isEmpty` / `e.notEmpty`, and `e.fn('arrayLength', …)`).\r\n\r\n**Dialect support.** Array operations are **Postgres-native**: `contains` →\r\n`value = ANY(col)`, `containsAll` → `col @> ARRAY[…]`, `containsAny` →\r\n`col && ARRAY[…]`, and length → `cardinality(col)`. The portable **base (ANSI)\r\ndialect has no array operators**, so containment (`contains` / `containsAny` /\r\n`containsAll`) throws a clear `QueryTypeError` (`array-op.unsupported-dialect`)\r\nrather than emit wrong SQL; emptiness and the length filters still work there\r\nvia `COALESCE(json_array_length(col), 0)`.\r\n\r\nA `param` (`{ kind: 'param', name }`) infers its type from how it is used and\r\nis bound at run/emit time. A filter predicate is likewise supplied at EXECUTION\r\ntime via `engine.run(query, { filters: { <source>: boolExpr } })` — the\r\nplaceholder evaluates it dynamically against the bound source (see\r\n[Execution model](#execution-model)). `autoPaginate` turns a query into a\r\nreusable, paged artifact by binding `limit` / `offset` to params (idempotently):\r\n\r\n```ts\r\nimport { autoPaginate } from '@aeye/query';\r\nconst paged = autoPaginate(select);   // adds { limit: param('limit'), offset: param('offset') }\r\nawait engine.run(paged, { params: { limit: 10, offset: 0 } });\r\n```\r\n\r\n## CTEs\r\n\r\nA `WITH` statement (`{ kind: 'cte', ctes, final }`) carries a list of named CTE\r\nentries consumed by a `final` query. An entry is one of two **distinct** shapes,\r\nstructurally discriminated:\r\n\r\n- **Non-recursive** — `{ name, query }`.\r\n- **Recursive** — `{ name, base, recursive }`: a `base` seed query UNION-ed with\r\n  a `recursive` arm that reads the CTE's own accumulating rows until a fixpoint\r\n  (iteration-capped). Recursion is its OWN shape — there is no `recursive?` flag\r\n  on the plain entry.\r\n\r\n```ts\r\n{ kind: 'cte',\r\n  ctes: [{ name: 'descendants',\r\n           base:      /* seed select */,\r\n           recursive: /* select that reads `descendants` */ }],\r\n  final: { kind: 'select', from: { kind: 'type', type: 'descendants' }, fields: [/* … */] } }\r\n```\r\n\r\n## Drill-down\r\n\r\n`drillDown` rebuilds the query that returns the **underlying rows** behind an\r\naggregate — PARAMETERIZED: each GROUP BY key is pinned to a bind param\r\n(`key = param(name)`), so the drilled query is reusable. It returns the rebuilt\r\n`query`, the `params` (a `DrillParam[]` mapping each output `field` → its\r\n`name`), and any `warnings`.\r\n\r\n```ts\r\nimport { drillDown, drillDownInto } from '@aeye/query';\r\n\r\n// The reusable, parameterized drilled query + its field → param mapping.\r\nconst d = drillDown(revenuePerUser, engine);\r\n//   d.params → [{ name:'userId', field:'userId', key: { kind:'field-ref', source:'order', field:'userId' } }]\r\n\r\n// Or drill into ONE aggregated row: extract its key values, then it's the same\r\n// run call. (This is the old literal-baking behavior, now param-driven.)\r\nconst into = drillDownInto(revenuePerUser, groupRow, engine);\r\nif ('query' in into) {\r\n  const underlying = await engine.run(into.query, { params: into.params }); // that row's orders\r\n}\r\n```\r\n\r\nThe param NAME is derived from the carrying output field (sanitized to a valid\r\nidentifier; suffixed `_2`, `_3`, … on collision with a param the query already\r\nuses). Failure cases (`drill.no-aggregation` / `non-invertible` /\r\n`having-aggregate` / `window-unsupported`) return LLM-friendly `Problems`.\r\n\r\nEach aggregate is replaced by its underlying row-level expression, and `count(*)`\r\n— which has no single value — expands to the FROM type's fields MINUS the ones\r\nthe SELECT already projects itself. So `SELECT status, count(*) … GROUP BY status`\r\ndrills to `status` plus the remaining columns, with the group key projected ONCE.\r\nThe skip is keyed on the EXPRESSION (its canonical form), never on the output\r\nname: two different expressions may legitimately share a name.\r\n\r\n## Cost & estimation\r\n\r\nBottom-up estimates driven by each Type's cardinality (`count` rows, `bytes`\r\nper row), its indexes + fixed **selectivity** for predicates (an indexed\r\nequality narrows toward one row; a non-indexed one applies `EQ_SELECTIVITY`),\r\nand per-Type / per-field `changes` rates. Five estimators hang off the engine:\r\n\r\n```ts\r\nconst cost    = engine.cost(select);        // { rows, bytes } — WORK to produce the result (scanned rows)\r\nconst output  = engine.outputCost(select);  // { rows, bytes } — SIZE of the result (delivered rows × projection width)\r\nconst affected = engine.affected(update);   // { rows, types: [{ type, rows }] } — rows an INSERT/UPDATE/DELETE (or CTE) mutates\r\nconst refs    = engine.references(select);  // { types, fields, functions } — exactly what the query READS\r\nconst ttl     = engine.changeInterval(select); // ms until the data behind it could change (0 = always, -1 = never, else fastest rate)\r\n\r\n// Constraints reject over-budget queries during validation:\r\nconst problems = engine.validateQuery(select, undefined, { maxRows: 100, maxBytes: 1_000_000 });\r\n// → cost.rows-exceeded / cost.bytes-exceeded when the estimate blows past a cap\r\n```\r\n\r\n`cost` estimates work (OR-aware, index-probe driven); `outputCost` sizes the\r\ndelivered result (post-WHERE/GROUP/DISTINCT, capped by LIMIT/OFFSET);\r\n`affected` counts mutated rows per Type; `references` powers `changeInterval`,\r\nwhich folds the read Types' / fields' / functions' `changes` rates into a single\r\nfreshness / cache-TTL signal. All accept the same execution-time\r\n`options.params` / `filters` / `sort` so the estimate reflects what actually runs.\r\n\r\n## Functions\r\n\r\nAll four function shapes are uniform: declare a `FunctionDef` (name, shape,\r\n**named** params, output) with `registerFunction`, then pair it with a\r\nshape-tagged runtime via `registerFunctionRun`. Calls reference the function by\r\nname with **named arguments** (`args: { paramName: <expr> }`):\r\n\r\n```ts\r\n// scalar — initials(value: text): text\r\nregistry.registerFunction({\r\n  name: 'initials', shape: 'scalar',\r\n  params: [{ name: 'value', type: { kind: 'text' } }],\r\n  output: { kind: 'text' },\r\n});\r\nregistry.registerFunctionRun('initials', {\r\n  shape: 'scalar',\r\n  run: (args) => Value.of(args.value.toText().split(/\\s+/).map((w) => w[0]).join('')),\r\n});\r\n\r\n// reference it by name with NAMED args:\r\n// { kind: 'function-call', function: 'initials', args: { value: <expr> } }\r\n```\r\n\r\nThe four shapes differ only in what their `run` receives:\r\n\r\n| shape       | `run(...)` signature                          | example       |\r\n| ----------- | --------------------------------------------- | ------------- |\r\n| `scalar`    | `(args, ctx)` → one value                     | `upper`       |\r\n| `tabular`   | `(args, ctx)` → rows                          | `rangeRows`   |\r\n| `aggregate` | `(rows, ctx)` → one value over a group        | `sum`, `count`|\r\n| `window`    | `(partition, index, ctx)` → value per row     | `rowNumber`   |\r\n\r\n`count(*)` is the empty-args convention: `{ kind: 'aggregate', function:\r\n'count', args: {} }`. The registry ships a **default library** (60+ functions\r\nacross all shapes — `upper`/`concat`/`coalesce`/…, string/math scalars\r\n(`trimLeft`/`padLeft`/`splitPart`/`log`/`iif`/…), `sum`/`avg`/`min`/`max`/\r\n`count`, `rowNumber`/`rank`/`lag`/…) registered by `createRegistry()`, so they\r\nare discoverable and runnable out of the box (see the reference below):\r\n\r\n```ts\r\nregistry.functionList();            // every FunctionDef (default lib + your own)\r\ndescribeFunctions(engine);          // a promptable, by-shape listing for an LLM\r\n```\r\n\r\n### Operators — `&&`, `<->`, `@>`\r\n\r\nA function declaration cannot express an INFIX operator: `a && b` has no call\r\nform in any dialect, and `FunctionDef.sql` is a NAME, never a template.\r\n`registerOperator` is the other half — a declaration whose SQL is a per-dialect\r\ntemplate, applied through the `operator` expr kind:\r\n\r\n```ts\r\nregistry.registerOperator({\r\n  name: '&&',                                     // SQL operator punctuation only\r\n  operands: [{ name: 'left', type: { kind: 'json', as: 'Geometry' } },\r\n             { name: 'right', type: { kind: 'json', as: 'Geometry' } }],\r\n  output: { kind: 'bool' },                       // concrete, never 'inferred'\r\n  instructions: 'Bounding-box overlap between two geometries. Cheap; a pre-filter for ST_Contains.',\r\n  emit: { postgres: '({left} && {right})' },      // PARENTHESIZED; per Dialect.name\r\n  selectivity: 0.1,\r\n});\r\nregistry.registerOperatorRun('&&', (args) => Value.of(bboxOverlaps(args.left, args.right)));\r\n\r\n// reference it by name with NAMED operands:\r\n// { kind: 'operator', op: '&&', args: { left: <expr>, right: <expr> } }\r\n```\r\n\r\n```sql\r\nWHERE (\"parcel\".\"shape\" && ST_GeomFromGeoJSON($1))\r\n```\r\n\r\nThe operand's **declared type reaches emission**, so a document operand binds\r\nthrough that type's own `cast` rather than the dialect's default json cast —\r\nwithout it Postgres refuses the statement (`operator does not exist: geometry &&\r\njsonb`). A value position asserts only what its declaration WROTE: an operand's\r\ncast resolves from the operand's own `with` bag and never from the refinement's\r\ndefaults, since those would pin a typmod the value never had to satisfy. A cast\r\nneeding an option the operand did not write is refused at emit\r\n(`cast.unwritten-option`).\r\n\r\n`registry.operatorList()` enumerates them; `describeOperators(engine)` renders\r\nthe promptable block (and `describeEngine` includes it when any is registered).\r\nA dialect with no `emit` entry is **refused** at emit\r\n(`operator.unsupported-dialect`), never degraded to a neutral fragment; so is a\r\nmissing `run` on the in-memory road (`operator.no-run`), because an operator is\r\nusually a predicate and NULL there means zero rows. Full rules — the name\r\ncharset, the template checks, and what `OperatorDef` deliberately does not carry\r\n— are in `aeye-query.md`.\r\n\r\n### Function reference\r\n\r\nAll builtin names are **camelCase** (no underscores). Where the emitted SQL\r\nfunction name differs from the camelCase name it is shown in the *SQL* column;\r\notherwise the SQL is `name(args)` on both dialects. `base` is the portable\r\nANSI dialect, `postgres` the pg dialect (they differ only where noted).\r\n\r\n**Window** (`e.window`)\r\n\r\n| function                    | SQL (base = postgres) | notes                                   |\r\n| --------------------------- | --------------------- | --------------------------------------- |\r\n| `rowNumber()`               | `row_number()`        | renamed from `row_number`               |\r\n| `rank()`                    | `rank()`              |                                         |\r\n| `denseRank()`               | `dense_rank()`        | renamed from `dense_rank`               |\r\n| `lag(value, offset?, default?)`  | `lag(…)`         |                                         |\r\n| `lead(value, offset?, default?)` | `lead(…)`        |                                         |\r\n| `percentRank()`             | `percent_rank()`      | `(rank − 1) / (N − 1)`                   |\r\n| `cumeDist()`                | `cume_dist()`         |                                         |\r\n| `ntile(n)`                  | `ntile(n)`            | 1-based bucket over `n` equal buckets   |\r\n| `firstValue(value)`         | `first_value(value)`  |                                         |\r\n| `lastValue(value)`          | `last_value(value)`   | full-partition frame (see note below)   |\r\n| `nthValue(value, n)`        | `nth_value(value, n)` | 1-based                                 |\r\n\r\n**Scalar — string** (`e.fn`)\r\n\r\n| function                              | SQL name        |\r\n| ------------------------------------- | --------------- |\r\n| `lower` `upper` `trim` `length` `substring` `replace` `concat` | *(same)* |\r\n| `trimLeft(value)`                     | `ltrim`         |\r\n| `trimRight(value)`                    | `rtrim`         |\r\n| `left(value, count)` `right(value, count)` | *(same)*   |\r\n| `padLeft(value, length, fill?)`       | `lpad`          |\r\n| `padRight(value, length, fill?)`      | `rpad`          |\r\n| `repeat(value, count)` `reverse(value)` | *(same)*      |\r\n| `indexOf(value, search)`              | `strpos` (1-based, 0 = absent) |\r\n| `startsWith(value, search)`           | `starts_with`   |\r\n| `splitPart(value, delimiter, index)`  | `split_part` (1-based) |\r\n| `concatWs(separator, values)`         | `concat_ws` (`values` = one array arg, like `concat`) |\r\n\r\n**Scalar — math** (`e.fn`)\r\n\r\n| function                              | SQL name        |\r\n| ------------------------------------- | --------------- |\r\n| `abs` `ceil` `floor` `round` `sqrt` `power` | *(same)*  |\r\n| `mod(value, divisor)` `sign` `exp` `ln` `trunc` `pi()` `random()` | *(same)* |\r\n| `log(base, value)`                    | `log(base, value)` |\r\n| `log10(value)`                        | `log` (pg single-arg `log` is base-10) |\r\n| `degrees` `radians` `sin` `cos` `tan` `asin` `acos` `atan` `atan2(y, x)` | *(same)* |\r\n\r\n**Scalar — conditional / other** (`e.fn`)\r\n\r\n| function                              | SQL             |\r\n| ------------------------------------- | --------------- |\r\n| `iif(condition, then, else)`          | `(CASE WHEN condition THEN then ELSE else END)` (both dialects) |\r\n| `coalesce` `nullif` `greatest` `least` `arrayLength` | *(same)* |\r\n| `now()`                               | `now()`         |\r\n| `currentDate()`                       | `CURRENT_DATE` (renamed from `current_date`; bare form, no parens) |\r\n\r\n**Scalar — date / time** (`e.*`) — temporal inputs accept an ISO date/timestamp\r\nstring or a temporal field; the `field` of a selector is a **literal** token\r\n(`'year'`/`'month'`/`'day'`/`'dow'`/`'doy'`/`'week'`/`'hour'`/`'minute'`/\r\n`'second'`/`'quarter'`/`'isodow'`/`'epoch'`) spliced inline (not a bind param).\r\n\r\n| function                              | base SQL                         | postgres SQL                          |\r\n| ------------------------------------- | -------------------------------- | ------------------------------------- |\r\n| `currentTime()` `currentTimestamp()`  | `CURRENT_TIME` / `CURRENT_TIMESTAMP` (bare) | *(same)*                   |\r\n| `year/month/day/hour/minute/second(d)`| `EXTRACT(<PART> FROM d)`          | *(same)*                              |\r\n| `dayOfWeek(d)`                        | `EXTRACT(DOW FROM d)`            | *(same)* — `0`=Sun … `6`=Sat          |\r\n| `dayOfYear(d)` `week(d)`              | `EXTRACT(DOY/WEEK FROM d)`       | *(same)* — `week` is the ISO week     |\r\n| `datePart(field, d)`                  | `EXTRACT(<field> FROM d)`        | `date_part('field', d)`               |\r\n| `dateAdd(field, n, d)`                | `d` (degrade — unchanged)       | `(d + (n \\|\\| ' ' \\|\\| 'field')::interval)` |\r\n| `dateDiff(field, a, b)`               | `(EXTRACT(field FROM b) − EXTRACT(field FROM a))` | `(date_part('field', b) − date_part('field', a))` — component difference |\r\n| `dateTrunc(field, d)`                 | `d` (degrade — unchanged)       | `date_trunc('field', d)`              |\r\n| `makeDate(year, month, day)`          | `make_date(…)`                  | `make_date(…)`                        |\r\n| `dateFormat(d, format)`               | `to_char(d, fmt)`               | `to_char(d, fmt)` — tokens `YYYY/MM/DD/HH24/HH/MI/SS` |\r\n| `epoch(ts)`                           | `EXTRACT(EPOCH FROM ts)`        | *(same)*                              |\r\n| `fromEpoch(value)`                    | `to_timestamp(value)`           | `to_timestamp(value)`                 |\r\n| `age(a, b)`                           | `age(a, b)`                     | `age(a, b)` — runtime returns whole-day span |\r\n\r\n**Scalar — array** (`e.*`) — postgres-native; the base (ANSI) dialect DEGRADES\r\ngracefully (a constant, or the first argument unchanged) and never throws.\r\n\r\n| function                              | base SQL (degrade) | postgres SQL                          |\r\n| ------------------------------------- | ------------------ | ------------------------------------- |\r\n| `arrayContains(arr, value)`           | `(1 = 0)`          | `(value = ANY(arr))`                  |\r\n| `arrayAppend(arr, value)`             | `arr`              | `array_append(arr, value)`            |\r\n| `arrayPrepend(arr, value)`            | `arr`              | `array_prepend(value, arr)`           |\r\n| `arrayConcat(a, b)`                   | `a`                | `(a \\|\\| b)`                          |\r\n| `arrayIndexOf(arr, value)`            | `0`                | `array_position(arr, value)` (1-based)|\r\n| `arraySlice(arr, lo, hi)`             | `arr`              | `arr[lo:hi]` (1-based inclusive)      |\r\n| `arrayRemove(arr, value)`             | `arr`              | `array_remove(arr, value)`            |\r\n| `arrayDistinct(arr)`                  | `arr`              | `ARRAY(SELECT DISTINCT unnest(arr))`  |\r\n| `arrayToString(arr, sep)`             | `''`               | `array_to_string(arr, sep)`           |\r\n| `stringToArray(str, sep)`             | `str`              | `string_to_array(str, sep)`           |\r\n\r\n**Aggregate** (`e.agg` / `e.sum` / …): `count` `sum` `avg` `min` `max`\r\n— all emit `name(args)` (or `count(*)` for empty args). Group 2d adds:\r\n\r\n| function                              | base SQL                         | postgres SQL                          |\r\n| ------------------------------------- | -------------------------------- | ------------------------------------- |\r\n| `stddev(value)` `variance(value)`     | `stddev(…)` / `variance(…)` (sample) | *(same)*                          |\r\n| `stringAgg(value, sep)`               | `string_agg(value, sep)`        | *(same)*                              |\r\n| `countIf(cond)`                       | `sum(CASE WHEN cond THEN 1 ELSE 0 END)` | *(same, both dialects)*       |\r\n| `arrayAgg(value)`                     | `NULL` (degrade)                | `array_agg(value)`                    |\r\n| `boolAnd(value)`                      | `(MIN(CASE WHEN value THEN 1 ELSE 0 END) = 1)` | `bool_and(value)`      |\r\n| `boolOr(value)`                       | `(MAX(CASE WHEN value THEN 1 ELSE 0 END) = 1)` | `bool_or(value)`       |\r\n\r\n## The LLM loop\r\n\r\nThe `llm/` surface turns the engine into an LLM tool:\r\n\r\n```ts\r\nimport { selectTypes, buildSchemas, querySchema, buildQueryTool } from '@aeye/query';\r\n\r\n// 1. Narrow the schema to the Types a request needs (semantic ranking).\r\nconst types = await selectTypes(engine, 'revenue by customer last month');\r\n\r\n// 2. Per-axis `depth` Zod schemas (see \"Schema depth\" below). `'paired'` locks\r\n//    every axis: Type-name positions are enum-locked, field refs are TYPE+FIELD\r\n//    pairs (an `order` source can't be paired with a `user`-only field),\r\n//    relation paths are rooted at a known Type, and function calls are typed.\r\nconst schemas = buildSchemas(engine, { depth: 'paired', types });\r\n\r\n// 3. Or get the tool-input schema, which falls back to a string description\r\n//    past `max` Types (shouldUseStringSchema). `depth` / `functions` thread\r\n//    straight through to `buildSchemas`.\r\nconst schema = querySchema(engine, { types, depth: 'paired' });\r\n\r\n// 4. A ready-wired `@aeye/core` `Tool` — drop it into any core / `@aeye/ai`\r\n//    agent's tool set. Its wire `schema` is the query schema; its custom `parse`\r\n//    REPLACES Zod (validate the envelope → build a runnable `Query` → run full\r\n//    engine validation), throwing a rich `QueryToolError` whose `.message` is a\r\n//    compiler-style report on failure. Its `call` RUNS the built query.\r\n//    `instructions` reflect the active depth + the selected functions.\r\nconst tool = buildQueryTool(engine, { depth: 'paired' });\r\nconst query = await tool.parse(ctx, JSON.stringify({ query: someQueryDef })); // built Query (throws QueryToolError on failure)\r\nconst result = awa","readmeFilename":"README.md"}