├── .editorconfig ├── .github └── main.workflow ├── .gitignore ├── README.md ├── license.md ├── package.json ├── src ├── aggregate-functions.ts ├── builtin-functions.ts ├── contains-partition-keys.ts ├── continuation-token.ts ├── executor.ts ├── helpers.ts ├── index.ts ├── parser.d.ts ├── parser.pegjs ├── transformer.ts └── types.ts ├── test ├── aggregate-functions.ts ├── builtin-functions.ts ├── errors.ts ├── index.ts ├── misc.ts ├── paginate.ts ├── partition-keys.ts ├── range.ts ├── subquery.ts └── utils │ ├── test-partition-keys.ts │ └── test-query.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Build, Test, and Publish" { 2 | on = "push" 3 | resolves = "Publish" 4 | } 5 | 6 | action "Install" { 7 | uses = "actions/npm@master" 8 | args = "install" 9 | } 10 | 11 | action "Build" { 12 | needs = "Install" 13 | uses = "actions/npm@master" 14 | args = "run build" 15 | } 16 | 17 | action "Test" { 18 | needs = "Build" 19 | uses = "actions/npm@master" 20 | args = "test" 21 | } 22 | 23 | action "Lint" { 24 | needs = "Build" 25 | uses = "actions/npm@master" 26 | args = "run lint" 27 | } 28 | 29 | action "Tag" { 30 | needs = ["Test", "Lint"] 31 | uses = "actions/bin/filter@master" 32 | args = "tag" 33 | } 34 | 35 | action "Publish" { 36 | needs = "Tag" 37 | uses = "actions/npm@master" 38 | args = "publish" 39 | secrets = ["NPM_AUTH_TOKEN"] 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | *.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cosmosdb-query 2 | 3 | A SQL parser and executer for Cosmos DB. 4 | 5 | ```js 6 | const { default: query } = require("@zeit/cosmosdb-query"); 7 | 8 | const items = [ 9 | { id: "foo" }, 10 | { id: "bar" } 11 | ]; 12 | 13 | const { result } = query("SELECT * FROM c WHERE c.id = @id") 14 | .exec(items, { 15 | parameters: [{ name: "@id", value: "foo" }] 16 | }); 17 | console.log(result); // [ { id: "foo" } ] 18 | ``` 19 | 20 | ### query(sql) 21 | 22 | - `sql` <string> 23 | - Returns: <Query> 24 | 25 | ```js 26 | const q = query("SELECT * FROM c") 27 | ``` 28 | 29 | ## Class: Query 30 | 31 | ### q.exec(items[, options]) 32 | 33 | - `items` <Object[]> | <null> 34 | - `options` <Object> 35 | - `parameters` <Object[]> The parameters to pass to query 36 | - `udf` <Object> 37 | - `maxItemCount` <number> The number of items to return at a time 38 | - `continuation` <Object> Continuation token 39 | - `compositeIndexes `<Object[][]> Optional composite index definitions for validating multiple `ORDER BY` properties. By default, no definition is required and this value is used only for validation. 40 | - Returns: <Object> 41 | - `result` <Object[]> Result documents 42 | - `continuation` <Object> Continuation token for subsequent calls 43 | 44 | 45 | Executes a query for `items`. 46 | 47 | ```js 48 | query(`SELECT VALUE udf.REGEX_MATCH("foobar", ".*bar")`).exec([], { 49 | udf: { 50 | REGEX_MATCH(input, pattern) { 51 | return input.match(pattern) !== null 52 | } 53 | } 54 | }); 55 | ``` 56 | 57 | When the `maxItemCount` and/or `continuation` options are used, 58 | all itesms have to contain the `_rid` field with unique values. 59 | 60 | ```js 61 | const items = [ 62 | { _rid: "a", value: 1 }, 63 | { _rid: "b", value: 2 }, 64 | { _rid: "c", value: 3 } 65 | ]; 66 | const q = query(`SELECT * FROM c`); 67 | const { result, continuation } = q.exec(items, { maxItemCount: 2 }); 68 | console.log(result); // [ { _rid: "a", value: 1 }, { _rid: "b", value: 2 } ] 69 | 70 | const { result: result2 } = q.exec(items, { maxItemCount: 2, continuation }); 71 | console.log(result2); // [ { _rid: "c", value: 3 } ] 72 | ``` 73 | 74 | ### q.containsPartitionKeys(keys) 75 | 76 | - `keys` <string[]> 77 | - Returns: <boolean> 78 | 79 | 80 | Determines whether query may contain partition keys. 81 | 82 | ```js 83 | const q = query("SELECT * FROM c WHERE c.id = 1"); 84 | if (!q.containsPartitionKeys(["/key"])) { 85 | throw new Error("query doesn't contain partition keys"); 86 | } 87 | ``` 88 | 89 | ### q.ast 90 | 91 | - <Object> 92 | 93 | The AST object of query. 94 | 95 | ## Class: SyntaxError 96 | 97 | ```js 98 | const { default: query, SyntaxError } = require("@zeit/cosmosdb-query"); 99 | 100 | try { 101 | query("INVALID SELECT").exec(items); 102 | } catch (err) { 103 | if (err instanceof SyntaxError) { 104 | console.error(err); 105 | } 106 | throw err; 107 | } 108 | ``` 109 | 110 | ## Supported queries 111 | 112 | All queries are supported except spatial functions `ST_ISVALID` and `ST_ISVALIDDETAILED`. 113 | 114 | The spatial functions `ST_INTERSECTS`, `ST_WITHIN`, and `ST_DISTANCE` are supported; use parameters to pass in GeoJSON as strings. Items in collections that are GeoJSON are expected to be of type string. 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zeit/cosmosdb-query", 3 | "version": "0.7.2", 4 | "description": "A SQL parser and executer for Cosmos DB", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.js", 8 | "repository": "github:zeit/cosmosdb-query", 9 | "files": [ 10 | "lib" 11 | ], 12 | "scripts": { 13 | "build": "tsc && yarn build-parser", 14 | "build-parser": "pegjs -o lib/parser.js src/parser.pegjs && cp src/parser.d.ts lib", 15 | "format": "prettier --write '{src,test}/**/*.ts'", 16 | "format-staged": "git diff --cached --name-only '*.ts' | xargs prettier --list-different", 17 | "lint": "eslint --ignore-pattern '*.pegjs' '{src,test}/**/*'", 18 | "lint-staged": "git diff --cached --name-only '*.ts' | xargs eslint", 19 | "prepublishOnly": "yarn build", 20 | "test": "yarn build && best -I 'test/*.ts' -r ts-node/register --verbose" 21 | }, 22 | "dependencies": { 23 | "@babel/generator": "7.6.2", 24 | "@babel/traverse": "7.6.2", 25 | "@turf/turf": "5.1.6", 26 | "@turf/nearest-point-to-line": "6.0.0", 27 | "@turf/point-to-line-distance": "6.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/babel__generator": "7.6.0", 31 | "@types/babel__traverse": "7.0.7", 32 | "@types/node": "12.7.9", 33 | "@typescript-eslint/eslint-plugin": "2.3.2", 34 | "@typescript-eslint/parser": "2.3.2", 35 | "@zeit/best": "0.5.3", 36 | "@zeit/git-hooks": "0.1.4", 37 | "eslint": "6.5.1", 38 | "eslint-config-airbnb": "18.0.1", 39 | "eslint-config-prettier": "6.3.0", 40 | "eslint-plugin-import": "2.18.2", 41 | "eslint-plugin-jsx-a11y": "6.2.3", 42 | "eslint-plugin-react": "7.15.1", 43 | "pegjs": "0.10.0", 44 | "prettier": "1.18.2", 45 | "ts-node": "8.4.1", 46 | "typescript": "3.6.3" 47 | }, 48 | "eslintConfig": { 49 | "extends": [ 50 | "airbnb", 51 | "prettier" 52 | ], 53 | "parser": "@typescript-eslint/parser", 54 | "parserOptions": { 55 | "ecmaVersion": 2018 56 | }, 57 | "rules": { 58 | "no-underscore-dangle": [ 59 | "error", 60 | { 61 | "allow": [ 62 | "_rid" 63 | ] 64 | } 65 | ] 66 | }, 67 | "settings": { 68 | "import/resolver": { 69 | "node": { 70 | "extensions": [ 71 | ".js", 72 | ".ts" 73 | ] 74 | } 75 | } 76 | } 77 | }, 78 | "git": { 79 | "pre-commit": [ 80 | "lint-staged", 81 | "format-staged" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/aggregate-functions.ts: -------------------------------------------------------------------------------- 1 | export const SUM = (a: number[]) => 2 | a.reduce((s, n) => { 3 | if (typeof n === "undefined") return s; 4 | return typeof s === "number" && typeof n === "number" ? s + n : undefined; 5 | }, 0); 6 | 7 | export const AVG = (a: number[]) => { 8 | if ( 9 | !a.length || 10 | a.some(v => typeof v !== "number" && typeof v !== "undefined") 11 | ) { 12 | return undefined; 13 | } 14 | const aa = a.filter(v => typeof v === "number"); 15 | const sum = SUM(aa); 16 | return typeof sum !== "undefined" ? sum / aa.length : undefined; 17 | }; 18 | 19 | export const COUNT = (a: any[]) => 20 | a.filter(v => typeof v !== "undefined").length; 21 | 22 | export const MAX = (a: any[]) => { 23 | const r = a.reduce((max, v) => { 24 | // ignore undefined 25 | if (typeof max === "undefined") return v; 26 | if (typeof v === "undefined") return max; 27 | 28 | // always return undefined if one of items is "object" 29 | if ((max && typeof max === "object") || (v && typeof v === "object")) 30 | return {}; 31 | 32 | if (typeof max === typeof v) return max > v ? max : v; 33 | 34 | // string > number > boolean > null 35 | if (typeof max === "string") return max; 36 | if (typeof v === "string") return v; 37 | if (typeof max === "number") return max; 38 | if (typeof v === "number") return v; 39 | return typeof max === "boolean" ? max : v; 40 | }, undefined); 41 | 42 | return r !== null && typeof r === "object" ? undefined : r; 43 | }; 44 | 45 | export const MIN = (a: any[]) => { 46 | const r = a.reduce((min, v) => { 47 | // ignore undefined 48 | if (typeof min === "undefined") return v; 49 | if (typeof v === "undefined") return min; 50 | 51 | // always return undefined if one of items is "object" 52 | if ((min && typeof min === "object") || (v && typeof v === "object")) 53 | return {}; 54 | 55 | if (typeof min === typeof v) return min < v ? min : v; 56 | 57 | // null < boolean < number < string 58 | if (min === null) return min; 59 | if (v === null) return v; 60 | if (typeof min === "boolean") return min; 61 | if (typeof v === "boolean") return v; 62 | return typeof min === "number" ? min : v; 63 | }, undefined); 64 | 65 | return r !== null && typeof r === "object" ? undefined : r; 66 | }; 67 | -------------------------------------------------------------------------------- /src/builtin-functions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | booleanDisjoint, 3 | centroid, 4 | feature, 5 | distance, 6 | booleanWithin, 7 | // eslint-disable-next-line no-unused-vars 8 | Feature 9 | } from "@turf/turf"; 10 | 11 | // @ts-ignore 12 | import { SyntaxError } from "./parser"; // eslint-disable-line import/no-unresolved 13 | 14 | const typeOf = (v: any) => { 15 | const t = typeof v; 16 | if (t !== "object") return t; 17 | if (v === null) return "null"; 18 | return Array.isArray(v) ? "array" : t; 19 | }; 20 | 21 | const def = (name: string, argTypes: any[], f: Function) => { 22 | const lastType = argTypes[argTypes.length - 1]; 23 | const variableType = lastType && lastType.variable ? lastType : null; 24 | const types = (variableType ? argTypes.slice(0, -1) : argTypes).map(t => 25 | typeof t === "string" ? { type: t, optional: false } : t 26 | ); 27 | const requiredTypes = types.filter(t => !t.optional); 28 | const isVariable = variableType || requiredTypes.length !== types.length; 29 | 30 | return function fn(...a: any[]) { 31 | if ( 32 | isVariable 33 | ? a.length < requiredTypes.length 34 | : a.length !== requiredTypes.length 35 | ) { 36 | throw new SyntaxError( 37 | `The ${name} function requires ${isVariable ? "at least " : ""}${ 38 | requiredTypes.length 39 | } argument(s)` 40 | ); 41 | } 42 | if ( 43 | !types.every( 44 | (t, i) => 45 | t.type === "any" || 46 | typeOf(a[i]) === t.type || 47 | (typeOf(a[i]) === "undefined" && t.optional) 48 | ) 49 | ) { 50 | return undefined; 51 | } 52 | if ( 53 | variableType && 54 | variableType.type !== "any" && 55 | !a.slice(types.length).every(v => typeOf(v) === variableType.type) 56 | ) { 57 | return undefined; 58 | } 59 | return f(...a); 60 | }; 61 | }; 62 | 63 | const deepEqual = (a: any, b: any, partial: boolean): boolean => { 64 | const typeOfA = typeOf(a); 65 | const typeOfB = typeOf(b); 66 | 67 | if (typeOfA === "array" && typeOfB === "array") { 68 | if ((a as Array).length !== (b as Array).length) return false; 69 | return a.every((v: any, i: number) => deepEqual(v, b[i], partial)); 70 | } 71 | 72 | if (typeOfA === "object" && typeOfB === "object") { 73 | const aEntries = Object.entries(a); 74 | const bEntries = Object.entries(b); 75 | if (!partial && aEntries.length !== bEntries.length) return false; 76 | return bEntries.every(([k, v]) => deepEqual(a[k], v, partial)); 77 | } 78 | 79 | return a === b; 80 | }; 81 | 82 | export const ABS = def("ABS", ["number"], (v: number) => Math.abs(v)); 83 | 84 | export const ACOS = def("ACOS", ["number"], (v: number) => Math.acos(v)); 85 | 86 | export const ARRAY_CONCAT = def( 87 | "ARRAY_CONCAT", 88 | ["array", "array", { type: "array", variable: true }], 89 | (...a: Array) => a.reduce((b, c) => [...b, ...c], []) 90 | ); 91 | 92 | export const ARRAY_CONTAINS = def( 93 | "ARRAY_CONTAINS", 94 | ["array", "any", { type: "boolean", optional: true }], 95 | (a: any[], c: any, partial: boolean = false) => 96 | a.some(v => deepEqual(v, c, partial)) 97 | ); 98 | 99 | export const ARRAY_LENGTH = def( 100 | "ARRAY_LENGTH", 101 | ["array"], 102 | (a: any[]) => a.length 103 | ); 104 | 105 | export const ARRAY_SLICE = def( 106 | "ARRAY_SLICE", 107 | ["array", "number", { type: "number", optional: true }], 108 | (a: any[], b: number, c?: number) => a.slice(b, c != null ? b + c : undefined) 109 | ); 110 | 111 | export const ASIN = def("ASIN", ["number"], (v: number) => Math.asin(v)); 112 | 113 | export const ATAN = def("ATAN", ["number"], (v: number) => Math.atan(v)); 114 | 115 | export const ATN2 = def("ASN2", ["number", "number"], (a: number, b: number) => 116 | Math.atan2(b, a) 117 | ); 118 | 119 | export const CEILING = def("CEILING", ["number"], (v: number) => Math.ceil(v)); 120 | 121 | export const CONCAT = def( 122 | "CONCAT", 123 | ["string", "string", { type: "string", variable: true }], 124 | (...a: string[]) => a.join("") 125 | ); 126 | 127 | export const CONTAINS = def( 128 | "CONTAINS", 129 | ["string", "string"], 130 | (a: string, b: string) => a.includes(b) 131 | ); 132 | 133 | export const COS = def("COS", ["number"], (v: number) => Math.cos(v)); 134 | 135 | export const COT = def("COT", ["number"], (v: number) => 1 / Math.tan(v)); 136 | 137 | export const DEGREES = def( 138 | "DEGREES", 139 | ["number"], 140 | (v: number) => (v * 180) / Math.PI 141 | ); 142 | 143 | export const ENDSWITH = def( 144 | "ENDSWITH", 145 | ["string", "string"], 146 | (a: string, b: string) => a.endsWith(b) 147 | ); 148 | 149 | export const EXP = def("EXP", ["number"], (v: number) => Math.exp(v)); 150 | 151 | export const FLOOR = def("FLOOR", ["number"], (v: number) => Math.floor(v)); 152 | 153 | export const INDEX_OF = def( 154 | "INDEX_OF", 155 | ["string", "string"], 156 | (a: string, b: string) => a.indexOf(b) 157 | ); 158 | 159 | export const IS_ARRAY = def("IS_ARRAY", ["any"], (v: any) => Array.isArray(v)); 160 | 161 | export const IS_BOOL = def( 162 | "IS_BOOL", 163 | ["any"], 164 | (v: any) => typeof v === "boolean" 165 | ); 166 | 167 | export const IS_DEFINED = def( 168 | "IS_DEFINED", 169 | ["any"], 170 | (v: any) => typeof v !== "undefined" 171 | ); 172 | 173 | export const IS_NULL = def("IS_NULL", ["any"], (v: any) => v === null); 174 | 175 | export const IS_NUMBER = def( 176 | "IS_NUMBER", 177 | ["any"], 178 | (v: any) => typeof v === "number" 179 | ); 180 | 181 | export const IS_OBJECT = def( 182 | "IS_OBJECT", 183 | ["any"], 184 | (v: any) => Boolean(v) && typeof v === "object" && !Array.isArray(v) 185 | ); 186 | 187 | export const IS_STRING = def( 188 | "IS_STRING", 189 | ["any"], 190 | (v: any) => typeof v === "string" 191 | ); 192 | 193 | export const IS_PRIMITIVE = def( 194 | "IS_PRIMITIVE", 195 | ["any"], 196 | (v: any) => IS_NULL(v) || IS_NUMBER(v) || IS_STRING(v) || IS_BOOL(v) 197 | ); 198 | 199 | export const LEFT = def("LEFT", ["string", "number"], (a: string, b: number) => 200 | a.slice(0, b) 201 | ); 202 | 203 | export const LENGTH = def("LENGTH", ["string"], (v: string) => v.length); 204 | 205 | export const LOG = def("LOG", ["number"], (v: number) => Math.log(v)); 206 | 207 | export const LOG10 = def("LOG10", ["number"], (v: number) => Math.log10(v)); 208 | 209 | export const LOWER = def("LOWER", ["string"], (v: string) => v.toLowerCase()); 210 | 211 | export const LTRIM = def("LTRIM", ["string"], (v: string) => v.trimLeft()); 212 | 213 | export const PI = def("PI", [], () => Math.PI); 214 | 215 | export const POWER = def( 216 | "POWER", 217 | ["number", "number"], 218 | (a: number, b: number) => a ** b 219 | ); 220 | 221 | export const RADIANS = def( 222 | "RADIANS", 223 | ["number"], 224 | (v: number) => (v * Math.PI) / 180 225 | ); 226 | 227 | export const REPLACE = def( 228 | "REPLACE", 229 | ["string", "string", "string"], 230 | (a: string, b: string, c: string) => a.replace(b, c) 231 | ); 232 | 233 | export const REPLICATE = def( 234 | "REPLICATE", 235 | ["string", "number"], 236 | (a: string, b: number) => a.repeat(b) 237 | ); 238 | 239 | export const REVERSE = def("REVERSE", ["string"], (v: string) => 240 | v 241 | .split("") 242 | .reverse() 243 | .join("") 244 | ); 245 | 246 | export const RIGHT = def( 247 | "RIGHT", 248 | ["string", "number"], 249 | (a: string, b: number) => a.slice(-b) 250 | ); 251 | 252 | export const ROUND = def("ROUND", ["number"], (v: number) => Math.round(v)); 253 | 254 | export const RTRIM = def("RTRIM", ["string"], (v: string) => v.trimRight()); 255 | 256 | export const SIGN = def("SIGN", ["number"], (v: number) => Math.sign(v)); 257 | 258 | export const SIN = def("SIN", ["number"], (v: number) => Math.sin(v)); 259 | 260 | export const SQRT = def("SQRT", ["number"], (v: number) => Math.sqrt(v)); 261 | 262 | export const SQUARE = def("SQUARE", ["number"], (v: number) => v ** 2); 263 | 264 | export const STARTSWITH = def( 265 | "STARTSWITH", 266 | ["string", "string"], 267 | (a: string, b: string) => a.startsWith(b) 268 | ); 269 | 270 | export const SUBSTRING = def( 271 | "SUBSTRING", 272 | ["string", "number", { type: "number", optional: true }], 273 | (a: string, b: number, c?: number) => 274 | a.substring(b, c != null ? b + c : undefined) 275 | ); 276 | 277 | export const TAN = def("TAN", ["number"], (v: number) => Math.tan(v)); 278 | 279 | export const TOSTRING = def( 280 | "ToString", 281 | ["any"], 282 | (v?: number | string | boolean) => { 283 | const t = typeOf(v); 284 | if (t === "undefined") { 285 | return undefined; 286 | } 287 | if (t === "object" || t === "array") { 288 | return JSON.stringify(v); 289 | } 290 | return String(v); 291 | } 292 | ); 293 | 294 | export const TRIM = def("TRIM", ["string"], (v: string) => v.trim()); 295 | 296 | export const TRUNC = def("TRUNC", ["number"], (v: number) => Math.trunc(v)); 297 | 298 | export const UPPER = def("UPPER", ["string"], (v: string) => v.toUpperCase()); 299 | 300 | // Spatial functions 301 | 302 | const spatialBinaryOp = (name: string, f: Function) => { 303 | return def(name, ["any", "any"], (a: string | object, b: string | object) => { 304 | const t1 = typeOf(a); 305 | if (t1 === "undefined") { 306 | return undefined; 307 | } 308 | 309 | const aObj = t1 === "object" ? a : JSON.parse(a as string); 310 | 311 | const t2 = typeOf(a); 312 | if (t2 === "undefined") { 313 | return undefined; 314 | } 315 | const bObj = t1 === "object" ? b : JSON.parse(b as string); 316 | 317 | const aFeat = feature(aObj); 318 | const bFeat = feature(bObj); 319 | 320 | return f(aFeat, bFeat); 321 | }); 322 | }; 323 | 324 | export const ST_DISTANCE = spatialBinaryOp( 325 | "ST_DISTANCE", 326 | (a: Feature, b: Feature) => { 327 | // Turf can only handle point distances - take the centroid 328 | // as a workaround. 329 | const pa = centroid(a); 330 | const pb = centroid(b); 331 | 332 | // Convert kilometers to meters. 333 | return distance(pa.geometry.coordinates, pb.geometry.coordinates) * 1000; 334 | } 335 | ); 336 | 337 | export const ST_WITHIN = spatialBinaryOp("ST_WITHIN", booleanWithin); 338 | 339 | export const ST_INTERSECTS = spatialBinaryOp( 340 | "ST_INTERSECTS", 341 | (a: Feature, b: Feature) => !booleanDisjoint(a, b) 342 | ); 343 | 344 | export const REGEXMATCH = def( 345 | "REGEXMATCH", 346 | ["string", "string", { type: "string", optional: true }], 347 | (str: string, p: string, f?: string) => { 348 | let pattern = p; 349 | let flags = f; 350 | if (flags) { 351 | if (!/^[msix]+$/.test(flags)) { 352 | throw new Error(`Unexpectd flags on REGEXMATCH: ${flags}`); 353 | } 354 | 355 | if (flags.includes("x")) { 356 | pattern = pattern.replace(/\s/g, ""); 357 | flags = flags.replace(/x/g, ""); 358 | } 359 | } 360 | 361 | // TODO: cache RegExp instances? 362 | const re = new RegExp(pattern, flags); 363 | return re.test(str); 364 | } 365 | ); 366 | -------------------------------------------------------------------------------- /src/contains-partition-keys.ts: -------------------------------------------------------------------------------- 1 | function conditionKeyNodes(node: { [x: string]: any }): any[] { 2 | if (node.type === "scalar_binary_expression") { 3 | const { left, right } = node; 4 | if (node.operator === "=") { 5 | if (left.type === "scalar_member_expression") { 6 | return [[left]]; 7 | } 8 | } else if (node.operator === "AND") { 9 | const rightNodes = conditionKeyNodes(right); 10 | return conditionKeyNodes(left) 11 | .map(a => rightNodes.map(b => [...a, ...b])) 12 | .reduce((a, b) => [...a, ...b], []); 13 | } else if (node.operator === "OR") { 14 | return [...conditionKeyNodes(left), ...conditionKeyNodes(right)]; 15 | } 16 | } 17 | 18 | return []; 19 | } 20 | 21 | function toPartitionKey(node: { [x: string]: any }): string | null { 22 | if (node.type === "scalar_member_expression") { 23 | return `${toPartitionKey(node.object) || ""}/${node.property.name || 24 | node.property.value}`; 25 | } 26 | return null; 27 | } 28 | 29 | export default function containsPartitionKeys( 30 | ast: { 31 | [x: string]: any; 32 | }, 33 | paths: string[] 34 | ) { 35 | if (!paths.length) return true; 36 | if (!ast.body || !ast.body.where) return false; 37 | 38 | const { condition } = ast.body.where; 39 | const nodes = conditionKeyNodes(condition); 40 | const keys = nodes.map(n => new Set(n.map(toPartitionKey))); 41 | return keys.every(k => paths.every(p => k.has(p))); 42 | } 43 | -------------------------------------------------------------------------------- /src/continuation-token.ts: -------------------------------------------------------------------------------- 1 | export type Token = { 2 | // the value of the _rid field 3 | RID: string; 4 | 5 | // the page number 6 | RT: number; 7 | 8 | // offset 9 | TRC: number; 10 | 11 | // offset for joined items which has the same "_rid" 12 | SRC?: number; 13 | 14 | // the value for the ORDER BY clause 15 | RTD?: any[]; 16 | }; 17 | 18 | function encodeAnyArray(RTD: any[]) { 19 | return Buffer.from(JSON.stringify(RTD)).toString("base64"); 20 | } 21 | 22 | function decodeAnyArray(RTD: string) { 23 | const v = JSON.parse(Buffer.from(RTD, "base64").toString()); 24 | if (!Array.isArray(v)) { 25 | throw new TypeError("invalid RTS on continuation token"); 26 | } 27 | return v; 28 | } 29 | 30 | export const encode = (t: Token) => { 31 | return `+RID:${t.RID}#RT:${t.RT}${t.SRC ? `#SRC:${t.SRC}` : ""}#TRC:${t.TRC}${ 32 | typeof t.RTD !== "undefined" ? `#RTD:${encodeAnyArray(t.RTD)}` : "" 33 | }`; 34 | }; 35 | 36 | export const decode = (token: string) => { 37 | return token 38 | .slice(1) 39 | .split("#") 40 | .reduce( 41 | (o, s) => { 42 | const i = s.indexOf(":"); 43 | const key = s.slice(0, i); 44 | let value: any = s.slice(i + 1); 45 | switch (key) { 46 | case "RT": 47 | case "SRC": 48 | case "TRC": 49 | value = parseInt(value, 10); 50 | break; 51 | case "RTD": 52 | value = decodeAnyArray(value); 53 | break; 54 | default: 55 | // noop 56 | } 57 | // eslint-disable-next-line no-param-reassign 58 | o[key] = value; 59 | return o; 60 | }, 61 | {} as { [x: string]: any } 62 | ) as Token; 63 | }; 64 | -------------------------------------------------------------------------------- /src/executor.ts: -------------------------------------------------------------------------------- 1 | import * as aggregateFunctions from "./aggregate-functions"; 2 | import * as builtinFunctions from "./builtin-functions"; 3 | import * as helpers from "./helpers"; 4 | // eslint-disable-next-line no-unused-vars 5 | import { CompositeIndex } from "./types"; 6 | 7 | export default ( 8 | collection: any[], 9 | { 10 | code, 11 | parameters, 12 | udf = {}, 13 | maxItemCount, 14 | continuation, 15 | compositeIndexes 16 | }: { 17 | code: string; 18 | parameters?: { 19 | name: string; 20 | value: any; 21 | }[]; 22 | udf?: { 23 | [x: string]: any; 24 | }; 25 | maxItemCount?: number; 26 | continuation?: { 27 | token: string; 28 | }; 29 | compositeIndexes?: CompositeIndex[][]; 30 | } 31 | ) => { 32 | // eslint-disable-next-line no-new-func 33 | const execute = new Function(`"use strict";return (${code})`)(); 34 | 35 | const params: { [x: string]: any } = {}; 36 | (parameters || []).forEach(({ name, value }) => { 37 | params[name.slice(1)] = value; 38 | }); 39 | 40 | return execute( 41 | aggregateFunctions, 42 | builtinFunctions, 43 | collection, 44 | helpers, 45 | udf, 46 | params, 47 | maxItemCount, 48 | continuation, 49 | compositeIndexes 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as continuationToken from "./continuation-token"; 2 | // eslint-disable-next-line no-unused-vars 3 | import { CompositeIndex } from "./types"; 4 | 5 | const TYPE_ORDERS = new Set([ 6 | "undefined", 7 | "null", 8 | "boolean", 9 | "number", 10 | "string", 11 | "array", 12 | "object" 13 | ]); 14 | 15 | const typeOf = (v: any) => { 16 | const t = typeof v; 17 | if (t !== "object") return t; 18 | if (v === null) return "null"; 19 | return Array.isArray(v) ? "array" : t; 20 | }; 21 | 22 | const equalTypes = (a: any, b: any) => { 23 | const typeOfA = typeOf(a); 24 | const typeOfB = typeOf(b); 25 | return typeOfA === typeOfB && typeOfA !== "undefined"; 26 | }; 27 | 28 | const deepEqual = (a: any, b: any): boolean => { 29 | const typeOfA = typeOf(a); 30 | const typeOfB = typeOf(b); 31 | 32 | if (typeOfA === "array" && typeOfB === "array") { 33 | if ((a as Array).length !== (b as Array).length) return false; 34 | return a.every((v: any, i: number) => deepEqual(v, b[i])); 35 | } 36 | 37 | if (typeOfA === "object" && typeOfB === "object") { 38 | const aEntries = Object.entries(a); 39 | const bEntries = Object.entries(b); 40 | if (aEntries.length !== bEntries.length) return false; 41 | return aEntries.every(([k, v]) => deepEqual(v, b[k])); 42 | } 43 | 44 | return a === b; 45 | }; 46 | 47 | const comparator = (a: any, b: any) => { 48 | if (a === b) return 0; 49 | 50 | const aType = typeOf(a); 51 | const bType = typeOf(b); 52 | 53 | if (aType === bType) { 54 | if (aType === "object") return 0; 55 | return a < b ? -1 : 1; 56 | } 57 | 58 | const typeOrders = [...TYPE_ORDERS]; 59 | for (let i = 0; i < typeOrders.length; i += 1) { 60 | if (aType === typeOrders[i]) return -1; 61 | if (bType === typeOrders[i]) return 1; 62 | } 63 | 64 | return 0; 65 | }; 66 | 67 | const getValue = (doc: any, [key, ...keys]: string[]): any => { 68 | const value = typeof doc === "object" && doc ? doc[key] : undefined; 69 | if (keys.length && typeof value !== "undefined") { 70 | return getValue(value, keys); 71 | } 72 | return value; 73 | }; 74 | 75 | export const stripUndefined = (obj: any): any => { 76 | if (Array.isArray(obj)) { 77 | // remove `undefined` from array unlike JSON 78 | return obj.reduce( 79 | (o, v) => (typeof v !== "undefined" ? [...o, stripUndefined(v)] : o), 80 | [] 81 | ); 82 | } 83 | 84 | if (obj && typeof obj === "object") { 85 | return Object.entries(obj).reduce( 86 | (o, [k, v]) => { 87 | if (typeof v !== "undefined") { 88 | // eslint-disable-next-line no-param-reassign 89 | o[k] = stripUndefined(v); 90 | } 91 | return o; 92 | }, 93 | {} as any 94 | ); 95 | } 96 | 97 | return obj; 98 | }; 99 | 100 | export const equal = (a: any, b: any) => { 101 | if (typeof a === "undefined" || typeof b === "undefined") { 102 | return undefined; 103 | } 104 | 105 | if (!equalTypes(a, b)) { 106 | return false; 107 | } 108 | 109 | return deepEqual(a, b); 110 | }; 111 | 112 | export const notEqual = (a: any, b: any) => { 113 | const eq = equal(a, b); 114 | return typeof eq !== "undefined" ? !eq : undefined; 115 | }; 116 | 117 | export const compare = (operator: string, a: any, b: any) => { 118 | if (!equalTypes(a, b)) { 119 | return undefined; 120 | } 121 | 122 | const typeOfA = typeOf(a); 123 | if (typeOfA === "object" || typeOfA === "array") { 124 | return undefined; 125 | } 126 | 127 | switch (operator) { 128 | case ">": 129 | return a > b; 130 | case ">=": 131 | return a >= b; 132 | case "<": 133 | return a < b; 134 | case "<=": 135 | return a <= b; 136 | default: 137 | throw new TypeError(`Unexpected operator: ${operator}`); 138 | } 139 | }; 140 | 141 | export const and = (a: any, b: any) => { 142 | if (typeof a !== "boolean" || typeof b !== "boolean") { 143 | return a === false || b === false ? false : undefined; 144 | } 145 | 146 | return a && b; 147 | }; 148 | 149 | export const or = (a: any, b: any) => { 150 | if (typeof a !== "boolean" || typeof b !== "boolean") { 151 | return a === true || b === true ? true : undefined; 152 | } 153 | 154 | return a || b; 155 | }; 156 | 157 | export const not = (v: any) => (typeof v === "boolean" ? !v : undefined); 158 | 159 | export const calculate = (operator: string, a: any, b: any) => { 160 | if (typeof a !== "number" || typeof b !== "number") { 161 | return undefined; 162 | } 163 | 164 | switch (operator) { 165 | case "+": 166 | return a + b; 167 | case "-": 168 | return a - b; 169 | case "*": 170 | return a * b; 171 | case "/": 172 | return a / b; 173 | case "%": 174 | return a % b; 175 | case "|": 176 | // eslint-disable-next-line no-bitwise 177 | return a | b; 178 | case "&": 179 | // eslint-disable-next-line no-bitwise 180 | return a & b; 181 | case "^": 182 | // eslint-disable-next-line no-bitwise 183 | return a ^ b; 184 | case "<<": 185 | // eslint-disable-next-line no-bitwise 186 | return a << b; 187 | case ">>": 188 | // eslint-disable-next-line no-bitwise 189 | return a >> b; 190 | case ">>>": 191 | // eslint-disable-next-line no-bitwise 192 | return a >>> b; 193 | default: 194 | throw new TypeError(`Unexpected operator: ${operator}`); 195 | } 196 | }; 197 | 198 | export const calculateUnary = (operator: string, v: any) => { 199 | if (typeof v !== "number") { 200 | return undefined; 201 | } 202 | 203 | switch (operator) { 204 | case "+": 205 | return +v; 206 | case "-": 207 | return -v; 208 | case "~": 209 | // eslint-disable-next-line no-bitwise 210 | return ~v; 211 | default: 212 | throw new TypeError(`Unexpected operator: ${operator}`); 213 | } 214 | }; 215 | 216 | export const concat = (a: any, b: any) => 217 | typeof a === "string" && typeof b === "string" ? a + b : undefined; 218 | 219 | export const sort = ( 220 | collection: { 221 | [x: string]: any; 222 | }[], 223 | getRid: (a: any) => any, 224 | compositeIndexes?: CompositeIndex[][], 225 | ...orders: [any[], boolean][] 226 | ) => { 227 | if (orders.length > 1 && compositeIndexes) { 228 | const found = compositeIndexes.some(indexes => { 229 | if (indexes.length !== orders.length) return false; 230 | 231 | return indexes.every((index, i) => { 232 | const [keys, desc] = orders[i]; 233 | const path = `/${keys.slice(1).join("/")}`; 234 | const order = desc ? "descending" : "ascending"; 235 | return path === index.path && order === index.order; 236 | }); 237 | }); 238 | 239 | if (!found) { 240 | throw new Error( 241 | "The order by query does not have a corresponding composite index that it can be served from." 242 | ); 243 | } 244 | } 245 | 246 | const sorted = collection.slice().sort((a, b) => { 247 | for (let i = 0, l = orders.length; i < l; i += 1) { 248 | const [keys, desc] = orders[i]; 249 | const aValue = getValue(a, keys); 250 | const bValue = getValue(b, keys); 251 | const r = comparator(aValue, bValue); 252 | if (r !== 0) return desc ? -r : r; 253 | } 254 | 255 | // sort by `_rid` 256 | const rid1 = getRid(a); 257 | const rid2 = getRid(b); 258 | return comparator(rid1, rid2); 259 | }); 260 | 261 | if (orders.length !== 1) return sorted; 262 | 263 | const [keys] = orders[0]; 264 | return sorted.filter(d => typeof getValue(d, keys) !== "undefined"); 265 | }; 266 | 267 | export const paginate = ( 268 | collection: [{ [x: string]: any }, { [x: string]: any }][], 269 | maxItemCount?: number, 270 | continuation?: { token: string }, 271 | getRid?: (a: any) => any, 272 | ...orders: [any[], boolean][] 273 | ) => { 274 | let result = collection; 275 | let token: continuationToken.Token; 276 | let offset = 0; 277 | 278 | if (continuation) { 279 | token = continuationToken.decode(continuation.token); 280 | 281 | let src = 0; 282 | let index = result.findIndex(([, d]) => { 283 | if (typeof token.RTD !== "undefined" && orders.length) { 284 | for (let i = 0, l = orders.length; i < l; i += 1) { 285 | const [keys, desc] = orders[i]; 286 | const rtd = getValue(d, keys); 287 | const r = comparator(rtd, token.RTD[i]) * (desc ? -1 : 1); 288 | if (r < 0) return false; 289 | if (r > 0) return true; 290 | } 291 | } 292 | 293 | const rid = getRid(d); 294 | if (!rid) { 295 | throw new Error( 296 | "The _rid field is required on items for the continuation option." 297 | ); 298 | } 299 | if (comparator(rid, token.RID) < 0) return false; 300 | if (!token.SRC || rid !== token.RID) return true; 301 | if (src === token.SRC) return true; 302 | src += 1; 303 | return false; 304 | }); 305 | 306 | index = index >= 0 ? index : result.length; 307 | result = result.slice(index); 308 | offset += index; 309 | } 310 | 311 | let nextContinuation: { 312 | token: string; 313 | range: { min: string; max: string }; 314 | } | null = null; 315 | if (maxItemCount > 0) { 316 | if (result.length > maxItemCount) { 317 | const [, item] = result[maxItemCount]; 318 | const RID = getRid(item); 319 | if (!RID) { 320 | throw new Error( 321 | "The _rid field is required on items for the maxItemCount option." 322 | ); 323 | } 324 | const RT = (token ? token.RT : 0) + 1; 325 | const TRC = (token ? token.TRC : 0) + maxItemCount; 326 | const RTD = orders.length 327 | ? orders.map(([keys]) => getValue(item, keys)) 328 | : undefined; 329 | 330 | // calculate "SRC" which is the offset of items with the same `_rid`; 331 | let j = offset + maxItemCount - 1; 332 | for (; j >= 0; j -= 1) { 333 | if (getRid(collection[j][1]) !== RID) break; 334 | } 335 | const SRC = offset + maxItemCount - j - 1; 336 | 337 | const nextToken = continuationToken.encode({ RID, RT, SRC, TRC, RTD }); 338 | 339 | nextContinuation = { 340 | token: nextToken, 341 | range: { min: "", max: "FF" } 342 | }; 343 | } 344 | 345 | result = result.slice(0, maxItemCount); 346 | } 347 | 348 | return { 349 | result: stripUndefined(result.map(([r]) => r)), 350 | continuation: nextContinuation 351 | }; 352 | }; 353 | 354 | export const exists = (rows: any[]) => 355 | rows.length > 1 ? true : typeof rows[0] !== "undefined"; 356 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import generate from "@babel/generator"; 3 | import containsPartitionKeys from "./contains-partition-keys"; 4 | import execute from "./executor"; 5 | import { parse, SyntaxError } from "./parser"; // eslint-disable-line import/no-unresolved 6 | import transform from "./transformer"; 7 | import { CompositeIndex } from "./types"; 8 | 9 | class Query { 10 | _query: string; 11 | 12 | _code: string | undefined | null; 13 | 14 | ast: { 15 | [x: string]: any; 16 | }; 17 | 18 | constructor(query: string) { 19 | this._query = query; 20 | this._code = null; 21 | this.ast = parse(this._query.trim()); 22 | } 23 | 24 | get code() { 25 | if (!this._code) { 26 | const jsAst = transform(this.ast); 27 | const { code } = generate(jsAst); 28 | this._code = code; 29 | } 30 | return this._code; 31 | } 32 | 33 | exec( 34 | coll: {}[], 35 | { 36 | parameters, 37 | udf, 38 | maxItemCount, 39 | continuation, 40 | compositeIndexes 41 | }: { 42 | parameters?: { 43 | name: string; 44 | value: any; 45 | }[]; 46 | udf?: { 47 | [x: string]: any; 48 | }; 49 | maxItemCount?: number; 50 | continuation?: { 51 | token: string; 52 | }; 53 | compositeIndexes?: CompositeIndex[][]; 54 | } = {} 55 | ) { 56 | const { code } = this; 57 | if (!code) { 58 | throw new Error("Missing code"); 59 | } 60 | 61 | return execute(coll, { 62 | code, 63 | parameters, 64 | udf, 65 | maxItemCount, 66 | continuation, 67 | compositeIndexes 68 | }); 69 | } 70 | 71 | containsPartitionKeys(paths: string[]) { 72 | return containsPartitionKeys(this.ast, paths); 73 | } 74 | } 75 | 76 | export default (query: string) => new Query(query); 77 | export { CompositeIndex, SyntaxError }; 78 | -------------------------------------------------------------------------------- /src/parser.d.ts: -------------------------------------------------------------------------------- 1 | export declare class SyntaxError extends Error {} 2 | export declare const parse: (s: string) => any; 3 | -------------------------------------------------------------------------------- /src/parser.pegjs: -------------------------------------------------------------------------------- 1 | // Reference: https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-select 2 | 3 | { 4 | function buildBinaryExpression(head, tail) { 5 | return tail.reduce((left, [, operator, , right]) => ({ 6 | type: 'scalar_binary_expression', 7 | left, 8 | operator, 9 | right 10 | }), head) 11 | } 12 | } 13 | 14 | sql = _ body:select_query _ 15 | { 16 | return { 17 | type: 'sql', 18 | body 19 | } 20 | } 21 | 22 | select_query 23 | = select _ 24 | top:(top _ v:top_specification { return v })? _ 25 | select:select_specification _ 26 | from:(from _ v:from_specification { return v })? _ 27 | where:(where _ v:filter_condition { return v })? _ 28 | orderBy:(order _ by _ v:sort_specification { return v })? 29 | { 30 | return { 31 | type: 'select_query', 32 | top, 33 | select, 34 | from, 35 | where, 36 | orderBy 37 | } 38 | } 39 | 40 | select_specification 41 | = '*' 42 | { 43 | return { 44 | type: 'select_specification', 45 | '*': true 46 | } 47 | } 48 | / properties:object_property_list 49 | { 50 | return { 51 | type: 'select_specification', 52 | properties 53 | } 54 | } 55 | / value _ value:scalar_expression 56 | { 57 | return { 58 | type: 'select_specification', 59 | value 60 | } 61 | } 62 | 63 | object_property_list 64 | = head:object_property 65 | tail:(_ "," _ v:object_property { return v })* 66 | { 67 | return { 68 | type: 'object_property_list', 69 | properties: [head, ...tail] 70 | } 71 | } 72 | 73 | from_specification 74 | = source:from_source joins:(_ join _ v:(from_source) { return v })* 75 | { 76 | return { 77 | type: 'from_specification', 78 | source, 79 | joins 80 | } 81 | } 82 | 83 | from_source 84 | = alias:identifier _ in _ expression:collection_expression 85 | { 86 | return { 87 | type: 'from_source', 88 | expression, 89 | alias, 90 | iteration: true 91 | } 92 | } 93 | / expression:collection_expression alias:((_ as)? _ v:identifier { return v })? 94 | { 95 | return { 96 | type: 'from_source', 97 | expression, 98 | alias 99 | } 100 | } 101 | 102 | collection_expression 103 | = collection_member_expression 104 | / collection_primary_expression 105 | / collection_subquery_expression 106 | 107 | filter_condition 108 | = condition:scalar_expression 109 | { 110 | return { 111 | type: 'filter_condition', 112 | condition 113 | } 114 | } 115 | 116 | sort_specification 117 | = head:sort_expression tail:(_ "," _ v:sort_expression { return v })* 118 | { 119 | return { 120 | type: 'sort_specification', 121 | expressions: [head, ...tail] 122 | } 123 | } 124 | 125 | sort_expression 126 | = expression:scalar_expression order:(_ v:(asc / desc) { return v })? 127 | { 128 | return { 129 | type: 'sort_expression', 130 | expression, 131 | order 132 | } 133 | } 134 | 135 | scalar_expression 136 | = scalar_conditional_expression 137 | 138 | scalar_function_expression 139 | = udf _ "." _ name:identifier _ "(" _ args:scalar_expression_list _ ")" 140 | { 141 | return { 142 | type: 'scalar_function_expression', 143 | name, 144 | arguments: args, 145 | udf: true 146 | } 147 | } 148 | / name:identifier _ "(" _ args:scalar_expression_list _ ")" 149 | { 150 | return { 151 | type: 'scalar_function_expression', 152 | name, 153 | arguments: args 154 | } 155 | } 156 | 157 | scalar_object_expression 158 | = "{" _ 159 | head:scalar_object_element_property? 160 | tail:(_ "," _ v:scalar_object_element_property { return v })* 161 | _ "}" 162 | { 163 | return { 164 | type: "scalar_object_expression", 165 | properties: head ? [head, ...tail] : [] 166 | } 167 | } 168 | 169 | scalar_array_expression 170 | = "[" _ elements:scalar_expression_list _ "]" 171 | { 172 | return { 173 | type: "scalar_array_expression", 174 | elements 175 | } 176 | } 177 | 178 | constant 179 | = undefined_constant 180 | / null_constant 181 | / boolean_constant 182 | / number_constant 183 | / string_constant 184 | / array_constant 185 | / object_constant 186 | 187 | undefined_constant 188 | = "undefined" 189 | { return { type: 'undefined_constant' } } 190 | 191 | null_constant 192 | = null 193 | { return { type: 'null_constant' } } 194 | 195 | boolean_constant 196 | = false 197 | { 198 | return { 199 | type: 'boolean_constant', 200 | value: false 201 | } 202 | } 203 | / true 204 | { 205 | return { 206 | type: 'boolean_constant', 207 | value: true 208 | } 209 | } 210 | 211 | number_constant 212 | = "-"? hex:"0x"? [0-9]+ ("." [0-9]+)? { 213 | return { 214 | type: "number_constant", 215 | // FIXME: support hex with float? 216 | value: hex ? parseInt(text(), 16) : parseFloat(text()) 217 | } 218 | } 219 | 220 | string_constant 221 | = "\"" chars:double_string_character* "\"" 222 | { 223 | return { 224 | type: "string_constant", 225 | value: chars.join("") 226 | } 227 | } 228 | / "'" chars:single_string_character* "'" 229 | { 230 | return { 231 | type: "string_constant", 232 | value: chars.join("") 233 | } 234 | } 235 | 236 | array_constant 237 | = "[" _ head:constant tail:(_ "," _ v:constant { return v })* _ "]" 238 | { 239 | return { 240 | type: "array_constant", 241 | elements: [head, ...tail] 242 | } 243 | } 244 | 245 | object_constant 246 | = "{" _ 247 | head:object_constant_property 248 | tail:(_ "," _ v:object_constant_property { return v })* 249 | _ "}" 250 | { 251 | return { 252 | type: "object_constant", 253 | properties: [head, ...tail] 254 | } 255 | } 256 | 257 | // by us 258 | _ = (whitespace / comment)* 259 | 260 | whitespace 261 | = [ \t\n\r] 262 | 263 | comment 264 | = "--" (![\n\r] source_character)* 265 | 266 | select = "SELECT"i !identifier_start 267 | top = "TOP"i !identifier_start 268 | from = "FROM"i !identifier_start 269 | where = "WHERE"i !identifier_start 270 | order = "ORDER"i !identifier_start 271 | by = "BY"i !identifier_start 272 | as = "AS"i !identifier_start 273 | join = "JOIN"i !identifier_start 274 | in = "IN"i !identifier_start 275 | value = "VALUE"i !identifier_start 276 | asc = "ASC"i !identifier_start { return "ASC" } 277 | desc = "DESC"i !identifier_start { return "DESC" } 278 | and = "AND"i !identifier_start { return "AND" } 279 | or = "OR"i !identifier_start { return "OR" } 280 | not = "NOT"i !identifier_start { return "NOT" } 281 | between = "BETWEEN"i !identifier_start 282 | exists = "EXISTS"i !identifier_start 283 | array = "ARRAY"i !identifier_start 284 | null = "null" !identifier_start 285 | true = "true" !identifier_start 286 | false = "false" !identifier_start 287 | udf = "udf" !identifier_start 288 | 289 | reserved 290 | = select 291 | / top 292 | / from 293 | / where 294 | / order 295 | / by 296 | / as 297 | / join 298 | / in 299 | / value 300 | / asc 301 | / desc 302 | / and 303 | / or 304 | / not 305 | / between 306 | / exists 307 | / array 308 | / null 309 | / true 310 | / false 311 | / udf 312 | 313 | identifier 314 | = !reserved name:identifier_name 315 | { 316 | return { 317 | type: 'identifier', 318 | name 319 | } 320 | } 321 | 322 | identifier_start 323 | = [a-zA-Z_] 324 | 325 | identifier_name 326 | = head:identifier_start tail:[a-zA-Z0-9_]* 327 | { return head + tail.join('') } 328 | 329 | parameter_name 330 | = "@" identifier_name 331 | { 332 | return { 333 | type: 'parameter_name', 334 | name: text() 335 | } 336 | } 337 | 338 | array_index 339 | = unsigned_integer 340 | 341 | unary_operator 342 | = "+" 343 | / "-" 344 | / "~" 345 | / not 346 | 347 | double_string_character 348 | = !('"' / "\\") source_character { return text(); } 349 | / "\\" seq:escape_sequence { return seq } 350 | 351 | single_string_character 352 | = !("'" / "\\") source_character { return text(); } 353 | / "\\" seq:escape_sequence { return seq } 354 | 355 | source_character 356 | = . 357 | 358 | escape_sequence 359 | = charactor_escape_sequence 360 | / unicode_escape_sequence 361 | 362 | charactor_escape_sequence 363 | = single_escape_character 364 | / non_escape_character 365 | 366 | single_escape_character 367 | = "'" 368 | / '"' 369 | / "\\" 370 | / "b" { return "\b" } 371 | / "f" { return "\f" } 372 | / "n" { return "\n" } 373 | / "r" { return "\r" } 374 | / "t" { return "\t" } 375 | 376 | non_escape_character 377 | = !(escape_character) source_character 378 | { return text() } 379 | 380 | escape_character 381 | = single_escape_character 382 | / "u" 383 | 384 | unicode_escape_sequence 385 | = "u" digits:$(hex_digit hex_digit hex_digit hex_digit) 386 | { return String.fromCharCode(parseInt(digits, 16)) } 387 | 388 | hex_digit 389 | = [0-9a-f]i 390 | 391 | object_property 392 | = property:scalar_expression alias:((_ as)? _ v:identifier { return v })? 393 | { return { property, alias } } 394 | 395 | scalar_primary_expression 396 | = identifier 397 | / parameter_name 398 | / constant 399 | / scalar_array_expression 400 | / scalar_object_expression 401 | / subquery_expression 402 | / "(" _ expression:scalar_expression _ ")" 403 | { return expression } 404 | 405 | subquery_expression 406 | = array_subquery_expression 407 | / exists_subquery_expression 408 | / scalar_subquery_expression 409 | 410 | array_subquery_expression 411 | = array _ expression:subquery 412 | { 413 | return { 414 | type: "array_subquery_expression", 415 | expression 416 | } 417 | } 418 | 419 | exists_subquery_expression 420 | = exists _ expression:subquery 421 | { 422 | return { 423 | type: 'exists_subquery_expression', 424 | expression 425 | } 426 | } 427 | 428 | scalar_subquery_expression 429 | = expression:subquery 430 | { 431 | return { 432 | type: "scalar_subquery_expression", 433 | expression 434 | } 435 | } 436 | 437 | scalar_member_expression 438 | = head:scalar_primary_expression 439 | tail:( 440 | _ "." _ property:identifier _ 441 | { return { property, computed: false } } 442 | / _ "[" _ property:(string_constant / array_index / parameter_name) _ "]" 443 | { return { property, computed: true } } 444 | )* 445 | { 446 | return tail.reduce((object, { property, computed }) => ({ 447 | type: 'scalar_member_expression', 448 | object, 449 | property, 450 | computed 451 | }), head) 452 | } 453 | 454 | scalar_unary_expression 455 | = scalar_function_expression 456 | / scalar_member_expression 457 | / operator:unary_operator _ argument:scalar_unary_expression 458 | { 459 | return { 460 | type: 'scalar_unary_expression', 461 | operator, 462 | argument 463 | } 464 | } 465 | 466 | scalar_conditional_expression 467 | = test:(scalar_binary_or_expression) _ "?" _ 468 | consequent:(scalar_conditional_expression) _ ":" _ 469 | alternate:(scalar_conditional_expression) 470 | { 471 | return { 472 | type: 'scalar_conditional_expression', 473 | test, 474 | consequent, 475 | alternate 476 | } 477 | } 478 | / scalar_binary_or_expression 479 | 480 | scalar_binary_or_expression 481 | = head:(scalar_binary_and_expression) 482 | tail:(_ (or / "??") _ scalar_binary_and_expression)* 483 | { return buildBinaryExpression(head, tail) } 484 | 485 | scalar_binary_and_expression 486 | = head:(scalar_binary_equality_expression) 487 | tail:(_ and _ scalar_binary_equality_expression)* 488 | { return buildBinaryExpression(head, tail) } 489 | 490 | scalar_binary_equality_expression 491 | = head:(scalar_binary_relational_expression) 492 | tail:(_ ("=" / "!=" / "<>") _ scalar_binary_relational_expression)* 493 | { return buildBinaryExpression(head, tail) } 494 | 495 | scalar_binary_relational_expression 496 | = head:(scalar_in_expression) 497 | tail:(_ ("<=" / ">=" / "<" / ">") _ scalar_in_expression)* 498 | { return buildBinaryExpression(head, tail) } 499 | 500 | scalar_in_expression 501 | = value:scalar_between_expression _ in _ "(" _ list:scalar_expression_list _")" 502 | { 503 | return { 504 | type: 'scalar_in_expression', 505 | value, 506 | list 507 | } 508 | } 509 | / scalar_between_expression 510 | 511 | scalar_between_expression 512 | = value:scalar_binary_bitwise_or_expression _ between _ begin:scalar_binary_bitwise_or_expression _ and _ end:scalar_binary_bitwise_or_expression 513 | { 514 | return { 515 | type: 'scalar_between_expression', 516 | value, 517 | begin, 518 | end 519 | } 520 | } 521 | / scalar_binary_bitwise_or_expression 522 | 523 | scalar_binary_bitwise_or_expression 524 | = head:(scalar_binary_bitwise_xor_expression) 525 | tail:(_ "|" _ scalar_binary_bitwise_xor_expression)* 526 | { return buildBinaryExpression(head, tail) } 527 | 528 | scalar_binary_bitwise_xor_expression 529 | = head:(scalar_binary_bitwise_and_expression) 530 | tail:(_ "^" _ scalar_binary_bitwise_and_expression)* 531 | { return buildBinaryExpression(head, tail) } 532 | 533 | scalar_binary_bitwise_and_expression 534 | = head:(scalar_binary_shift_expression) 535 | tail:(_ "&" _ scalar_binary_shift_expression)* 536 | { return buildBinaryExpression(head, tail) } 537 | 538 | scalar_binary_shift_expression 539 | = head:(scalar_binary_additive_expression) 540 | tail:(_ ("<<" / ">>>" / ">>") _ scalar_binary_additive_expression)* 541 | { return buildBinaryExpression(head, tail) } 542 | 543 | scalar_binary_additive_expression 544 | = head:(scalar_binary_multiplicative_expression) 545 | tail:(_ ("+" / "-" / "||") _ scalar_binary_multiplicative_expression)* 546 | { return buildBinaryExpression(head, tail) } 547 | 548 | scalar_binary_multiplicative_expression 549 | = head:(scalar_unary_expression) 550 | tail:(_ ("*" / "/" / "%") _ scalar_unary_expression)* 551 | { return buildBinaryExpression(head, tail) } 552 | 553 | scalar_object_element_property 554 | = key:(identifier / string_constant) _ ":" _ value:scalar_expression 555 | { return { key, value } } 556 | 557 | object_constant_property 558 | = key:(identifier / string_constant) _ ":" _ value:constant 559 | { return { key, value } } 560 | 561 | collection_primary_expression 562 | = expression:identifier 563 | { 564 | return { 565 | type: 'collection_expression', 566 | expression 567 | } 568 | } 569 | 570 | collection_member_expression 571 | = head:collection_primary_expression 572 | tail:( 573 | _ "." _ property:identifier _ { return { property, computed: false } } 574 | / _ "[" _ property:(string_constant / array_index / parameter_name) _ "]" { return { property, computed: true } } 575 | )+ 576 | { 577 | return tail.reduce((object, { property, computed }) => ({ 578 | type: 'collection_member_expression', 579 | object, 580 | property, 581 | computed 582 | }), head) 583 | } 584 | 585 | collection_subquery_expression 586 | = expression:subquery 587 | { 588 | return { 589 | type: "collection_subquery_expression", 590 | expression 591 | } 592 | } 593 | 594 | top_specification 595 | = value:(unsigned_integer / parameter_name) 596 | { 597 | return { 598 | type: 'top_specification', 599 | value 600 | } 601 | } 602 | 603 | unsigned_integer 604 | = [0-9]+ 605 | { 606 | return { 607 | type: 'number_constant', 608 | value: Number(text()) 609 | } 610 | } 611 | 612 | scalar_expression_list 613 | = head:scalar_expression? tail:(_ "," _ v:scalar_expression { return v })* 614 | { return head ? [head, ...tail] : [] } 615 | 616 | subquery 617 | = "(" _ subquery:select_query _ ")" 618 | { return subquery } 619 | -------------------------------------------------------------------------------- /src/transformer.ts: -------------------------------------------------------------------------------- 1 | import traverse from "@babel/traverse"; 2 | import * as aggregateFunctions from "./aggregate-functions"; 3 | // @ts-ignore 4 | import { SyntaxError } from "./parser"; // eslint-disable-line import/no-unresolved 5 | 6 | type Context = { 7 | aggregation?: boolean; 8 | ast?: any; 9 | highCardinality?: boolean; 10 | document?: any; 11 | orderBy?: any[]; 12 | }; 13 | 14 | function transform(contexts: Context[], node: { [x: string]: any }) { 15 | // eslint-disable-next-line no-use-before-define 16 | const def = definitions[node.type]; 17 | if (!def) { 18 | throw new Error(`Invalid type: ${node.type}`); 19 | } 20 | 21 | return def(contexts, node); 22 | } 23 | 24 | function isAggregateFunction({ 25 | type, 26 | name, 27 | udf 28 | }: { 29 | type: string; 30 | name: any; 31 | udf: boolean; 32 | }) { 33 | return ( 34 | type === "scalar_function_expression" && 35 | Object.prototype.hasOwnProperty.call( 36 | aggregateFunctions, 37 | name.name.toUpperCase() 38 | ) && 39 | !udf 40 | ); 41 | } 42 | 43 | function strictTrueNode(node: any) { 44 | return { 45 | type: "BinaryExpression", 46 | left: node, 47 | operator: "===", 48 | right: { 49 | type: "BooleanLiteral", 50 | value: true 51 | } 52 | }; 53 | } 54 | 55 | function isNotUndefinedNode(argument: any) { 56 | return { 57 | type: "BinaryExpression", 58 | left: { 59 | type: "UnaryExpression", 60 | operator: "typeof", 61 | prefix: true, 62 | argument 63 | }, 64 | operator: "!==", 65 | right: { 66 | type: "StringLiteral", 67 | value: "undefined" 68 | } 69 | }; 70 | } 71 | 72 | function callHelperNode(name: string, ...args: any[]) { 73 | return { 74 | type: "CallExpression", 75 | callee: { 76 | type: "MemberExpression", 77 | object: { 78 | type: "Identifier", 79 | name: "$h" 80 | }, 81 | property: { 82 | type: "Identifier", 83 | name 84 | } 85 | }, 86 | arguments: args.filter(a => a) 87 | }; 88 | } 89 | 90 | function ridPathNode(ctx: Context) { 91 | return { 92 | type: "ArrowFunctionExpression", 93 | params: [ 94 | { 95 | type: "Identifier", 96 | name: "$" 97 | } 98 | ], 99 | body: { 100 | type: "MemberExpression", 101 | object: { 102 | type: "MemberExpression", 103 | object: { 104 | type: "Identifier", 105 | name: "$" 106 | }, 107 | property: { 108 | type: "Identifier", 109 | name: ctx.document.properties[0].key.name 110 | } 111 | }, 112 | property: { 113 | type: "Identifier", 114 | name: "_rid" 115 | } 116 | } 117 | }; 118 | } 119 | 120 | function clone(o: any) { 121 | return JSON.parse(JSON.stringify(o)); 122 | } 123 | 124 | function transformWithNewContext( 125 | contexts: Context[], 126 | node: { [x: string]: any } 127 | ) { 128 | const ctx = contexts[contexts.length - 1]; 129 | const nextCtx: Context = { 130 | document: ctx.document ? clone(ctx.document) : null 131 | }; 132 | contexts.push(nextCtx); 133 | const transformed = transform(contexts, node); 134 | contexts.pop(); 135 | return [transformed, nextCtx]; 136 | } 137 | 138 | function transformSortExpression(contexts: Context[], expression: any): any { 139 | if (expression.type === "scalar_member_expression") { 140 | const object = transformSortExpression(contexts, expression.object); 141 | let property = transform(contexts, expression.property); 142 | if (!expression.computed) { 143 | property = { 144 | type: "StringLiteral", 145 | value: property.name 146 | }; 147 | } 148 | 149 | return { 150 | type: "ArrayExpression", 151 | elements: [...object.elements, property] 152 | }; 153 | } 154 | 155 | const node = { 156 | type: "StringLiteral", 157 | value: expression.name 158 | }; 159 | return { 160 | type: "ArrayExpression", 161 | elements: [node] 162 | }; 163 | } 164 | 165 | const definitions: { [key: string]: Function } = { 166 | array_constant( 167 | contexts: Context[], 168 | { elements }: { elements: any[] } 169 | ): { type: string; elements: any[] } { 170 | return { 171 | type: "ArrayExpression", 172 | elements: elements.map(v => transform(contexts, v)) 173 | }; 174 | }, 175 | 176 | array_subquery_expression( 177 | contexts: Context[], 178 | { expression }: { expression: any } 179 | ) { 180 | return transformWithNewContext(contexts, expression)[0]; 181 | }, 182 | 183 | boolean_constant(contexts: Context[], { value }: { value: boolean }) { 184 | return { 185 | type: "BooleanLiteral", 186 | value 187 | }; 188 | }, 189 | 190 | collection_expression( 191 | contexts: Context[], 192 | { expression }: { expression: any } 193 | ): any { 194 | return transform(contexts, expression); 195 | }, 196 | 197 | collection_member_expression( 198 | contexts: Context[], 199 | { 200 | object, 201 | property, 202 | computed 203 | }: { object: any; property: any; computed: boolean } 204 | ) { 205 | return { 206 | type: "MemberExpression", 207 | object: transform(contexts, object), 208 | property: transform(contexts, property), 209 | computed 210 | }; 211 | }, 212 | 213 | collection_subquery_expression( 214 | contexts: Context[], 215 | { expression }: { expression: any } 216 | ) { 217 | return transformWithNewContext(contexts, expression)[0]; 218 | }, 219 | 220 | exists_subquery_expression( 221 | contexts: Context[], 222 | { expression }: { expression: any } 223 | ) { 224 | const rows = transformWithNewContext(contexts, expression)[0]; 225 | return callHelperNode("exists", rows); 226 | }, 227 | 228 | filter_condition(contexts: Context[], { condition }: { condition: any }) { 229 | const ctx = contexts[contexts.length - 1]; 230 | return { 231 | type: "CallExpression", 232 | callee: { 233 | type: "MemberExpression", 234 | object: ctx.ast, 235 | property: { 236 | type: "Identifier", 237 | name: "filter" 238 | } 239 | }, 240 | arguments: [ 241 | { 242 | type: "ArrowFunctionExpression", 243 | params: ctx.document ? [ctx.document] : [], 244 | body: strictTrueNode(transform(contexts, condition)) 245 | } 246 | ] 247 | }; 248 | }, 249 | 250 | from_source( 251 | contexts: Context[], 252 | { expression, alias }: { expression: any; alias?: any } 253 | ) { 254 | return transform(contexts, alias || expression); 255 | }, 256 | 257 | from_specification( 258 | contexts: Context[], 259 | { 260 | source, 261 | joins 262 | }: { 263 | source: { expression: any; alias?: any; iteration?: boolean }; 264 | joins?: { expression: any; alias?: any; iteration?: boolean }[]; 265 | } 266 | ) { 267 | const ctx = contexts[contexts.length - 1]; 268 | return [source, ...(joins || [])].reduce( 269 | (object, { expression, alias, iteration }, i) => { 270 | const isSubquery = expression.type === "collection_subquery_expression"; 271 | if (!ctx.highCardinality) { 272 | ctx.highCardinality = iteration || isSubquery; 273 | } 274 | 275 | const exp = transform(contexts, expression); 276 | const aliasNode = alias ? transform(contexts, alias) : null; 277 | const node = aliasNode || exp; 278 | const nameNode = !isSubquery 279 | ? node.property || node 280 | : aliasNode || { type: "Identifier", name: `$${i}` }; 281 | 282 | if (!ctx.document) { 283 | if (exp.type !== "MemberExpression") { 284 | ctx.document = exp; 285 | } else { 286 | traverse({ type: "Program", body: [exp] } as any, { 287 | MemberExpression(path: any) { 288 | const { object: o } = path.node; 289 | if (o.type === "Identifier") { 290 | ctx.document = o; 291 | path.stop(); 292 | } 293 | } 294 | }); 295 | } 296 | } 297 | 298 | const { document } = ctx; 299 | 300 | ctx.document = { 301 | type: "ObjectPattern", 302 | properties: [ 303 | ...(document.type === "ObjectPattern" ? document.properties : []), 304 | { 305 | type: "ObjectProperty", 306 | computed: false, 307 | shorthand: true, 308 | key: nameNode, 309 | value: nameNode 310 | } 311 | ] 312 | }; 313 | 314 | if (iteration || isSubquery) { 315 | const arrayValue = iteration && isSubquery; 316 | const mapProps = { 317 | type: "CallExpression", 318 | callee: { 319 | type: "MemberExpression", 320 | object: { 321 | type: "LogicalExpression", 322 | left: arrayValue 323 | ? { 324 | type: "MemberExpression", 325 | object: exp, 326 | property: { 327 | type: "NumericLiteral", 328 | value: 0 329 | } 330 | } 331 | : exp, 332 | operator: "||", 333 | right: { 334 | type: "ArrayExpression", 335 | elements: [] as any[] 336 | } 337 | }, 338 | property: { 339 | type: "Identifier", 340 | name: "map" 341 | } 342 | }, 343 | arguments: [ 344 | { 345 | type: "ArrowFunctionExpression", 346 | params: [nameNode], 347 | body: { 348 | type: "ObjectExpression", 349 | properties: [ 350 | ...(document.type === "ObjectPattern" 351 | ? document.properties 352 | : []), 353 | { 354 | type: "ObjectProperty", 355 | computed: false, 356 | key: nameNode, 357 | value: nameNode 358 | } 359 | ] 360 | } 361 | } 362 | ] 363 | }; 364 | if (!object) { 365 | // e.g. 366 | // FROM c 367 | // JOIN (SELECT VALUE t FROM t IN c.tags WHERE t.name = 'foo') 368 | // 369 | // collection 370 | // .reduce(($, c) => [...$, { c }], []) 371 | // .reduce(($, { c }) => [ 372 | // ...$, 373 | // ...(c.tag || []).map(t => ({ c, t })).filter(...).map(({ c, t }) => t) 374 | // ], []) 375 | return mapProps; 376 | } 377 | 378 | // e.g. 379 | // FROM Families f 380 | // JOIN c IN f.children 381 | // JOIN p IN c.pets 382 | // 383 | // collection 384 | // .reduce(($, Families) => [...$, { f: Families }], []) 385 | // .reduce(($, { f }) => [...$, ...(f.children || []).map(c => ({ c, f }))], []) 386 | // .reduce(($, { f, c }) => [...$, ...(c.pets || []).map(p => ({ c, f, p }))], []) 387 | return { 388 | type: "CallExpression", 389 | callee: { 390 | type: "MemberExpression", 391 | object, 392 | property: { 393 | type: "Identifier", 394 | name: "reduce" 395 | } 396 | }, 397 | arguments: [ 398 | { 399 | type: "ArrowFunctionExpression", 400 | params: [{ type: "Identifier", name: "$" }, document], 401 | body: { 402 | type: "ArrayExpression", 403 | elements: [ 404 | { 405 | type: "SpreadElement", 406 | argument: { 407 | type: "Identifier", 408 | name: "$" 409 | } 410 | }, 411 | { 412 | type: "SpreadElement", 413 | argument: mapProps 414 | } 415 | ] 416 | } 417 | }, 418 | { 419 | type: "ArrayExpression", 420 | elements: [] 421 | } 422 | ] 423 | }; 424 | } 425 | 426 | // e.g, 427 | // FROM Families.children[0] c 428 | // 429 | // collection 430 | // .reduce(($, Families) => [...$, { c: Families.children[0] }], []) 431 | // 432 | // FROM Families f 433 | // JOIN f.children 434 | // 435 | // collection 436 | // .reduce(($, Families) => [...$, { f: Families }], []) 437 | // .reduce(($, f) => (typeof f.children !== 'undefined' ? [...$, { f, children: f.children }] : $), []) 438 | return { 439 | type: "CallExpression", 440 | callee: { 441 | type: "MemberExpression", 442 | object, 443 | property: { 444 | type: "Identifier", 445 | name: "reduce" 446 | } 447 | }, 448 | arguments: [ 449 | { 450 | type: "ArrowFunctionExpression", 451 | params: [{ type: "Identifier", name: "$" }, document], 452 | body: { 453 | type: "ConditionalExpression", 454 | test: isNotUndefinedNode(exp), 455 | consequent: { 456 | type: "ArrayExpression", 457 | elements: [ 458 | { 459 | type: "SpreadElement", 460 | argument: { 461 | type: "Identifier", 462 | name: "$" 463 | } 464 | }, 465 | { 466 | type: "ObjectExpression", 467 | properties: [ 468 | ...(document.type === "ObjectPattern" 469 | ? document.properties 470 | : []), 471 | { 472 | type: "ObjectProperty", 473 | computed: false, 474 | key: nameNode, 475 | value: exp 476 | } 477 | ] 478 | } 479 | ] 480 | }, 481 | alternate: { 482 | type: "Identifier", 483 | name: "$" 484 | } 485 | } 486 | }, 487 | { 488 | type: "ArrayExpression", 489 | elements: [] 490 | } 491 | ] 492 | }; 493 | }, 494 | contexts.length === 1 ? ctx.ast : null 495 | ); 496 | }, 497 | 498 | identifier(contexts: Context[], { name }: { name: string }) { 499 | return { 500 | type: "Identifier", 501 | name 502 | }; 503 | }, 504 | 505 | null_constant() { 506 | return { type: "NullLiteral" }; 507 | }, 508 | 509 | number_constant(contexts: Context[], { value }: { value: number }) { 510 | return { 511 | type: "NumericLiteral", 512 | value 513 | }; 514 | }, 515 | 516 | object_constant( 517 | contexts: Context[], 518 | { properties }: { properties: { key: any; value: any }[] } 519 | ) { 520 | return { 521 | type: "ObjectExpression", 522 | properties: properties.map(({ key, value }) => ({ 523 | type: "ObjectProperty", 524 | key: transform(contexts, key), 525 | value: transform(contexts, value) 526 | })) 527 | }; 528 | }, 529 | 530 | object_property_list( 531 | contexts: Context[], 532 | { properties }: { properties: { property: any; alias: any }[] } 533 | ) { 534 | let n = 0; 535 | return { 536 | type: "ObjectExpression", 537 | properties: properties.map(({ property, alias }) => { 538 | let key; 539 | if (alias) { 540 | key = alias; 541 | } else if (property.type === "scalar_member_expression") { 542 | key = property.property; 543 | } else if (property.type === "identifier") { 544 | key = property; 545 | } 546 | return { 547 | type: "ObjectProperty", 548 | key: key 549 | ? transform(contexts, key) 550 | : // eslint-disable-next-line no-plusplus 551 | { type: "Identifier", name: `$${++n}` }, 552 | value: transform(contexts, property) 553 | }; 554 | }) 555 | }; 556 | }, 557 | 558 | parameter_name(contexts: Context[], { name }: { name: string }) { 559 | return { 560 | type: "MemberExpression", 561 | object: { 562 | type: "Identifier", 563 | name: "$p" 564 | }, 565 | property: { 566 | type: "Identifier", 567 | name: name.slice(1) 568 | } 569 | }; 570 | }, 571 | 572 | scalar_array_expression( 573 | contexts: Context[], 574 | { elements }: { elements: any[] } 575 | ) { 576 | return { 577 | type: "ArrayExpression", 578 | elements: elements.map(v => transform(contexts, v)) 579 | }; 580 | }, 581 | 582 | scalar_between_expression( 583 | contexts: Context[], 584 | { value, begin, end }: { value: any; begin: any; end: any } 585 | ) { 586 | const left = transform(contexts, value); 587 | return { 588 | type: "BinaryExpression", 589 | left: { 590 | type: "BinaryExpression", 591 | left, 592 | operator: ">=", 593 | right: transform(contexts, begin) 594 | }, 595 | operator: "&&", 596 | right: { 597 | type: "BinaryExpression", 598 | left, 599 | operator: "<=", 600 | right: transform(contexts, end) 601 | } 602 | }; 603 | }, 604 | 605 | scalar_binary_expression( 606 | contexts: Context[], 607 | { left, operator, right }: { left: any; operator: string; right: any } 608 | ) { 609 | const l = transform(contexts, left); 610 | const r = transform(contexts, right); 611 | 612 | if (operator === "??") { 613 | // `typeof left !== "undefined" ? left : right` 614 | return { 615 | type: "ConditionalExpression", 616 | test: isNotUndefinedNode(l), 617 | consequent: l, 618 | alternate: r 619 | }; 620 | } 621 | 622 | if (operator === "AND") { 623 | return callHelperNode("and", l, r); 624 | } 625 | if (operator === "OR") { 626 | return callHelperNode("or", l, r); 627 | } 628 | if (operator === "=") { 629 | return callHelperNode("equal", l, r); 630 | } 631 | if (operator === "!=" || operator === "<>") { 632 | return callHelperNode("notEqual", l, r); 633 | } 634 | if ( 635 | operator === ">" || 636 | operator === "<" || 637 | operator === ">=" || 638 | operator === "<=" 639 | ) { 640 | return callHelperNode( 641 | "compare", 642 | { 643 | type: "StringLiteral", 644 | value: operator 645 | }, 646 | l, 647 | r 648 | ); 649 | } 650 | if (operator === "||") { 651 | return callHelperNode("concat", l, r); 652 | } 653 | 654 | return callHelperNode( 655 | "calculate", 656 | { 657 | type: "StringLiteral", 658 | value: operator 659 | }, 660 | l, 661 | r 662 | ); 663 | }, 664 | 665 | scalar_conditional_expression( 666 | contexts: Context[], 667 | { 668 | test, 669 | consequent, 670 | alternate 671 | }: { test: any; consequent: any; alternate: any } 672 | ) { 673 | return { 674 | type: "ConditionalExpression", 675 | test: strictTrueNode(transform(contexts, test)), 676 | consequent: transform(contexts, consequent), 677 | alternate: transform(contexts, alternate) 678 | }; 679 | }, 680 | 681 | scalar_function_expression( 682 | contexts: Context[], 683 | { 684 | type, 685 | name, 686 | arguments: args, 687 | udf 688 | }: { type: string; name: any; arguments: any[]; udf: boolean } 689 | ) { 690 | const ctx = contexts[contexts.length - 1]; 691 | const aggregation = 692 | ctx.aggregation && isAggregateFunction({ type, name, udf }); 693 | 694 | const f = transform(contexts, name); 695 | if (!udf) f.name = f.name.toUpperCase(); 696 | 697 | return { 698 | type: "CallExpression", 699 | callee: { 700 | type: "MemberExpression", 701 | object: { 702 | type: "Identifier", 703 | // eslint-disable-next-line no-nested-ternary 704 | name: udf ? "udf" : aggregation ? "$a" : "$b" 705 | }, 706 | property: f 707 | }, 708 | arguments: aggregation 709 | ? args.map(a => ({ 710 | type: "CallExpression", 711 | callee: { 712 | type: "MemberExpression", 713 | object: { 714 | type: "Identifier", 715 | name: "$__" 716 | }, 717 | property: { 718 | type: "Identifier", 719 | name: "map" 720 | } 721 | }, 722 | arguments: [ 723 | { 724 | type: "ArrowFunctionExpression", 725 | params: ctx.document ? [ctx.document] : [], 726 | body: transform(contexts, a) 727 | } 728 | ] 729 | })) 730 | : args.map(a => transform(contexts, a)) 731 | }; 732 | }, 733 | 734 | scalar_in_expression( 735 | contexts: Context[], 736 | { value, list }: { value: any; list: any[] } 737 | ) { 738 | return { 739 | type: "CallExpression", 740 | callee: { 741 | type: "MemberExpression", 742 | object: { 743 | type: "ArrayExpression", 744 | elements: list.map(l => transform(contexts, l)) 745 | }, 746 | property: { 747 | type: "Identifier", 748 | name: "includes" 749 | } 750 | }, 751 | arguments: [transform(contexts, value)] 752 | }; 753 | }, 754 | 755 | scalar_member_expression( 756 | contexts: Context[], 757 | { 758 | object, 759 | property, 760 | computed 761 | }: { object: any; property: any; computed: boolean } 762 | ) { 763 | const objectNode = transform(contexts, object); 764 | 765 | // ``` 766 | // ($_ = object, typeof $_ === "object" && $_ ? $_[property] : undefined) 767 | // ``` 768 | return { 769 | type: "SequenceExpression", 770 | expressions: [ 771 | { 772 | type: "AssignmentExpression", 773 | left: { 774 | type: "Identifier", 775 | name: "$_" 776 | }, 777 | operator: "=", 778 | right: objectNode 779 | }, 780 | { 781 | type: "ConditionalExpression", 782 | test: { 783 | type: "LogicalExpression", 784 | left: { 785 | type: "BinaryExpression", 786 | left: { 787 | type: "UnaryExpression", 788 | operator: "typeof", 789 | prefix: true, 790 | argument: { 791 | type: "Identifier", 792 | name: "$_" 793 | } 794 | }, 795 | operator: "===", 796 | right: { 797 | type: "StringLiteral", 798 | value: "object" 799 | } 800 | }, 801 | operator: "&&", 802 | right: { 803 | type: "Identifier", 804 | name: "$_" 805 | } 806 | }, 807 | consequent: { 808 | type: "MemberExpression", 809 | object: { 810 | type: "Identifier", 811 | name: "$_" 812 | }, 813 | property: transform(contexts, property), 814 | computed 815 | }, 816 | alternate: { 817 | type: "Identifier", 818 | name: "undefined" 819 | } 820 | } 821 | ] 822 | }; 823 | }, 824 | 825 | scalar_object_expression( 826 | contexts: Context[], 827 | { properties }: { properties: any[] } 828 | ) { 829 | return { 830 | type: "ObjectExpression", 831 | properties: properties.map(({ key, value }) => ({ 832 | type: "ObjectProperty", 833 | key: transform(contexts, key), 834 | value: transform(contexts, value) 835 | })) 836 | }; 837 | }, 838 | 839 | scalar_subquery_expression( 840 | contexts: Context[], 841 | { expression }: { expression: any } 842 | ) { 843 | const [object, ctx] = transformWithNewContext(contexts, expression); 844 | 845 | if (!ctx.aggregation && ctx.highCardinality) { 846 | throw new SyntaxError( 847 | "The cardinality of a scalar subquery result set cannot be greater than one." 848 | ); 849 | } 850 | 851 | return { 852 | type: "MemberExpression", 853 | object, 854 | property: { 855 | type: "NumericLiteral", 856 | value: 0 857 | } 858 | }; 859 | }, 860 | 861 | scalar_unary_expression( 862 | contexts: Context[], 863 | { operator, argument }: { operator: string; argument: any } 864 | ) { 865 | const node = transform(contexts, argument); 866 | 867 | if (operator === "NOT") { 868 | return callHelperNode("not", node); 869 | } 870 | 871 | return callHelperNode( 872 | "calculateUnary", 873 | { 874 | type: "StringLiteral", 875 | value: operator 876 | }, 877 | node 878 | ); 879 | }, 880 | 881 | select_query( 882 | contexts: Context[], 883 | { 884 | top, 885 | select, 886 | from, 887 | where, 888 | orderBy 889 | }: { top: any; select: any; from: any; where: any; orderBy: any } 890 | ) { 891 | const ctx = contexts[contexts.length - 1]; 892 | 893 | if (from) { 894 | ctx.ast = { 895 | type: "Identifier", 896 | name: "$c" 897 | }; 898 | ctx.ast = transform(contexts, from); 899 | } else { 900 | ctx.ast = { 901 | type: "ArrayExpression", 902 | elements: [ 903 | contexts.length === 1 || !ctx.document 904 | ? { 905 | type: "NullLiteral" 906 | } 907 | : clone(ctx.document) 908 | ] 909 | }; 910 | } 911 | 912 | if (where) { 913 | ctx.ast = transform(contexts, where); 914 | } 915 | 916 | if (orderBy) { 917 | ctx.ast = transform(contexts, orderBy); 918 | } else if (contexts.length === 1 && ctx.document) { 919 | // try sort by `_rid` when `FROM` is specified 920 | ctx.ast = callHelperNode("sort", ctx.ast, ridPathNode(ctx)); 921 | } 922 | 923 | if (top) { 924 | ctx.ast = transform(contexts, top); 925 | } 926 | 927 | return transform(contexts, select); 928 | }, 929 | 930 | select_specification( 931 | contexts: Context[], 932 | { 933 | "*": all, 934 | properties, 935 | value 936 | }: { "*": boolean; properties?: { properties: any[] }; value: any } 937 | ) { 938 | const ctx = contexts[contexts.length - 1]; 939 | if (properties) { 940 | ctx.aggregation = properties.properties.some(({ property }) => 941 | isAggregateFunction(property) 942 | ); 943 | } else { 944 | ctx.aggregation = value ? isAggregateFunction(value) : false; 945 | } 946 | 947 | if (ctx.aggregation) { 948 | // ``` 949 | // ( 950 | // $__ = $c.filter(...), 951 | // [{ $1: COUNT($__.map(c => c.id)), $2: ... }] 952 | // ) 953 | // ``` 954 | return { 955 | type: "SequenceExpression", 956 | expressions: [ 957 | // cache filtered result to a variable 958 | { 959 | type: "AssignmentExpression", 960 | left: { 961 | type: "Identifier", 962 | name: "$__" 963 | }, 964 | operator: "=", 965 | right: ctx.ast 966 | }, 967 | { 968 | type: "ArrayExpression", 969 | elements: [transform(contexts, properties || value)] 970 | } 971 | ] 972 | }; 973 | } 974 | 975 | if (all) { 976 | if (!ctx.document) { 977 | throw new SyntaxError( 978 | "'SELECT *' is not valid if FROM clause is omitted." 979 | ); 980 | } 981 | if (ctx.document.properties.length > 1) { 982 | throw new SyntaxError( 983 | "'SELECT *' is only valid with a single input set." 984 | ); 985 | } 986 | } 987 | 988 | if (!ctx.document) { 989 | return { 990 | type: "CallExpression", 991 | callee: { 992 | type: "MemberExpression", 993 | object: ctx.ast, 994 | property: { 995 | type: "Identifier", 996 | name: "map" 997 | } 998 | }, 999 | arguments: [ 1000 | { 1001 | type: "ArrowFunctionExpression", 1002 | params: [] as any[], 1003 | body: transform(contexts, properties || value) 1004 | } 1005 | ] 1006 | }; 1007 | } 1008 | 1009 | const select = all 1010 | ? ctx.document.properties[0] 1011 | : transform(contexts, properties || value); 1012 | 1013 | return { 1014 | type: "CallExpression", 1015 | callee: { 1016 | type: "MemberExpression", 1017 | object: ctx.ast, 1018 | property: { 1019 | type: "Identifier", 1020 | name: "map" 1021 | } 1022 | }, 1023 | arguments: [ 1024 | { 1025 | type: "ArrowFunctionExpression", 1026 | params: [ctx.document], 1027 | body: 1028 | contexts.length === 1 1029 | ? { 1030 | type: "ArrayExpression", 1031 | elements: [select, ctx.document] 1032 | } 1033 | : select 1034 | } 1035 | ] 1036 | }; 1037 | }, 1038 | 1039 | sort_specification( 1040 | contexts: Context[], 1041 | { expressions }: { expressions: any[] } 1042 | ) { 1043 | const ctx = contexts[contexts.length - 1]; 1044 | ctx.orderBy = expressions.map(e => transform(contexts, e)); 1045 | 1046 | return callHelperNode( 1047 | "sort", 1048 | ctx.ast, 1049 | ridPathNode(ctx), 1050 | { type: "Identifier", name: "$compositeIndexes" }, 1051 | ...(ctx.orderBy || []) 1052 | ); 1053 | }, 1054 | 1055 | sort_expression( 1056 | contexts: Context[], 1057 | { expression, order }: { expression: any; order: string } 1058 | ) { 1059 | if (expression.type !== "scalar_member_expression") { 1060 | throw new SyntaxError( 1061 | "Unsupported ORDER BY clause. ORDER BY item expression could not be mapped to a document path." 1062 | ); 1063 | } 1064 | 1065 | const keys = transformSortExpression(contexts, expression); 1066 | 1067 | return { 1068 | type: "ArrayExpression", 1069 | elements: [ 1070 | keys, 1071 | { 1072 | type: "BooleanLiteral", 1073 | value: order === "DESC" 1074 | } 1075 | ] 1076 | }; 1077 | }, 1078 | 1079 | sql(contexts: Context[], { body }: { body: any }) { 1080 | const ctx: Context = {}; 1081 | contexts.push(ctx); 1082 | const node = transform(contexts, body); 1083 | 1084 | let returnNode; 1085 | if (!ctx.document || ctx.aggregation) { 1086 | returnNode = { 1087 | type: "ObjectExpression", 1088 | properties: [ 1089 | { 1090 | type: "ObjectProperty", 1091 | computed: false, 1092 | shorthand: false, 1093 | kind: "init", 1094 | key: { 1095 | type: "Identifier", 1096 | name: "result" 1097 | }, 1098 | value: callHelperNode("stripUndefined", node) 1099 | }, 1100 | { 1101 | type: "ObjectProperty", 1102 | computed: false, 1103 | shorthand: false, 1104 | kind: "init", 1105 | key: { 1106 | type: "Identifier", 1107 | name: "continuation" 1108 | }, 1109 | value: { type: "NullLiteral" } 1110 | } 1111 | ] 1112 | }; 1113 | } else { 1114 | returnNode = callHelperNode( 1115 | "paginate", 1116 | node, 1117 | { type: "Identifier", name: "$maxItemCount" }, 1118 | { type: "Identifier", name: "$continuation" }, 1119 | ridPathNode(ctx), 1120 | ...(ctx.orderBy || []) 1121 | ); 1122 | } 1123 | 1124 | return { 1125 | type: "ArrowFunctionExpression", 1126 | params: [ 1127 | // aggregate functions 1128 | { 1129 | type: "Identifier", 1130 | name: "$a" 1131 | }, 1132 | // built-in functions 1133 | { 1134 | type: "Identifier", 1135 | name: "$b" 1136 | }, 1137 | // document array (collection) 1138 | { 1139 | type: "Identifier", 1140 | name: "$c" 1141 | }, 1142 | // helper functions 1143 | { 1144 | type: "Identifier", 1145 | name: "$h" 1146 | }, 1147 | // udf 1148 | { 1149 | type: "Identifier", 1150 | name: "udf" 1151 | }, 1152 | // parameters 1153 | { 1154 | type: "Identifier", 1155 | name: "$p" 1156 | }, 1157 | { 1158 | type: "Identifier", 1159 | name: "$maxItemCount" 1160 | }, 1161 | { 1162 | type: "Identifier", 1163 | name: "$continuation" 1164 | }, 1165 | { 1166 | type: "Identifier", 1167 | name: "$compositeIndexes" 1168 | }, 1169 | // temporal cache 1170 | { 1171 | type: "Identifier", 1172 | name: "$_" 1173 | }, 1174 | // intermediate cache 1175 | { 1176 | type: "Identifier", 1177 | name: "$__" 1178 | } 1179 | ], 1180 | body: { 1181 | type: "BlockStatement", 1182 | body: [ 1183 | { 1184 | type: "ReturnStatement", 1185 | argument: returnNode 1186 | } 1187 | ] 1188 | } 1189 | }; 1190 | }, 1191 | 1192 | string_constant(contexts: Context[], { value }: { value: string }) { 1193 | return { 1194 | type: "StringLiteral", 1195 | value 1196 | }; 1197 | }, 1198 | 1199 | top_specification(contexts: Context[], { value }: { value: any }) { 1200 | const ctx = contexts[contexts.length - 1]; 1201 | return { 1202 | type: "CallExpression", 1203 | callee: { 1204 | type: "MemberExpression", 1205 | object: ctx.ast, 1206 | property: { 1207 | type: "Identifier", 1208 | name: "slice" 1209 | } 1210 | }, 1211 | arguments: [ 1212 | { 1213 | type: "NumericLiteral", 1214 | value: 0 1215 | }, 1216 | transform(contexts, value) 1217 | ] 1218 | }; 1219 | }, 1220 | 1221 | undefined_constant() { 1222 | return { 1223 | type: "Identifier", 1224 | name: "undefined" 1225 | }; 1226 | } 1227 | }; 1228 | 1229 | export default (sqlAst: { [x: string]: any }) => transform([], sqlAst); 1230 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type CompositeIndex = { 2 | path: string; 3 | order: "ascending" | "descending"; 4 | }; 5 | -------------------------------------------------------------------------------- /test/aggregate-functions.ts: -------------------------------------------------------------------------------- 1 | // test examples on https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sql-query-reference#bk_built_in_functions 2 | 3 | import testQuery from "./utils/test-query"; 4 | 5 | const fixtures = [ 6 | { key: null }, 7 | { key: false }, 8 | { key: true }, 9 | { key: "abc" }, 10 | { key: "cdfg" }, 11 | { key: "opqrs" }, 12 | { key: "ttttttt" }, 13 | { key: "xyz" }, 14 | { key: "oo" }, 15 | { key: "ppp" }, 16 | { 17 | key: "uniquePartitionKey", 18 | resourceId: "0", 19 | field: "1" 20 | }, 21 | { 22 | key: "uniquePartitionKey", 23 | resourceId: "1", 24 | field: "2" 25 | }, 26 | { key: 1 }, 27 | { key: 2 }, 28 | { key: 3 }, 29 | { key: 4 }, 30 | { key: 5 }, 31 | { key: 6 }, 32 | { key: 7 }, 33 | { key: 8 }, 34 | { key: 9 } 35 | ]; 36 | 37 | export const max = testQuery( 38 | fixtures, 39 | { query: "SELECT VALUE MAX(r.key) FROM r" }, 40 | ["xyz"] 41 | ); 42 | 43 | export const maxBooleanAndNull = testQuery( 44 | [{ key: null }, { key: false }], 45 | { query: "SELECT VALUE MAX(r.key) FROM r" }, 46 | [false] 47 | ); 48 | 49 | export const maxBooleans = testQuery( 50 | [{ key: false }, { key: true }], 51 | { query: "SELECT VALUE MAX(r.key) FROM r" }, 52 | [true] 53 | ); 54 | 55 | export const maxNumbersAndBoolean = testQuery( 56 | [{ key: 0 }, { key: 1 }, { key: true }], 57 | { query: "SELECT VALUE MAX(r.key) FROM r" }, 58 | [1] 59 | ); 60 | 61 | export const min = testQuery( 62 | fixtures, 63 | { query: "SELECT VALUE MIN(r.key) FROM r" }, 64 | [null] 65 | ); 66 | 67 | export const minBooleans = testQuery( 68 | [{ key: false }, { key: true }], 69 | { query: "SELECT VALUE MIN(r.key) FROM r" }, 70 | [false] 71 | ); 72 | 73 | export const minNumbers = testQuery( 74 | [{ key: 1 }, { key: 2 }], 75 | { query: "SELECT VALUE MIN(r.key) FROM r" }, 76 | [1] 77 | ); 78 | 79 | export const minStringAndNumber = testQuery( 80 | [{ key: "hi" }, { key: 123 }], 81 | { query: "SELECT VALUE MIN(r.key) FROM r" }, 82 | [123] 83 | ); 84 | 85 | export const sum = testQuery( 86 | [{ key: 1 }, { key: 2 }, { key: 3 }], 87 | { query: "SELECT VALUE SUM(r.key) FROM r" }, 88 | [6] 89 | ); 90 | 91 | export const sumNonNumber = testQuery( 92 | [{ key: 1 }, { key: 2 }, { key: "3" }], 93 | { query: "SELECT VALUE SUM(r.key) FROM r" }, 94 | [] 95 | ); 96 | -------------------------------------------------------------------------------- /test/builtin-functions.ts: -------------------------------------------------------------------------------- 1 | // test examples on https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sql-query-reference#bk_built_in_functions 2 | /* eslint-disable camelcase */ 3 | 4 | import testQuery from "./utils/test-query"; 5 | 6 | const test = (query: string, expected: any) => 7 | testQuery(null, { query }, expected); 8 | 9 | export const ABS = test("SELECT ABS(-1), ABS(0), ABS(1)", [ 10 | { $1: 1, $2: 0, $3: 1 } 11 | ]); 12 | 13 | export const ACOS = test("SELECT ACOS(-1)", [{ $1: 3.1415926535897931 }]); 14 | 15 | export const ASIN = test("SELECT ASIN(-1)", [{ $1: -1.5707963267948966 }]); 16 | 17 | export const ATAN = test("SELECT ATAN(-45.01)", [{ $1: -1.5485826962062663 }]); 18 | 19 | export const ATN2 = test("SELECT ATN2(35.175643, 129.44)", [ 20 | { $1: 1.3054517947300646 } 21 | ]); 22 | 23 | export const CEILING = test( 24 | ` 25 | SELECT CEILING(123.45), CEILING(-123.45), CEILING(0.0) 26 | `, 27 | [{ $1: 124, $2: -123, $3: 0 }] 28 | ); 29 | 30 | export const COS = test("SELECT COS(14.78)", [{ $1: -0.59946542619465426 }]); 31 | 32 | export const COT = test("SELECT COT(124.1332)", [ 33 | { $1: -0.040311998371148884 } 34 | ]); 35 | 36 | export const DEGREES = test("SELECT DEGREES(PI()/2)", [{ $1: 90 }]); 37 | 38 | export const EXP1 = test("SELECT EXP(10)", [{ $1: 22026.465794806718 }]); 39 | 40 | export const EXP2 = test("SELECT EXP(LOG(20)), LOG(EXP(20))", [ 41 | { $1: 19.999999999999996, $2: 20 } 42 | ]); 43 | 44 | export const FLOOR = test( 45 | ` 46 | SELECT FLOOR(123.45), FLOOR(-123.45), FLOOR(0.0) 47 | `, 48 | [{ $1: 123, $2: -124, $3: 0 }] 49 | ); 50 | 51 | export const LOG_1 = test("SELECT LOG(10)", [{ $1: 2.3025850929940459 }]); 52 | 53 | export const LOG_2 = test("SELECT EXP(LOG(10))", [{ $1: 10.000000000000002 }]); 54 | 55 | export const LOG10 = test("SELECT LOG10(100)", [{ $1: 2 }]); 56 | 57 | export const PI = test("SELECT PI()", [{ $1: 3.1415926535897931 }]); 58 | 59 | export const POWER = test("SELECT POWER(2, 3), POWER(2.5, 3)", [ 60 | { $1: 8, $2: 15.625 } 61 | ]); 62 | 63 | export const RADIANS = test( 64 | "SELECT RADIANS(-45.01), RADIANS(-181.01), RADIANS(0), RADIANS(0.1472738), RADIANS(197.1099392)", 65 | [ 66 | { 67 | $1: -0.7855726963226477, 68 | $2: -3.1592204790349356, 69 | $3: 0, 70 | $4: 0.0025704127119236249, 71 | $5: 3.4402174274458375 72 | } 73 | ] 74 | ); 75 | 76 | export const ROUND = test( 77 | ` 78 | SELECT ROUND(2.4), ROUND(2.6), ROUND(2.5), ROUND(-2.4), ROUND(-2.6) 79 | `, 80 | [{ $1: 2, $2: 3, $3: 3, $4: -2, $5: -3 }] 81 | ); 82 | 83 | export const SIGN = test( 84 | "SELECT SIGN(-2), SIGN(-1), SIGN(0), SIGN(1), SIGN(2)", 85 | [{ $1: -1, $2: -1, $3: 0, $4: 1, $5: 1 }] 86 | ); 87 | 88 | export const SIN = test("SELECT SIN(45.175643)", [{ $1: 0.929607286611012 }]); 89 | 90 | export const SQRT = test("SELECT SQRT(1), SQRT(2.0), SQRT(3)", [ 91 | { $1: 1, $2: 1.4142135623730952, $3: 1.7320508075688772 } 92 | ]); 93 | 94 | export const SQUARE = test("SELECT SQUARE(1), SQUARE(2.0), SQUARE(3)", [ 95 | { $1: 1, $2: 4, $3: 9 } 96 | ]); 97 | 98 | export const TAN = test("SELECT TAN(PI()/2)", [{ $1: 16331239353195370 }]); 99 | 100 | export const TRUNC = test( 101 | "SELECT TRUNC(2.4), TRUNC(2.6), TRUNC(2.5), TRUNC(-2.4), TRUNC(-2.6)", 102 | [{ $1: 2, $2: 2, $3: 2, $4: -2, $5: -2 }] 103 | ); 104 | 105 | export const IS_ARRAY = test( 106 | ` 107 | SELECT 108 | IS_ARRAY(true), 109 | IS_ARRAY(1), 110 | IS_ARRAY("value"), 111 | IS_ARRAY(null), 112 | IS_ARRAY({prop: "value"}), 113 | IS_ARRAY([1, 2, 3]), 114 | IS_ARRAY({prop: "value"}.prop2) 115 | `, 116 | [ 117 | { 118 | $1: false, 119 | $2: false, 120 | $3: false, 121 | $4: false, 122 | $5: false, 123 | $6: true, 124 | $7: false 125 | } 126 | ] 127 | ); 128 | 129 | export const IS_BOOL = test( 130 | ` 131 | SELECT 132 | IS_BOOL(true), 133 | IS_BOOL(1), 134 | IS_BOOL("value"), 135 | IS_BOOL(null), 136 | IS_BOOL({prop: "value"}), 137 | IS_BOOL([1, 2, 3]), 138 | IS_BOOL({prop: "value"}.prop2) 139 | `, 140 | [ 141 | { 142 | $1: true, 143 | $2: false, 144 | $3: false, 145 | $4: false, 146 | $5: false, 147 | $6: false, 148 | $7: false 149 | } 150 | ] 151 | ); 152 | 153 | export const IS_DEFINED = test( 154 | ` 155 | SELECT IS_DEFINED({ "a" : 5 }.a), IS_DEFINED({ "a" : 5 }.b) 156 | `, 157 | [ 158 | { 159 | $1: true, 160 | $2: false 161 | } 162 | ] 163 | ); 164 | 165 | export const IS_NULL = test( 166 | ` 167 | SELECT 168 | IS_NULL(true), 169 | IS_NULL(1), 170 | IS_NULL("value"), 171 | IS_NULL(null), 172 | IS_NULL({prop: "value"}), 173 | IS_NULL([1, 2, 3]), 174 | IS_NULL({prop: "value"}.prop2) 175 | `, 176 | [ 177 | { 178 | $1: false, 179 | $2: false, 180 | $3: false, 181 | $4: true, 182 | $5: false, 183 | $6: false, 184 | $7: false 185 | } 186 | ] 187 | ); 188 | 189 | export const IS_NUMBER = test( 190 | ` 191 | SELECT 192 | IS_NUMBER(true), 193 | IS_NUMBER(1), 194 | IS_NUMBER("value"), 195 | IS_NUMBER(null), 196 | IS_NUMBER({prop: "value"}), 197 | IS_NUMBER([1, 2, 3]), 198 | IS_NUMBER({prop: "value"}.prop2) 199 | `, 200 | [ 201 | { 202 | $1: false, 203 | $2: true, 204 | $3: false, 205 | $4: false, 206 | $5: false, 207 | $6: false, 208 | $7: false 209 | } 210 | ] 211 | ); 212 | 213 | export const IS_OBJECT = test( 214 | ` 215 | SELECT 216 | IS_OBJECT(true), 217 | IS_OBJECT(1), 218 | IS_OBJECT("value"), 219 | IS_OBJECT(null), 220 | IS_OBJECT({prop: "value"}), 221 | IS_OBJECT([1, 2, 3]), 222 | IS_OBJECT({prop: "value"}.prop2) 223 | `, 224 | [ 225 | { 226 | $1: false, 227 | $2: false, 228 | $3: false, 229 | $4: false, 230 | $5: true, 231 | $6: false, 232 | $7: false 233 | } 234 | ] 235 | ); 236 | 237 | export const IS_PRIMITIVE = test( 238 | ` 239 | SELECT 240 | IS_PRIMITIVE(true), 241 | IS_PRIMITIVE(1), 242 | IS_PRIMITIVE("value"), 243 | IS_PRIMITIVE(null), 244 | IS_PRIMITIVE({prop: "value"}), 245 | IS_PRIMITIVE([1, 2, 3]), 246 | IS_PRIMITIVE({prop: "value"}.prop2) 247 | `, 248 | [{ $1: true, $2: true, $3: true, $4: true, $5: false, $6: false, $7: false }] 249 | ); 250 | 251 | export const IS_STRING = test( 252 | ` 253 | SELECT 254 | IS_STRING(true), 255 | IS_STRING(1), 256 | IS_STRING("value"), 257 | IS_STRING(null), 258 | IS_STRING({prop: "value"}), 259 | IS_STRING([1, 2, 3]), 260 | IS_STRING({prop: "value"}.prop2) 261 | `, 262 | [ 263 | { 264 | $1: false, 265 | $2: false, 266 | $3: true, 267 | $4: false, 268 | $5: false, 269 | $6: false, 270 | $7: false 271 | } 272 | ] 273 | ); 274 | 275 | export const CONCAT = test( 276 | ` 277 | SELECT CONCAT("abc", "def") 278 | `, 279 | [{ $1: "abcdef" }] 280 | ); 281 | 282 | export const CONTAINS = test( 283 | ` 284 | SELECT CONTAINS("abc", "ab"), CONTAINS("abc", "d") 285 | `, 286 | [{ $1: true, $2: false }] 287 | ); 288 | 289 | export const ENDSWITH = test( 290 | 'SELECT ENDSWITH("abc", "b"), ENDSWITH("abc", "bc")', 291 | [{ $1: false, $2: true }] 292 | ); 293 | 294 | export const INDEX_OF = test( 295 | ` 296 | SELECT INDEX_OF("abc", "ab"), INDEX_OF("abc", "b"), INDEX_OF("abc", "d") 297 | `, 298 | [{ $1: 0, $2: 1, $3: -1 }] 299 | ); 300 | 301 | export const LEFT = test('SELECT LEFT("abc", 1), LEFT("abc", 2)', [ 302 | { $1: "a", $2: "ab" } 303 | ]); 304 | 305 | export const LENGTH = test( 306 | ` 307 | SELECT LENGTH("abc") 308 | `, 309 | [{ $1: 3 }] 310 | ); 311 | 312 | export const LOWER = test( 313 | ` 314 | SELECT LOWER("Abc") 315 | `, 316 | [{ $1: "abc" }] 317 | ); 318 | 319 | export const LTRIM = test( 320 | 'SELECT LTRIM(" abc"), LTRIM("abc"), LTRIM("abc ")', 321 | [{ $1: "abc", $2: "abc", $3: "abc " }] 322 | ); 323 | 324 | export const REPLACE = test( 325 | 'SELECT REPLACE("This is a Test", "Test", "desk")', 326 | [{ $1: "This is a desk" }] 327 | ); 328 | 329 | export const REPLICATE = test('SELECT REPLICATE("a", 3)', [{ $1: "aaa" }]); 330 | 331 | export const REVERSE = test( 332 | ` 333 | SELECT REVERSE("Abc") 334 | `, 335 | [{ $1: "cbA" }] 336 | ); 337 | 338 | export const RIGHT = test('SELECT RIGHT("abc", 1), RIGHT("abc", 2)', [ 339 | { $1: "c", $2: "bc" } 340 | ]); 341 | 342 | export const RTRIM = test( 343 | 'SELECT RTRIM(" abc"), RTRIM("abc"), RTRIM("abc ")', 344 | [{ $1: " abc", $2: "abc", $3: "abc" }] 345 | ); 346 | 347 | export const STARTSWITH = test( 348 | ` 349 | SELECT STARTSWITH("abc", "b"), STARTSWITH("abc", "a") 350 | `, 351 | [{ $1: false, $2: true }] 352 | ); 353 | 354 | export const SUBSTRING = test( 355 | ` 356 | SELECT SUBSTRING("abc", 1, 1) 357 | `, 358 | [{ $1: "b" }] 359 | ); 360 | 361 | export const ToString1 = test( 362 | ` 363 | SELECT ToString(1.0000), ToString("Hello World"), ToString(NaN), ToString(Infinity), 364 | ToString(IS_STRING(ToString(undefined))), IS_STRING(ToString(0.1234)), ToString(false), ToString(undefined) 365 | `, 366 | [ 367 | { 368 | $1: "1", 369 | $2: "Hello World", 370 | $3: "NaN", 371 | $4: "Infinity", 372 | $5: "false", 373 | $6: true, 374 | $7: "false" 375 | } 376 | ] 377 | ); 378 | 379 | export const ToString2 = testQuery( 380 | [ 381 | { 382 | Products: [ 383 | { ProductID: 1, Weight: 4, WeightUnits: "lb" }, 384 | { ProductID: 2, Weight: 32, WeightUnits: "kg" }, 385 | { ProductID: 3, Weight: 400, WeightUnits: "g" }, 386 | { ProductID: 4, Weight: 8999, WeightUnits: "mg" } 387 | ] 388 | } 389 | ], 390 | { 391 | query: ` 392 | SELECT 393 | CONCAT(ToString(p.Weight), p.WeightUnits) 394 | FROM p in c.Products 395 | ` 396 | }, 397 | [{ $1: "4lb" }, { $1: "32kg" }, { $1: "400g" }, { $1: "8999mg" }] 398 | ); 399 | 400 | export const ToString3 = testQuery( 401 | [ 402 | { 403 | id: "08259", 404 | description: "Cereals ready-to-eat, KELLOGG, KELLOGG'S CRISPIX", 405 | nutrients: [ 406 | { id: "305", description: "Caffeine", units: "mg" }, 407 | { 408 | id: "306", 409 | description: "Cholesterol, HDL", 410 | nutritionValue: 30, 411 | units: "mg" 412 | }, 413 | { 414 | id: "307", 415 | description: "Sodium, NA", 416 | nutritionValue: 612, 417 | units: "mg" 418 | }, 419 | { 420 | id: "308", 421 | description: "Protein, ABP", 422 | nutritionValue: 60, 423 | units: "mg" 424 | }, 425 | { 426 | id: "309", 427 | description: "Zinc, ZN", 428 | nutritionValue: null, 429 | units: "mg" 430 | } 431 | ] 432 | } 433 | ], 434 | { 435 | query: ` 436 | SELECT 437 | n.id AS nutrientID, 438 | REPLACE(ToString(n.nutritionValue), "6", "9") AS nutritionVal 439 | FROM food 440 | JOIN n IN food.nutrients 441 | ` 442 | }, 443 | [ 444 | { nutrientID: "305" }, 445 | { nutrientID: "306", nutritionVal: "30" }, 446 | { nutrientID: "307", nutritionVal: "912" }, 447 | { nutrientID: "308", nutritionVal: "90" }, 448 | { nutrientID: "309", nutritionVal: "null" } 449 | ] 450 | ); 451 | 452 | export const TRIM = test( 453 | ` 454 | SELECT TRIM(" abc"), TRIM(" abc "), TRIM("abc "), TRIM("abc") 455 | `, 456 | [{ $1: "abc", $2: "abc", $3: "abc", $4: "abc" }] 457 | ); 458 | 459 | export const UPPER = test( 460 | ` 461 | SELECT UPPER("Abc") 462 | `, 463 | [{ $1: "ABC" }] 464 | ); 465 | 466 | export const ARRAY_CONCAT = test( 467 | ` 468 | SELECT ARRAY_CONCAT(["apples", "strawberries"], ["bananas"]) 469 | `, 470 | [{ $1: ["apples", "strawberries", "bananas"] }] 471 | ); 472 | 473 | export const ARRAY_CONTAINS1 = test( 474 | ` 475 | SELECT 476 | ARRAY_CONTAINS(["apples", "strawberries", "bananas"], "apples"), 477 | ARRAY_CONTAINS(["apples", "strawberries", "bananas"], "mangoes") 478 | `, 479 | [{ $1: true, $2: false }] 480 | ); 481 | 482 | export const ARRAY_CONTAINS2 = test( 483 | ` 484 | SELECT 485 | ARRAY_CONTAINS([{"name": "apples", "fresh": true}, {"name": "strawberries", "fresh": true}], {"name": "apples"}, true), 486 | ARRAY_CONTAINS([{"name": "apples", "fresh": true}, {"name": "strawberries", "fresh": true}], {"name": "apples"}), 487 | ARRAY_CONTAINS([{"name": "apples", "fresh": true}, {"name": "strawberries", "fresh": true}], {"name": "mangoes"}, true) 488 | `, 489 | [ 490 | { 491 | $1: true, 492 | $2: false, 493 | $3: false 494 | } 495 | ] 496 | ); 497 | 498 | export const ARRAY_LENGTH = test( 499 | ` 500 | SELECT ARRAY_LENGTH(["apples", "strawberries", "bananas"]) 501 | `, 502 | [{ $1: 3 }] 503 | ); 504 | 505 | export const ARRAY_SLICE = test( 506 | ` 507 | SELECT 508 | ARRAY_SLICE(["apples", "strawberries", "bananas"], 1), 509 | ARRAY_SLICE(["apples", "strawberries", "bananas"], 1, 1) 510 | `, 511 | [ 512 | { 513 | $1: ["strawberries", "bananas"], 514 | $2: ["strawberries"] 515 | } 516 | ] 517 | ); 518 | 519 | // Spatial functions 520 | 521 | const geometries = { 522 | points: { 523 | a: { 524 | type: "Point", 525 | coordinates: [0.5, 0.5] 526 | }, 527 | b: { 528 | type: "Point", 529 | coordinates: [1, 0] 530 | }, 531 | c: { 532 | type: "Point", 533 | coordinates: [1.5, -0.5] 534 | }, 535 | d: { 536 | type: "Point", 537 | coordinates: [0, 0] 538 | } 539 | }, 540 | polygons: { 541 | a: { 542 | type: "Polygon", 543 | coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]] 544 | }, 545 | b: { 546 | type: "Polygon", 547 | coordinates: [ 548 | [[0.5, -0.5], [0.5, 0.5], [1.5, 0.5], [1.5, -0.5], [0.5, -0.5]] 549 | ] 550 | }, 551 | c: { 552 | type: "Polygon", 553 | coordinates: [[[1, -1], [1, 0], [2, 0], [2, -1], [1, -1]]] 554 | }, 555 | d: { 556 | type: "Polygon", 557 | coordinates: [ 558 | [[1.5, -1.5], [1.5, -0.5], [2.5, -0.5], [2.5, -1.5], [1.5, -1.5]] 559 | ] 560 | }, 561 | e: { 562 | type: "Polygon", 563 | coordinates: [ 564 | [[0.25, 0.25], [0.25, 0.75], [0.75, 0.75], [0.75, 0.25], [0.25, 0.25]] 565 | ] 566 | }, 567 | f: { 568 | type: "Polygon", 569 | coordinates: [[[0.25, 0], [0.25, 0.5], [0.75, 0.5], [0.75, 0], [0.25, 0]]] 570 | } 571 | } 572 | }; 573 | 574 | export const ST_DISTANCE = testQuery( 575 | null, 576 | { 577 | query: ` 578 | SELECT 579 | ROUND(ST_DISTANCE(@pa, @pb)), 580 | ST_DISTANCE(@pa, @pa) 581 | `, 582 | parameters: [ 583 | { 584 | name: "@pa", 585 | value: geometries.points.a 586 | }, 587 | { 588 | name: "@pb", 589 | value: geometries.points.b 590 | }, 591 | { 592 | name: "@a", 593 | value: geometries.polygons.a 594 | }, 595 | { 596 | name: "@d", 597 | value: geometries.polygons.d 598 | } 599 | ] 600 | }, 601 | [ 602 | { 603 | $1: 78626, 604 | $2: 0.0 605 | } 606 | ] 607 | ); 608 | 609 | export const ST_DISTANCE_query = testQuery( 610 | [ 611 | { id: "a", geometry: geometries.points.a }, 612 | { id: "b", geometry: geometries.points.b }, 613 | { id: "c", geometry: geometries.points.c }, 614 | { id: "d", geometry: geometries.points.d } 615 | ], 616 | { 617 | query: ` 618 | SELECT items.id 619 | FROM items 620 | WHERE ST_DISTANCE(items.geometry, @geom) <= 80000 621 | `, 622 | parameters: [ 623 | { 624 | name: "@geom", 625 | value: geometries.polygons.b 626 | } 627 | ] 628 | }, 629 | [{ id: "a" }, { id: "b" }, { id: "c" }] 630 | ); 631 | 632 | export const ST_WITHIN = testQuery( 633 | null, 634 | { 635 | query: ` 636 | SELECT 637 | ST_WITHIN(@pa, @a), 638 | ST_WITHIN(@pa, @b), 639 | ST_WITHIN(@pd, @a) 640 | `, 641 | parameters: [ 642 | { 643 | name: "@a", 644 | value: geometries.polygons.a 645 | }, 646 | { 647 | name: "@b", 648 | value: geometries.polygons.b 649 | }, 650 | { 651 | name: "@pa", 652 | value: geometries.points.a 653 | }, 654 | { 655 | name: "@pd", 656 | value: geometries.points.d 657 | } 658 | ] 659 | }, 660 | [ 661 | { 662 | $1: true, 663 | $2: false, 664 | $3: false 665 | } 666 | ] 667 | ); 668 | 669 | export const ST_WITHIN_query = testQuery( 670 | [ 671 | { id: "a", geometry: geometries.points.a }, 672 | { id: "b", geometry: geometries.points.b }, 673 | { id: "c", geometry: geometries.points.c }, 674 | { id: "d", geometry: geometries.points.d } 675 | ], 676 | { 677 | query: ` 678 | SELECT items.id 679 | FROM items 680 | WHERE ST_WITHIN(items.geometry, @geom) 681 | `, 682 | parameters: [ 683 | { 684 | name: "@geom", 685 | value: geometries.polygons.a 686 | } 687 | ] 688 | }, 689 | [{ id: "a" }] 690 | ); 691 | 692 | export const ST_INTERSECTS = testQuery( 693 | null, 694 | { 695 | query: ` 696 | SELECT 697 | ST_INTERSECTS(@a, @b), 698 | ST_INTERSECTS(@a, @c), 699 | ST_INTERSECTS(@a, @d) 700 | `, 701 | parameters: [ 702 | { 703 | name: "@a", 704 | value: JSON.stringify(geometries.polygons.a) 705 | }, 706 | { 707 | name: "@b", 708 | value: JSON.stringify(geometries.polygons.b) 709 | }, 710 | { 711 | name: "@c", 712 | value: JSON.stringify(geometries.polygons.c) 713 | }, 714 | { 715 | name: "@d", 716 | value: JSON.stringify(geometries.polygons.d) 717 | } 718 | ] 719 | }, 720 | [ 721 | { 722 | $1: true, 723 | $2: true, 724 | $3: false 725 | } 726 | ] 727 | ); 728 | 729 | export const ST_INTERSECTS_query = testQuery( 730 | [ 731 | { id: "a", geometry: geometries.points.a }, 732 | { id: "b", geometry: geometries.points.b }, 733 | { id: "c", geometry: geometries.points.c }, 734 | { id: "d", geometry: geometries.points.d } 735 | ], 736 | { 737 | query: ` 738 | SELECT items.id 739 | FROM items 740 | WHERE ST_INTERSECTS(items.geometry, @geom) 741 | `, 742 | parameters: [ 743 | { 744 | name: "@geom", 745 | value: geometries.polygons.a 746 | } 747 | ] 748 | }, 749 | [{ id: "a" }, { id: "b" }, { id: "d" }] 750 | ); 751 | 752 | export const REGEXMATCH1 = test( 753 | ` 754 | SELECT RegexMatch ("abcd", "ABC", "") AS NoModifiers, 755 | RegexMatch ("abcd", "ABC", "i") AS CaseInsensitive, 756 | RegexMatch ("abcd", "ab.", "") AS WildcardCharacter, 757 | RegexMatch ("abcd", "ab c", "x") AS IgnoreWhiteSpace, 758 | RegexMatch ("abcd", "aB c", "ix") AS CaseInsensitiveAndIgnoreWhiteSpace 759 | `, 760 | [ 761 | { 762 | NoModifiers: false, 763 | CaseInsensitive: true, 764 | WildcardCharacter: true, 765 | IgnoreWhiteSpace: true, 766 | CaseInsensitiveAndIgnoreWhiteSpace: true 767 | } 768 | ] 769 | ); 770 | 771 | export const REGEXMATCH2 = testQuery( 772 | [ 773 | { description: "salt" }, 774 | { description: "sal" }, 775 | { description: "definitely salt" } 776 | ], 777 | { 778 | query: ` 779 | SELECT * 780 | FROM c 781 | WHERE RegexMatch (c.description, "salt{1}","") 782 | ` 783 | }, 784 | [{ description: "salt" }, { description: "definitely salt" }] 785 | ); 786 | 787 | export const REGEXMATCH3 = testQuery( 788 | [{ description: "42" }, { description: "asdf" }, { description: "hi 0" }], 789 | { 790 | query: ` 791 | SELECT * 792 | FROM c 793 | WHERE RegexMatch (c.description, "[0-9]","") 794 | ` 795 | }, 796 | [{ description: "42" }, { description: "hi 0" }] 797 | ); 798 | 799 | export const REGEXMATCH4 = testQuery( 800 | [ 801 | { description: " salt " }, 802 | { description: " Salt " }, 803 | { description: "salt " }, 804 | { description: " stla " } 805 | ], 806 | { 807 | query: ` 808 | SELECT * 809 | FROM c 810 | WHERE RegexMatch (c.description, " s... ","i") 811 | ` 812 | }, 813 | [ 814 | { description: " salt " }, 815 | { description: " Salt " }, 816 | { description: " stla " } 817 | ] 818 | ); 819 | -------------------------------------------------------------------------------- /test/errors.ts: -------------------------------------------------------------------------------- 1 | import testQuery from "./utils/test-query"; 2 | import { SyntaxError } from "../lib"; 3 | 4 | export const functionWrongNumberOfArgument = testQuery( 5 | null, 6 | { 7 | query: "select ABS()" 8 | }, 9 | new SyntaxError("The ABS function requires 1 argument(s)") 10 | ); 11 | 12 | export const reserved = testQuery( 13 | [], 14 | { 15 | query: "select c.value from c" 16 | }, 17 | SyntaxError 18 | ); 19 | 20 | export const asteriskIsOnlyValidWithSingleInputSet = testQuery( 21 | [], 22 | { 23 | query: "select * from c join d in c.children" 24 | }, 25 | new SyntaxError("'SELECT *' is only valid with a single input set.") 26 | ); 27 | 28 | export const asteriskIsNotValidIfFromClauseIsOmitted = testQuery( 29 | [], 30 | { 31 | query: "select *" 32 | }, 33 | new SyntaxError("'SELECT *' is not valid if FROM clause is omitted.") 34 | ); 35 | 36 | export const cardinalityOfScalarSubqueryResultSetCannotBeGreaterThenOne = testQuery( 37 | [], 38 | { 39 | query: "select (select l from l in c.list) from c" 40 | }, 41 | new SyntaxError( 42 | "The cardinality of a scalar subquery result set cannot be greater than one." 43 | ) 44 | ); 45 | 46 | export const multipleOrderByWithoutCompositeIndexes = testQuery( 47 | [], 48 | { 49 | query: "SELECT c.id FROM c ORDER BY c.a, c.b DESC", 50 | compositeIndexes: [] 51 | }, 52 | new Error( 53 | "The order by query does not have a corresponding composite index that it can be served from." 54 | ) 55 | ); 56 | 57 | export const multipleOrderByWithoutCorrespondingCompositeIndexes1 = testQuery( 58 | [], 59 | { 60 | query: "SELECT c.id FROM c ORDER BY c.a, c.b DESC", 61 | compositeIndexes: [ 62 | [{ path: "/a", order: "ascending" }, { path: "/b", order: "ascending" }] 63 | ] 64 | }, 65 | new Error( 66 | "The order by query does not have a corresponding composite index that it can be served from." 67 | ) 68 | ); 69 | 70 | export const multipleOrderByWithoutCorrespondingCompositeIndexes2 = testQuery( 71 | [], 72 | { 73 | query: "SELECT c.id FROM c ORDER BY c.a, c.b DESC", 74 | compositeIndexes: [ 75 | [{ path: "/b", order: "descending" }, { path: "/a", order: "ascending" }] 76 | ] 77 | }, 78 | new Error( 79 | "The order by query does not have a corresponding composite index that it can be served from." 80 | ) 81 | ); 82 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // test examples on https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sql-query 2 | 3 | import testQuery from "./utils/test-query"; 4 | 5 | const collection = [ 6 | { 7 | id: "AndersenFamily", 8 | lastName: "Andersen", 9 | parents: [{ firstName: "Thomas" }, { firstName: "Mary Kay" }], 10 | children: [ 11 | { 12 | firstName: "Henriette Thaulow", 13 | gender: "female", 14 | grade: 5, 15 | pets: [{ givenName: "Fluffy" }] 16 | } 17 | ], 18 | address: { state: "WA", county: "King", city: "seattle" }, 19 | creationDate: 1431620472, 20 | isRegistered: true 21 | }, 22 | 23 | { 24 | id: "WakefieldFamily", 25 | parents: [ 26 | { familyName: "Wakefield", givenName: "Robin" }, 27 | { familyName: "Miller", givenName: "Ben" } 28 | ], 29 | children: [ 30 | { 31 | familyName: "Merriam", 32 | givenName: "Jesse", 33 | gender: "female", 34 | grade: 1, 35 | pets: [{ givenName: "Goofy" }, { givenName: "Shadow" }] 36 | }, 37 | { 38 | familyName: "Miller", 39 | givenName: "Lisa", 40 | gender: "female", 41 | grade: 8 42 | } 43 | ], 44 | address: { state: "NY", county: "Manhattan", city: "NY" }, 45 | creationDate: 1431620462, 46 | isRegistered: false 47 | } 48 | ]; 49 | 50 | export const all = testQuery( 51 | collection, 52 | { query: "SELECT * FROM c" }, 53 | collection 54 | ); 55 | 56 | export const query1 = testQuery( 57 | collection, 58 | { query: 'SELECT * FROM Families f WHERE f.id = "AndersenFamily"' }, 59 | collection.filter(({ id }) => id === "AndersenFamily") 60 | ); 61 | 62 | export const query2 = testQuery( 63 | collection, 64 | { 65 | query: ` 66 | SELECT {"Name":f.id, "City":f.address.city} AS Family 67 | FROM Families f 68 | WHERE f.address.city = f.address.state 69 | ` 70 | }, 71 | [ 72 | { 73 | Family: { 74 | Name: "WakefieldFamily", 75 | City: "NY" 76 | } 77 | } 78 | ] 79 | ); 80 | 81 | export const query3 = testQuery( 82 | collection, 83 | { 84 | query: ` 85 | SELECT c.givenName 86 | FROM Families f 87 | JOIN c IN f.children 88 | WHERE f.id = 'WakefieldFamily' 89 | ORDER BY f.address.city ASC 90 | ` 91 | }, 92 | [{ givenName: "Jesse" }, { givenName: "Lisa" }] 93 | ); 94 | 95 | export const select = testQuery( 96 | collection, 97 | { query: 'SELECT f.address FROM Families f WHERE f.id = "AndersenFamily"' }, 98 | collection 99 | .filter(f => f.id === "AndersenFamily") 100 | .map(({ address }) => ({ address })) 101 | ); 102 | 103 | export const nestedProperties = testQuery( 104 | collection, 105 | { 106 | query: 107 | 'SELECT f.address.state, f.address.city FROM Families f WHERE f.id = "AndersenFamily"' 108 | }, 109 | collection 110 | .filter(f => f.id === "AndersenFamily") 111 | .map(({ address: { state, city } }) => ({ state, city })) 112 | ); 113 | 114 | export const JSONExpressions = testQuery( 115 | collection, 116 | { 117 | query: ` 118 | SELECT { "state": f.address.state, "city": f.address.city, "name": f.id } 119 | FROM Families f 120 | WHERE f.id = "AndersenFamily" 121 | ` 122 | }, 123 | collection 124 | .filter(f => f.id === "AndersenFamily") 125 | .map(f => ({ 126 | $1: { state: f.address.state, city: f.address.city, name: f.id } 127 | })) 128 | ); 129 | 130 | export const implicitArgumentVariables = testQuery( 131 | collection, 132 | { 133 | query: ` 134 | SELECT { "state": f.address.state, "city": f.address.city }, 135 | { "name": f.id } 136 | FROM Families f 137 | WHERE f.id = "AndersenFamily" 138 | ` 139 | }, 140 | collection 141 | .filter(f => f.id === "AndersenFamily") 142 | .map(f => ({ 143 | $1: { state: f.address.state, city: f.address.city }, 144 | $2: { name: f.id } 145 | })) 146 | ); 147 | 148 | export const subdocuments = testQuery( 149 | collection, 150 | { query: "SELECT * FROM Families.children" }, 151 | collection.map(Families => Families.children) 152 | ); 153 | 154 | export const subdocumentsExcluded = testQuery( 155 | collection, 156 | { query: "SELECT * FROM Families.address.state" }, 157 | collection.map(Families => Families.address.state).filter(v => v != null) 158 | ); 159 | 160 | export const binaryOperator1 = testQuery( 161 | collection, 162 | { 163 | query: ` 164 | SELECT * 165 | FROM Families.children[0] c 166 | WHERE c.grade % 2 = 1 -- matching grades == 5, 1 167 | ` 168 | }, 169 | collection 170 | .map(Families => Families.children[0]) 171 | .filter(c => c.grade % 2 === 1) 172 | ); 173 | 174 | export const binaryOperator2 = testQuery( 175 | collection, 176 | { 177 | query: ` 178 | SELECT * 179 | FROM Families.children[0] c 180 | WHERE c.grade ^ 4 = 1 -- matching grades == 5 181 | ` 182 | }, 183 | collection 184 | .map(Families => Families.children[0]) 185 | // eslint-disable-next-line no-bitwise 186 | .filter(c => (c.grade ^ 4) === 1) 187 | ); 188 | 189 | export const binaryOperator3 = testQuery( 190 | collection, 191 | { 192 | query: ` 193 | SELECT * 194 | FROM Families.children[0] c 195 | WHERE c.grade >= 5 -- matching grades == 5 196 | ` 197 | }, 198 | collection.map(Families => Families.children[0]).filter(c => c.grade >= 5) 199 | ); 200 | 201 | export const unaryOperator1 = testQuery( 202 | collection, 203 | { 204 | query: ` 205 | SELECT * 206 | FROM Families.children[0] c 207 | WHERE NOT(c.grade = 5) -- matching grades == 1 208 | ` 209 | }, 210 | collection.map(Families => Families.children[0]).filter(c => !(c.grade === 5)) 211 | ); 212 | 213 | export const unaryOperator2 = testQuery( 214 | collection, 215 | { 216 | query: ` 217 | SELECT * 218 | FROM Families.children[0] c 219 | WHERE (-c.grade = -5) -- matching grades == 5 220 | ` 221 | }, 222 | collection.map(Families => Families.children[0]).filter(c => -c.grade === -5) 223 | ); 224 | 225 | export const betweenKeyword1 = testQuery( 226 | collection, 227 | { 228 | query: ` 229 | SELECT * 230 | FROM Families.children[0] c 231 | WHERE c.grade BETWEEN 1 AND 5 232 | ` 233 | }, 234 | collection 235 | .map(Families => Families.children[0]) 236 | .filter(c => c.grade >= 1 && c.grade <= 5) 237 | ); 238 | 239 | export const betweenKeyword2 = testQuery( 240 | collection, 241 | { 242 | query: ` 243 | SELECT (c.grade BETWEEN 0 AND 10) 244 | FROM Families.children[0] c 245 | ` 246 | }, 247 | collection 248 | .map(Families => Families.children[0]) 249 | .map(c => ({ $1: c.grade >= 0 && c.grade <= 10 })) 250 | ); 251 | 252 | export const inKeyword1 = testQuery( 253 | collection, 254 | { 255 | query: ` 256 | SELECT * 257 | FROM Families 258 | WHERE Families.id IN ('AndersenFamily', 'WakefieldFamily') 259 | ` 260 | }, 261 | collection.filter(Families => 262 | ["AndersenFamily", "WakefieldFamily"].includes(Families.id) 263 | ) 264 | ); 265 | 266 | export const inKeyword2 = testQuery( 267 | collection, 268 | { 269 | query: ` 270 | SELECT * 271 | FROM Families 272 | WHERE Families.address.state IN ("NY", "WA", "CA", "PA", "OH", "OR", "MI", "WI", "MN", "FL") 273 | ` 274 | }, 275 | collection.filter(Families => 276 | ["NY", "WA", "CA", "PA", "OH", "OR", "MI", "WI", "MN", "FL"].includes( 277 | Families.address.state 278 | ) 279 | ) 280 | ); 281 | 282 | export const ternaryOperator1 = testQuery( 283 | collection, 284 | { 285 | query: ` 286 | SELECT (c.grade < 5)? "elementary": "other" AS gradeLevel 287 | FROM Families.children[0] c 288 | ` 289 | }, 290 | collection 291 | .map(Families => Families.children[0]) 292 | .map(c => ({ gradeLevel: c.grade < 5 ? "elementary" : "other" })) 293 | ); 294 | 295 | export const ternaryOperator2 = testQuery( 296 | collection, 297 | { 298 | query: ` 299 | SELECT (c.grade < 5)? "elementary": ((c.grade < 9)? "junior": "high") AS gradeLevel 300 | FROM Families.children[0] c 301 | ` 302 | }, 303 | collection 304 | .map(Families => Families.children[0]) 305 | .map(c => ({ 306 | // eslint-disable-next-line no-nested-ternary 307 | gradeLevel: c.grade < 5 ? "elementary" : c.grade < 9 ? "junior" : "high" 308 | })) 309 | ); 310 | 311 | export const coalesceOperator = testQuery( 312 | collection, 313 | { 314 | query: ` 315 | SELECT f.lastName ?? f.surname AS familyName 316 | FROM Families f 317 | ` 318 | }, 319 | [{ familyName: "Andersen" }, {}] 320 | ); 321 | 322 | export const quotedPropertyAccessor = testQuery( 323 | collection, 324 | { 325 | query: ` 326 | SELECT f["lastName"] 327 | FROM Families f 328 | WHERE f["id"] = "AndersenFamily" 329 | ` 330 | }, 331 | collection 332 | .filter(f => f.id === "AndersenFamily") 333 | .map(({ lastName }: { lastName?: string }) => ({ lastName })) 334 | ); 335 | 336 | export const aliasing = testQuery( 337 | collection, 338 | { 339 | query: ` 340 | SELECT 341 | { "state": f.address.state, "city": f.address.city } AS AddressInfo, 342 | { "name": f.id } NameInfo 343 | FROM Families f 344 | WHERE f.id = "AndersenFamily" 345 | ` 346 | }, 347 | collection 348 | .filter(f => f.id === "AndersenFamily") 349 | .map(f => ({ 350 | AddressInfo: { state: f.address.state, city: f.address.city }, 351 | NameInfo: { name: f.id } 352 | })) 353 | ); 354 | 355 | export const scalarExpressions1 = testQuery( 356 | null, 357 | { query: 'SELECT "Hello World"' }, 358 | [{ $1: "Hello World" }] 359 | ); 360 | 361 | export const scalarExpressions2 = testQuery( 362 | null, 363 | { query: "SELECT ((2 + 11 % 7)-2)/3" }, 364 | [{ $1: (2 + (11 % 7) - 2) / 3 }] 365 | ); 366 | 367 | export const scalarExpressions3 = testQuery( 368 | collection, 369 | { 370 | query: ` 371 | SELECT f.address.city = f.address.state AS AreFromSameCityState 372 | FROM Families f 373 | ` 374 | }, 375 | collection.map(f => ({ 376 | AreFromSameCityState: f.address.city === f.address.state 377 | })) 378 | ); 379 | 380 | export const objectAndArrayCreation = testQuery( 381 | collection, 382 | { 383 | query: ` 384 | SELECT [f.address.city, f.address.state] AS CityState 385 | FROM Families f 386 | ` 387 | }, 388 | collection.map(f => ({ CityState: [f.address.city, f.address.state] })) 389 | ); 390 | 391 | export const valueKeyword1 = testQuery( 392 | null, 393 | { 394 | query: 'SELECT VALUE "Hello World"' 395 | }, 396 | ["Hello World"] 397 | ); 398 | 399 | export const valueKeyword2 = testQuery( 400 | collection, 401 | { 402 | query: ` 403 | SELECT VALUE f.address 404 | FROM Families f 405 | ` 406 | }, 407 | collection.map(f => f.address) 408 | ); 409 | 410 | export const valueKeyword3 = testQuery( 411 | collection, 412 | { 413 | query: ` 414 | SELECT VALUE f.address.state 415 | FROM Families f 416 | ` 417 | }, 418 | collection.map(f => f.address.state) 419 | ); 420 | 421 | export const asteriskOperator = testQuery( 422 | collection, 423 | { 424 | query: ` 425 | SELECT * 426 | FROM Families f 427 | WHERE f.id = "AndersenFamily" 428 | ` 429 | }, 430 | collection.filter(f => f.id === "AndersenFamily") 431 | ); 432 | 433 | export const topOperator = testQuery( 434 | collection, 435 | { 436 | query: ` 437 | SELECT TOP 1 * 438 | FROM Families f 439 | ` 440 | }, 441 | collection.slice(0, 1) 442 | ); 443 | 444 | export const aggregateFunctions1 = testQuery( 445 | collection, 446 | { 447 | query: ` 448 | SELECT COUNT(1) 449 | FROM Families f 450 | ` 451 | }, 452 | [{ $1: 2 }] 453 | ); 454 | 455 | export const aggregateFunctions2 = testQuery( 456 | collection, 457 | { 458 | query: ` 459 | SELECT VALUE COUNT(1) 460 | FROM Families f 461 | ` 462 | }, 463 | [2] 464 | ); 465 | 466 | export const aggregateFunctions3 = testQuery( 467 | collection, 468 | { 469 | query: ` 470 | SELECT VALUE COUNT(1) 471 | FROM Families f 472 | WHERE f.address.state = "WA" 473 | ` 474 | }, 475 | [1] 476 | ); 477 | 478 | export const orderByClause1 = testQuery( 479 | collection, 480 | { 481 | query: ` 482 | SELECT f.id, f.address.city 483 | FROM Families f 484 | ORDER BY f.address.city 485 | ` 486 | }, 487 | [ 488 | { 489 | id: "WakefieldFamily", 490 | city: "NY" 491 | }, 492 | { 493 | id: "AndersenFamily", 494 | city: "seattle" 495 | } 496 | ] 497 | ); 498 | 499 | export const orderByClause2 = testQuery( 500 | collection, 501 | { 502 | query: ` 503 | SELECT f.id, f.creationDate 504 | FROM Families f 505 | ORDER BY f.creationDate DESC 506 | ` 507 | }, 508 | [ 509 | { 510 | id: "AndersenFamily", 511 | creationDate: 1431620472 512 | }, 513 | { 514 | id: "WakefieldFamily", 515 | creationDate: 1431620462 516 | } 517 | ] 518 | ); 519 | 520 | export const orderByClause3 = testQuery( 521 | collection, 522 | { 523 | query: ` 524 | SELECT f.id, f.creationDate 525 | FROM Families f 526 | ORDER BY f.address.city ASC, f.creationDate DESC 527 | ` 528 | }, 529 | [ 530 | { 531 | id: "WakefieldFamily", 532 | creationDate: 1431620462 533 | }, 534 | { 535 | id: "AndersenFamily", 536 | creationDate: 1431620472 537 | } 538 | ] 539 | ); 540 | 541 | export const iteration1 = testQuery( 542 | collection, 543 | { query: "SELECT * FROM c IN Families.children" }, 544 | collection.reduce((_, Families) => [..._, ...Families.children], []) 545 | ); 546 | 547 | export const iteration2 = testQuery( 548 | collection, 549 | { query: "SELECT c.givenName FROM c IN Families.children WHERE c.grade = 8" }, 550 | [ 551 | { 552 | givenName: "Lisa" 553 | } 554 | ] 555 | ); 556 | 557 | export const iteration3 = testQuery( 558 | collection, 559 | { 560 | query: ` 561 | SELECT COUNT(child) 562 | FROM child IN Families.children 563 | ` 564 | }, 565 | [ 566 | { 567 | $1: 3 568 | } 569 | ] 570 | ); 571 | 572 | export const join1 = testQuery( 573 | collection, 574 | { 575 | query: ` 576 | SELECT f.id 577 | FROM Families f 578 | JOIN f.NonExistent 579 | ` 580 | }, 581 | [] 582 | ); 583 | 584 | export const join2 = testQuery( 585 | collection, 586 | { 587 | query: ` 588 | SELECT f.id 589 | FROM Families f 590 | JOIN f.children 591 | ` 592 | }, 593 | [ 594 | { 595 | id: "AndersenFamily" 596 | }, 597 | { 598 | id: "WakefieldFamily" 599 | } 600 | ] 601 | ); 602 | 603 | export const join3 = testQuery( 604 | collection, 605 | { 606 | query: ` 607 | SELECT f.id 608 | FROM Families f 609 | JOIN c IN f.children 610 | ` 611 | }, 612 | [ 613 | { 614 | id: "AndersenFamily" 615 | }, 616 | { 617 | id: "WakefieldFamily" 618 | }, 619 | { 620 | id: "WakefieldFamily" 621 | } 622 | ] 623 | ); 624 | 625 | export const join4 = testQuery( 626 | collection, 627 | { 628 | query: ` 629 | SELECT 630 | f.id AS familyName, 631 | c.givenName AS childGivenName, 632 | c.firstName AS childFirstName, 633 | p.givenName AS petName 634 | FROM Families f 635 | JOIN c IN f.children 636 | JOIN p IN c.pets 637 | ` 638 | }, 639 | [ 640 | { 641 | familyName: "AndersenFamily", 642 | childFirstName: "Henriette Thaulow", 643 | petName: "Fluffy" 644 | }, 645 | { 646 | familyName: "WakefieldFamily", 647 | childGivenName: "Jesse", 648 | petName: "Goofy" 649 | }, 650 | { 651 | familyName: "WakefieldFamily", 652 | childGivenName: "Jesse", 653 | petName: "Shadow" 654 | } 655 | ] 656 | ); 657 | 658 | export const join5 = testQuery( 659 | collection, 660 | { 661 | query: ` 662 | SELECT 663 | f.id AS familyName, 664 | c.givenName AS childGivenName, 665 | c.firstName AS childFirstName, 666 | p.givenName AS petName 667 | FROM Families f 668 | JOIN c IN f.children 669 | JOIN p IN c.pets 670 | WHERE p.givenName = "Shadow" 671 | ` 672 | }, 673 | [ 674 | { 675 | familyName: "WakefieldFamily", 676 | childGivenName: "Jesse", 677 | petName: "Shadow" 678 | } 679 | ] 680 | ); 681 | 682 | const REGEX_MATCH = function REGEX_MATCH(input: string, pattern: string) { 683 | return input.match(pattern) !== null; 684 | }; 685 | 686 | export const udf1 = testQuery( 687 | collection, 688 | { 689 | query: ` 690 | SELECT udf.REGEX_MATCH(Families.address.city, ".*eattle") 691 | FROM Families 692 | `, 693 | udf: { REGEX_MATCH } 694 | }, 695 | [ 696 | { 697 | $1: true 698 | }, 699 | { 700 | $1: false 701 | } 702 | ] 703 | ); 704 | 705 | export const udf2 = testQuery( 706 | collection, 707 | { 708 | query: ` 709 | SELECT Families.id, Families.address.city 710 | FROM Families 711 | WHERE udf.REGEX_MATCH(Families.address.city, ".*eattle") 712 | `, 713 | udf: { REGEX_MATCH } 714 | }, 715 | [ 716 | { 717 | id: "AndersenFamily", 718 | city: "seattle" 719 | } 720 | ] 721 | ); 722 | 723 | export const udf3 = testQuery( 724 | collection, 725 | { 726 | query: ` 727 | SELECT f.address.city, udf.SEALEVEL(f.address.city) AS seaLevel 728 | FROM Families f 729 | `, 730 | udf: { 731 | SEALEVEL(city: string) { 732 | switch (city) { 733 | case "seattle": 734 | return 520; 735 | case "NY": 736 | return 410; 737 | case "Chicago": 738 | return 673; 739 | default: 740 | return -1; 741 | } 742 | } 743 | } 744 | }, 745 | [ 746 | { 747 | city: "seattle", 748 | seaLevel: 520 749 | }, 750 | { 751 | city: "NY", 752 | seaLevel: 410 753 | } 754 | ] 755 | ); 756 | 757 | export const parameterized = testQuery( 758 | collection, 759 | { 760 | query: 761 | "SELECT * FROM Families f WHERE f.lastName = @lastName AND f.address.state = @addressState", 762 | parameters: [ 763 | { name: "@lastName", value: "Wakefield" }, 764 | { name: "@addressState", value: "NY" } 765 | ] 766 | }, 767 | collection.filter(f => f.lastName === "Wakefield" && f.address.state === "NY") 768 | ); 769 | 770 | export const builtInMathematicalFunction = testQuery( 771 | null, 772 | { query: "SELECT VALUE ABS(-4)" }, 773 | [4] 774 | ); 775 | 776 | export const builtInTypeCheckingFunction = testQuery( 777 | null, 778 | { query: "SELECT VALUE IS_NUMBER(-4)" }, 779 | [true] 780 | ); 781 | 782 | export const builtInStringFunction1 = testQuery( 783 | collection, 784 | { 785 | query: ` 786 | SELECT VALUE UPPER(Families.id) 787 | FROM Families 788 | ` 789 | }, 790 | collection.map(Families => Families.id.toUpperCase()) 791 | ); 792 | 793 | export const builtInStringFunction2 = testQuery( 794 | collection, 795 | { 796 | query: ` 797 | SELECT Families.id, CONCAT(Families.address.city, ",", Families.address.state) AS location 798 | FROM Families 799 | ` 800 | }, 801 | collection.map(Families => ({ 802 | id: Families.id, 803 | location: `${Families.address.city},${Families.address.state}` 804 | })) 805 | ); 806 | 807 | export const builtInStringFunction3 = testQuery( 808 | collection, 809 | { 810 | query: ` 811 | SELECT Families.id, Families.address.city 812 | FROM Families 813 | WHERE STARTSWITH(Families.id, "Wakefield") 814 | ` 815 | }, 816 | collection 817 | .filter(Families => Families.id.startsWith("Wakefield")) 818 | .map(Families => ({ id: Families.id, city: Families.address.city })) 819 | ); 820 | 821 | export const builtInArrayFunction1 = testQuery( 822 | collection, 823 | { 824 | query: ` 825 | SELECT Families.id 826 | FROM Families 827 | WHERE ARRAY_CONTAINS(Families.parents, { givenName: "Robin", familyName: "Wakefield" }) 828 | ` 829 | }, 830 | collection 831 | .filter(Families => 832 | Families.parents.some( 833 | (v: { [x: string]: any }) => 834 | v.givenName === "Robin" && v.familyName === "Wakefield" 835 | ) 836 | ) 837 | .map(Families => ({ id: Families.id })) 838 | ); 839 | 840 | export const builtInArrayFunction2 = testQuery( 841 | collection, 842 | { 843 | query: ` 844 | SELECT Families.id, ARRAY_LENGTH(Families.children) AS numberOfChildren 845 | FROM Families 846 | ` 847 | }, 848 | collection.map(Families => ({ 849 | id: Families.id, 850 | numberOfChildren: Families.children.length 851 | })) 852 | ); 853 | -------------------------------------------------------------------------------- /test/misc.ts: -------------------------------------------------------------------------------- 1 | import testQuery from "./utils/test-query"; 2 | 3 | export const root = testQuery( 4 | [{ id: "sample database" }], 5 | { 6 | query: "select * from root r where r.id=@id", 7 | parameters: [{ name: "@id", value: "sample database" }] 8 | }, 9 | [{ id: "sample database" }] 10 | ); 11 | 12 | export const compareGreaterAndLess = testQuery( 13 | [{ id: "a", deletedAt: 10 }, { id: "b", deletedAt: 20 }], 14 | { 15 | query: "select * from c WHERE c.deletedAt < @a AND c.deletedAt > @b", 16 | parameters: [{ name: "@a", value: 15 }, { name: "@b", value: 5 }] 17 | }, 18 | [{ id: "a", deletedAt: 10 }] 19 | ); 20 | 21 | export const topMoreThan10 = testQuery( 22 | [{ id: "b" }, { id: "c" }, { id: "a" }], 23 | { 24 | query: "select top 123 * from c order by c.id" 25 | }, 26 | [{ id: "a" }, { id: "b" }, { id: "c" }] 27 | ); 28 | 29 | export const parameterizedTop = testQuery( 30 | [{ id: "b" }, { id: "c" }, { id: "a" }], 31 | { 32 | query: "select top @top * from c order by c.id", 33 | parameters: [{ name: "@top", value: 1 }] 34 | }, 35 | [{ id: "a" }] 36 | ); 37 | 38 | export const conditionStrictTrue1 = testQuery( 39 | [{ id: "hi" }], 40 | { 41 | query: "select * from c where true" 42 | }, 43 | [{ id: "hi" }] 44 | ); 45 | 46 | export const conditionStrictTrue2 = testQuery( 47 | [{ id: "hi" }], 48 | { 49 | query: "select * from c where 1 OR true" 50 | }, 51 | [{ id: "hi" }] 52 | ); 53 | 54 | export const conditionNotStrictTrue1 = testQuery( 55 | [{ id: "hi" }], 56 | { 57 | query: "select * from c where 1" 58 | }, 59 | [] 60 | ); 61 | 62 | export const conditionNotStrictTrue2 = testQuery( 63 | [{ id: "hi" }], 64 | { 65 | query: "select * from c where 1 and true" 66 | }, 67 | [] 68 | ); 69 | 70 | export const conditionNotStrictTrue3 = testQuery( 71 | [{ id: "hi" }], 72 | { 73 | query: "select * from c where 1 OR 'ok'" 74 | }, 75 | [] 76 | ); 77 | 78 | export const equal = testQuery( 79 | null, 80 | { 81 | query: ` 82 | select 83 | null = null, 84 | {} = {}, 85 | { hi: undefined } = { hi: undefined }, 86 | { foo: { bar: {} } } = { foo: { bar: {} } }, 87 | { foo: 1, bar: 2 } = { bar: 2, foo: 1 }, 88 | { foo: 1, bar: 2 } = { foo: 1, bar: '2' }, 89 | { foo: 1, bar: 2 } = { foo: 1, bar: 2, baz: 3 }, 90 | [] = [], 91 | [undefined] = [undefined], 92 | [[1]] = [[1]], 93 | [1, 2] = [1, 2], 94 | [1, 2] = [1, '2'], 95 | [1, 2] = [1, 2, 3], 96 | [{foo: [{bar: true}]}] = [{foo:[{bar: true}]}], 97 | [{foo: [{bar: true}]}] = [{foo:[{bar: false}]}] 98 | ` 99 | }, 100 | [ 101 | { 102 | $1: true, 103 | $2: true, 104 | $3: true, 105 | $4: true, 106 | $5: true, 107 | $6: false, 108 | $7: false, 109 | $8: true, 110 | $9: true, 111 | $10: true, 112 | $11: true, 113 | $12: false, 114 | $13: false, 115 | $14: true, 116 | $15: false 117 | } 118 | ] 119 | ); 120 | 121 | export const equalUndefined = testQuery( 122 | [{ id: "hi" }], 123 | { 124 | query: ` 125 | select 126 | c.nonExist = c.nonExist, 127 | undefined = undefined, 128 | undefined = 0, 129 | undefined = null, 130 | null = 0, 131 | false = 0, 132 | true = 1, 133 | 1 = "1", 134 | {} = null, 135 | {} = [] 136 | from c 137 | ` 138 | }, 139 | [ 140 | { 141 | $5: false, 142 | $6: false, 143 | $7: false, 144 | $8: false, 145 | $9: false, 146 | $10: false 147 | } 148 | ] 149 | ); 150 | 151 | export const notEqualUndefined = testQuery( 152 | [{ id: "hi" }], 153 | { 154 | query: ` 155 | select 156 | c.nonExist != c.nonExist, 157 | undefined != undefined, 158 | undefined != 0, 159 | undefined != null, 160 | null != 0, 161 | false != 0, 162 | true != 1, 163 | 1 != "1", 164 | {} != null, 165 | {} != [] 166 | from c 167 | ` 168 | }, 169 | [ 170 | { 171 | $5: true, 172 | $6: true, 173 | $7: true, 174 | $8: true, 175 | $9: true, 176 | $10: true 177 | } 178 | ] 179 | ); 180 | 181 | [">", "<", ">=", "<="].forEach(op => { 182 | exports[`compareUndefined ${op}`] = testQuery( 183 | [{ id: "hi" }], 184 | { 185 | query: ` 186 | select 187 | c.nonExist ${op} c.nonExist, 188 | undefined ${op} undefined, 189 | undefined ${op} null, 190 | 1 ${op} false, 191 | 0 ${op} '1', 192 | null ${op} 0, 193 | 1 ${op} "1", 194 | {} ${op} {}, 195 | [] ${op} [] 196 | from c 197 | ` 198 | }, 199 | [{}] 200 | ); 201 | }); 202 | 203 | export const logicalOrOperator = testQuery( 204 | null, 205 | { 206 | query: ` 207 | select 208 | true or undefined, 209 | undefined or true, 210 | null or true, 211 | 0 or true, 212 | '' or true, 213 | {} or true, 214 | [] or true 215 | ` 216 | }, 217 | [ 218 | { 219 | $1: true, 220 | $2: true, 221 | $3: true, 222 | $4: true, 223 | $5: true, 224 | $6: true, 225 | $7: true 226 | } 227 | ] 228 | ); 229 | 230 | export const logicalOrOperatorUndefined = testQuery( 231 | null, 232 | { 233 | query: ` 234 | select 235 | false or undefined, 236 | undefined or false, 237 | 0 or false, 238 | '' or false, 239 | {} or false, 240 | [] or false, 241 | undefined or undefined, 242 | null or undefined, 243 | 0 or undefined, 244 | '' or undefined, 245 | {} or undefined, 246 | [] or undefined 247 | ` 248 | }, 249 | [{}] 250 | ); 251 | 252 | export const logicalAndOperator = testQuery( 253 | null, 254 | { 255 | query: ` 256 | select 257 | false and undefined, 258 | undefined and false, 259 | null and false, 260 | 0 and false, 261 | '' and false, 262 | {} and false, 263 | [] and false 264 | ` 265 | }, 266 | [ 267 | { 268 | $1: false, 269 | $2: false, 270 | $3: false, 271 | $4: false, 272 | $5: false, 273 | $6: false, 274 | $7: false 275 | } 276 | ] 277 | ); 278 | 279 | export const logicalAndOperatorUndefined = testQuery( 280 | null, 281 | { 282 | query: ` 283 | select 284 | true and undefined, 285 | undefined and true, 286 | 0 and true, 287 | '' and true, 288 | {} and true, 289 | [] and true, 290 | undefined and undefined, 291 | null and undefined, 292 | 0 and undefined, 293 | '' and undefined, 294 | {} and undefined, 295 | [] and undefined 296 | ` 297 | }, 298 | [{}] 299 | ); 300 | 301 | export const logicalNotOperatorUndefined = testQuery( 302 | null, 303 | { 304 | query: ` 305 | select 306 | not undefined, 307 | not null, 308 | not 0, 309 | not '', 310 | not {}, 311 | not [] 312 | ` 313 | }, 314 | [{}] 315 | ); 316 | 317 | export const ternaryOperator = testQuery( 318 | null, 319 | { 320 | query: ` 321 | select 322 | true ? true : false, 323 | null ? true : false, 324 | 1 ? true : false, 325 | 'str' ? true : false, 326 | {} ? true : false, 327 | [] ? true : false 328 | ` 329 | }, 330 | [ 331 | { 332 | $1: true, 333 | $2: false, 334 | $3: false, 335 | $4: false, 336 | $5: false, 337 | $6: false 338 | } 339 | ] 340 | ); 341 | 342 | export const concatenateOperator = testQuery( 343 | null, 344 | { 345 | query: ` 346 | select 347 | "foo" || "bar", 348 | "foo" || 0, 349 | 0 || "bar", 350 | undefined || "bar", 351 | null || "bar", 352 | true || "bar", 353 | {} || "bar", 354 | [] || "bar" 355 | ` 356 | }, 357 | [ 358 | { 359 | $1: "foobar" 360 | } 361 | ] 362 | ); 363 | 364 | ["+", "-", "*", "/", "%", "|", "&", "^", "<<", ">>", ">>>"].forEach(op => { 365 | exports[`arithmeticOperatorUndefined ${op}`] = testQuery( 366 | null, 367 | { 368 | query: ` 369 | select 370 | 0 ${op} undefined, 371 | 0 ${op} null, 372 | 0 ${op} true, 373 | 0 ${op} '1', 374 | 0 ${op} {}, 375 | 0 ${op} [], 376 | undefined ${op} 1, 377 | null ${op} 1, 378 | true ${op} 1, 379 | '1' ${op} 1, 380 | {} ${op} 1, 381 | [] ${op} 1 382 | ` 383 | }, 384 | [{}] 385 | ); 386 | }); 387 | 388 | ["+", "-", "~"].forEach(op => { 389 | exports[`arithmeticOperatorUndefined ${op}`] = testQuery( 390 | null, 391 | { 392 | query: ` 393 | select 394 | ${op}undefined, 395 | ${op}null, 396 | ${op}'0', 397 | ${op}true, 398 | ${op}false, 399 | ${op}{}, 400 | ${op}[] 401 | ` 402 | }, 403 | [{}] 404 | ); 405 | }); 406 | 407 | export const functionCall = testQuery( 408 | null, 409 | { 410 | query: "select ABS(-1), abs(-1), abs (-1), abs ( -1 )" 411 | }, 412 | [ 413 | { 414 | $1: 1, 415 | $2: 1, 416 | $3: 1, 417 | $4: 1 418 | } 419 | ] 420 | ); 421 | 422 | export const arrayContains = testQuery( 423 | null, 424 | { 425 | query: ` 426 | select 427 | array_contains([], null), 428 | array_contains([[1, 2]], [1], true), 429 | array_contains([[{foo:1, bar:2}]], [{foo:1}], true), 430 | array_contains([{foo: { a: 1, b: 2}, bar: 2}], {foo: { a: 1}}, true) 431 | ` 432 | }, 433 | [ 434 | { 435 | $1: false, 436 | $2: false, 437 | $3: true, 438 | $4: true 439 | } 440 | ] 441 | ); 442 | 443 | export const orderTypes = testQuery( 444 | [ 445 | 10, 446 | 1, 447 | 0, 448 | "2", 449 | 0.5, 450 | "b", 451 | "1", 452 | 2, 453 | "10", 454 | "01", 455 | false, 456 | "A", 457 | [], 458 | "B", 459 | "a", 460 | [2], 461 | [1], 462 | {}, 463 | { hi: 2 }, 464 | true, 465 | { hi: 1 }, 466 | null, 467 | undefined 468 | ].map((v, i) => ({ id: i, v })), 469 | { 470 | query: "select value c.v from c order by c.v" 471 | }, 472 | [ 473 | null, 474 | false, 475 | true, 476 | 0, 477 | 0.5, 478 | 1, 479 | 2, 480 | 10, 481 | "01", 482 | "1", 483 | "10", 484 | "2", 485 | "A", 486 | "B", 487 | "a", 488 | "b", 489 | [], 490 | [1], 491 | [2], 492 | {}, 493 | { hi: 2 }, 494 | { hi: 1 } 495 | ] 496 | ); 497 | 498 | export const aggregationWithUndefined = testQuery( 499 | [10, 1, 0, 0.5, 2, undefined].map((v, i) => ({ id: i, v })), 500 | { 501 | query: ` 502 | select 503 | count(c.v), 504 | sum(c.v), 505 | avg(c.v), 506 | max(c.v), 507 | min(c.v) 508 | from c 509 | ` 510 | }, 511 | [ 512 | { 513 | $1: 5, 514 | $2: 13.5, 515 | $3: 2.7, 516 | $4: 10, 517 | $5: 0 518 | } 519 | ] 520 | ); 521 | 522 | export const aggregationWithNull = testQuery( 523 | [10, 1, 0, 0.5, 2, null].map((v, i) => ({ id: i, v })), 524 | { 525 | query: ` 526 | select 527 | count(c.v), 528 | sum(c.v), 529 | avg(c.v), 530 | max(c.v), 531 | min(c.v) 532 | from c 533 | ` 534 | }, 535 | [ 536 | { 537 | $1: 6, 538 | $4: 10, 539 | $5: null 540 | } 541 | ] 542 | ); 543 | 544 | export const aggregationWithBoolean = testQuery( 545 | [10, 1, 0, 0.5, 2, true, false].map((v, i) => ({ id: i, v })), 546 | { 547 | query: ` 548 | select 549 | count(c.v), 550 | sum(c.v), 551 | avg(c.v), 552 | max(c.v), 553 | min(c.v) 554 | from c 555 | ` 556 | }, 557 | [ 558 | { 559 | $1: 7, 560 | $4: 10, 561 | $5: false 562 | } 563 | ] 564 | ); 565 | 566 | export const aggregationWithString = testQuery( 567 | [10, 1, 0, 0.5, 2, "01", "1", "10", "2", "A", "B", "a", "b"].map((v, i) => ({ 568 | id: i, 569 | v 570 | })), 571 | { 572 | query: ` 573 | select 574 | count(c.v), 575 | sum(c.v), 576 | avg(c.v), 577 | max(c.v), 578 | min(c.v) 579 | from c 580 | ` 581 | }, 582 | [ 583 | { 584 | $1: 13, 585 | $4: "b", 586 | $5: 0 587 | } 588 | ] 589 | ); 590 | 591 | export const aggregationWithArrayAndObject = testQuery( 592 | [10, 1, 0, 0.5, 2, [], {}].map((v, i) => ({ 593 | id: i, 594 | v 595 | })), 596 | { 597 | query: ` 598 | select 599 | count(c.v), 600 | sum(c.v), 601 | avg(c.v), 602 | max(c.v), 603 | min(c.v) 604 | from c 605 | ` 606 | }, 607 | [ 608 | { 609 | $1: 7 610 | } 611 | ] 612 | ); 613 | 614 | export const aggregationEmpty = testQuery( 615 | [], 616 | { 617 | query: ` 618 | select 619 | count(c.v), 620 | sum(c.v), 621 | avg(c.v), 622 | max(c.v), 623 | min(c.v) 624 | from c 625 | ` 626 | }, 627 | [ 628 | { 629 | $1: 0, 630 | $2: 0 631 | } 632 | ] 633 | ); 634 | 635 | export const functionWithParameters = testQuery( 636 | [{ id: "foo", name: "foo" }, { id: "bar", name: "bar" }], 637 | { 638 | query: "select * from c where ARRAY_CONTAINS(@names, c.name)", 639 | parameters: [{ name: "@names", value: ["foo"] }] 640 | }, 641 | [{ id: "foo", name: "foo" }] 642 | ); 643 | 644 | export const withEmptyNestedProperty = testQuery( 645 | [ 646 | { id: "foo", child: { name: "foo" } }, 647 | { id: "bar", child: null }, 648 | { id: "baz" } 649 | ], 650 | { 651 | query: "select * from c where c.child.name = @name", 652 | parameters: [{ name: "@name", value: "foo" }] 653 | }, 654 | [{ id: "foo", child: { name: "foo" } }] 655 | ); 656 | 657 | export const selectWithEmptyNestedProperty = testQuery( 658 | [ 659 | { id: "foo", child: { name: "foo" } }, 660 | { id: "bar", child: null }, 661 | { id: "baz" } 662 | ], 663 | { 664 | query: "select c.child.name from c" 665 | }, 666 | [{ name: "foo" }, {}, {}] 667 | ); 668 | 669 | export const orderByWithEmptyNestedProperty = testQuery( 670 | [ 671 | { id: "foo", child: { name: "foo" } }, 672 | { id: "bar", child: null }, 673 | { id: "baz" } 674 | ], 675 | { 676 | query: "select * from c order by c.child.name", 677 | parameters: [{ name: "@name", value: "foo" }] 678 | }, 679 | [{ id: "foo", child: { name: "foo" } }] 680 | ); 681 | 682 | export const deeplyNestedProperty = testQuery( 683 | [ 684 | { id: "foo", child: { grandchild: { greatgrandchild: { name: "foo" } } } }, 685 | { id: "bar", child: { grandchild: null } } 686 | ], 687 | { 688 | query: 689 | "select * from c where c.child.grandchild.greatgrandchild.name = @name", 690 | parameters: [{ name: "@name", value: "foo" }] 691 | }, 692 | [{ id: "foo", child: { grandchild: { greatgrandchild: { name: "foo" } } } }] 693 | ); 694 | 695 | export const conditionPropWithParameter = testQuery( 696 | [ 697 | { id: "foo", name: "foo" }, 698 | { id: "bar", name: "bar" }, 699 | { id: "baz", name: "baz" } 700 | ], 701 | { 702 | query: "select * from c where c[@prop] = 'foo'", 703 | parameters: [{ name: "@prop", value: "name" }] 704 | }, 705 | [{ id: "foo", name: "foo" }] 706 | ); 707 | 708 | export const orderByWithParameter = testQuery( 709 | [ 710 | { id: "foo", name: "foo" }, 711 | { id: "bar", name: "bar" }, 712 | { id: "baz", name: "baz" } 713 | ], 714 | { 715 | query: "select * from c order by c[@prop]", 716 | parameters: [{ name: "@prop", value: "name" }] 717 | }, 718 | [ 719 | { id: "bar", name: "bar" }, 720 | { id: "baz", name: "baz" }, 721 | { id: "foo", name: "foo" } 722 | ] 723 | ); 724 | 725 | export const notEqualDifferentType = testQuery( 726 | [{ id: "javi", foo: null }], 727 | { 728 | query: "select * from c where c.id = @id AND c.foo != 'test'", 729 | parameters: [{ name: "@id", value: "javi" }] 730 | }, 731 | [{ id: "javi", foo: null }] 732 | ); 733 | 734 | export const multipleOrderBy = testQuery( 735 | [ 736 | { id: "id1", sortKey1: "a", sortKey2: "a" }, 737 | { id: "id2", sortKey1: "a", sortKey2: "b" }, 738 | { id: "id3", sortKey1: "b", sortKey2: "a" }, 739 | { id: "id4", sortKey1: "b", sortKey2: "b" } 740 | ], 741 | { 742 | query: "select * from c order by c.sortKey1, c.sortKey2 DESC" 743 | }, 744 | [ 745 | { id: "id2", sortKey1: "a", sortKey2: "b" }, 746 | { id: "id1", sortKey1: "a", sortKey2: "a" }, 747 | { id: "id4", sortKey1: "b", sortKey2: "b" }, 748 | { id: "id3", sortKey1: "b", sortKey2: "a" } 749 | ] 750 | ); 751 | 752 | export const multipleOrderByWithCompositeIndexes = testQuery( 753 | [ 754 | { id: "id1", sortKey1: "a", sortKey2: "a" }, 755 | { id: "id2", sortKey1: "a", sortKey2: "b" }, 756 | { id: "id3", sortKey1: "b", sortKey2: "a" }, 757 | { id: "id4", sortKey1: "b", sortKey2: "b" } 758 | ], 759 | { 760 | query: "select * from c order by c.sortKey1, c.sortKey2 DESC", 761 | compositeIndexes: [ 762 | [ 763 | { path: "/sortKey1", order: "ascending" }, 764 | { path: "/sortKey2", order: "descending" } 765 | ] 766 | ] 767 | }, 768 | [ 769 | { id: "id2", sortKey1: "a", sortKey2: "b" }, 770 | { id: "id1", sortKey1: "a", sortKey2: "a" }, 771 | { id: "id4", sortKey1: "b", sortKey2: "b" }, 772 | { id: "id3", sortKey1: "b", sortKey2: "a" } 773 | ] 774 | ); 775 | 776 | export const multipleOrderByWithCompositeIndexes2 = testQuery( 777 | [ 778 | { id: "id1", prop: { sortKey1: "a", sortKey2: "a" } }, 779 | { id: "id2", prop: { sortKey1: "a", sortKey2: "b" } }, 780 | { id: "id3", prop: { sortKey1: "b", sortKey2: "a" } }, 781 | { id: "id4", prop: { sortKey1: "b", sortKey2: "b" } } 782 | ], 783 | { 784 | query: "select * from c order by c.prop.sortKey1 DESC, c.prop.sortKey2", 785 | compositeIndexes: [ 786 | [ 787 | { path: "/prop/sortKey1", order: "descending" }, 788 | { path: "/prop/sortKey2", order: "ascending" } 789 | ] 790 | ] 791 | }, 792 | [ 793 | { id: "id3", prop: { sortKey1: "b", sortKey2: "a" } }, 794 | { id: "id4", prop: { sortKey1: "b", sortKey2: "b" } }, 795 | { id: "id1", prop: { sortKey1: "a", sortKey2: "a" } }, 796 | { id: "id2", prop: { sortKey1: "a", sortKey2: "b" } } 797 | ] 798 | ); 799 | 800 | export const filterUndefinedOrderBy = testQuery( 801 | [{ id: "foo", sortKey: "a" }, { id: "bar" }, { id: "baz", sortKey: "b" }], 802 | { 803 | query: "SELECT * FROM c ORDER BY c.sortKey" 804 | }, 805 | [{ id: "foo", sortKey: "a" }, { id: "baz", sortKey: "b" }] 806 | ); 807 | 808 | export const DoNotfilterUndefinedMultipleOrderBy = testQuery( 809 | [ 810 | { id: "id1", sortKey1: "a", sortKey2: "a" }, 811 | { id: "id2", sortKey1: "b" }, 812 | { id: "id3", sortKey2: "b" }, 813 | { id: "id4" } 814 | ], 815 | { 816 | query: "SELECT * FROM c ORDER BY c.sortKey1, c.sortKey2 DESC" 817 | }, 818 | [ 819 | { id: "id3", sortKey2: "b" }, 820 | { id: "id4" }, 821 | { id: "id1", sortKey1: "a", sortKey2: "a" }, 822 | { id: "id2", sortKey1: "b" } 823 | ] 824 | ); 825 | -------------------------------------------------------------------------------- /test/paginate.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import query from "../lib"; 3 | 4 | const collection1 = [ 5 | { _rid: "a" }, 6 | { _rid: "c" }, 7 | { _rid: "b" }, 8 | { _rid: "d" } 9 | ]; 10 | 11 | const collection2 = [ 12 | { 13 | _rid: "a", 14 | children: [{ name: "a1" }, { name: "a2" }, { name: "a3" }, { name: "a4" }] 15 | }, 16 | { _rid: "b", children: [{ name: "b1" }] } 17 | ]; 18 | 19 | const collection3 = [ 20 | { _rid: "a", x: 3 }, 21 | { _rid: "b", x: 2 }, 22 | { _rid: "c", x: 1 }, 23 | { _rid: "d", x: 2 } 24 | ]; 25 | 26 | const collection4 = [ 27 | { _rid: "id1", sortKey1: "a", sortKey2: "a" }, 28 | { _rid: "id2", sortKey1: "a", sortKey2: "b" }, 29 | { _rid: "id3", sortKey1: "b", sortKey2: "a" }, 30 | { _rid: "id4", sortKey1: "b", sortKey2: "b" }, 31 | { _rid: "id5", sortKey1: "b", sortKey2: "c" } 32 | ]; 33 | 34 | export function testMaxItemCount() { 35 | const { result, continuation } = query(`SELECT * FROM c`).exec(collection1, { 36 | maxItemCount: 2 37 | }); 38 | assert.deepStrictEqual(result, [{ _rid: "a" }, { _rid: "b" }]); 39 | assert.deepStrictEqual(continuation, { 40 | token: "+RID:c#RT:1#TRC:2", 41 | range: { min: "", max: "FF" } 42 | }); 43 | } 44 | 45 | export function testContinuation() { 46 | const q = query(`SELECT * FROM c`); 47 | 48 | let { result, continuation } = q.exec(collection1, { maxItemCount: 1 }); 49 | assert.deepStrictEqual(result, [{ _rid: "a" }]); 50 | assert.equal(continuation.token, "+RID:b#RT:1#TRC:1"); 51 | 52 | ({ result, continuation } = q.exec(collection1, { 53 | maxItemCount: 2, 54 | continuation 55 | })); 56 | assert.deepStrictEqual(result, [{ _rid: "b" }, { _rid: "c" }]); 57 | assert.equal(continuation.token, "+RID:d#RT:2#TRC:3"); 58 | 59 | ({ result, continuation } = q.exec(collection1, { continuation })); 60 | assert.deepStrictEqual(result, [{ _rid: "d" }]); 61 | assert.equal(continuation, null); 62 | } 63 | 64 | export function testContinuationWithoutNextItem() { 65 | const q = query(`SELECT * FROM c`); 66 | const { continuation } = q.exec(collection1, { maxItemCount: 2 }); 67 | const collection = collection1.filter(d => d._rid !== "c"); 68 | const { result } = q.exec(collection, { continuation }); 69 | assert.deepStrictEqual(result, [{ _rid: "d" }]); 70 | } 71 | 72 | export function testJoin() { 73 | const q = query(`SELECT c.name FROM r JOIN c IN r.children`); 74 | 75 | let { result, continuation } = q.exec(collection2, { maxItemCount: 2 }); 76 | assert.deepStrictEqual(result, [{ name: "a1" }, { name: "a2" }]); 77 | assert.deepStrictEqual(continuation, { 78 | token: "+RID:a#RT:1#SRC:2#TRC:2", 79 | range: { min: "", max: "FF" } 80 | }); 81 | 82 | ({ result, continuation } = q.exec(collection2, { 83 | maxItemCount: 2, 84 | continuation 85 | })); 86 | assert.deepStrictEqual(result, [{ name: "a3" }, { name: "a4" }]); 87 | assert.deepStrictEqual(continuation, { 88 | token: "+RID:b#RT:2#TRC:4", 89 | range: { min: "", max: "FF" } 90 | }); 91 | } 92 | 93 | export function testOrderBy() { 94 | const q = query(`SELECT * FROM c ORDER BY c.x`); 95 | 96 | let { result, continuation } = q.exec(collection3, { maxItemCount: 2 }); 97 | assert.deepStrictEqual(result, [{ _rid: "c", x: 1 }, { _rid: "b", x: 2 }]); 98 | assert.equal(continuation.token, "+RID:d#RT:1#TRC:2#RTD:WzJd"); 99 | 100 | ({ result, continuation } = q.exec(collection3, { 101 | maxItemCount: 2, 102 | continuation 103 | })); 104 | assert.deepStrictEqual(result, [{ _rid: "d", x: 2 }, { _rid: "a", x: 3 }]); 105 | 106 | assert.equal(continuation, null); 107 | } 108 | 109 | export function testOrderByDesc() { 110 | const q = query(`SELECT * FROM c ORDER BY c.x DESC`); 111 | 112 | let { result, continuation } = q.exec(collection3, { maxItemCount: 2 }); 113 | assert.deepStrictEqual(result, [{ _rid: "a", x: 3 }, { _rid: "b", x: 2 }]); 114 | assert.equal(continuation.token, "+RID:d#RT:1#TRC:2#RTD:WzJd"); 115 | 116 | ({ result, continuation } = q.exec(collection3, { 117 | maxItemCount: 2, 118 | continuation 119 | })); 120 | assert.deepStrictEqual(result, [{ _rid: "d", x: 2 }, { _rid: "c", x: 1 }]); 121 | 122 | assert.equal(continuation, null); 123 | } 124 | 125 | export function testOrderByWithoutNextItem() { 126 | const q = query(`SELECT * FROM c ORDER BY c.x`); 127 | let { result, continuation } = q.exec(collection3, { maxItemCount: 2 }); 128 | const collection = collection3.filter(d => d._rid !== "d"); 129 | ({ result, continuation } = q.exec(collection, { 130 | maxItemCount: 2, 131 | continuation 132 | })); 133 | assert.deepStrictEqual(result, [{ _rid: "a", x: 3 }]); 134 | assert.equal(continuation, null); 135 | } 136 | 137 | export function testMultipleOrderBy() { 138 | const q = query(`SELECT * FROM c ORDER BY c.sortKey1, c.sortKey2 DESC`); 139 | 140 | let { result, continuation } = q.exec(collection4, { maxItemCount: 2 }); 141 | assert.deepStrictEqual(result, [ 142 | { _rid: "id2", sortKey1: "a", sortKey2: "b" }, 143 | { _rid: "id1", sortKey1: "a", sortKey2: "a" } 144 | ]); 145 | assert.equal(continuation.token, "+RID:id5#RT:1#TRC:2#RTD:WyJiIiwiYyJd"); 146 | 147 | ({ result, continuation } = q.exec(collection4, { 148 | maxItemCount: 2, 149 | continuation 150 | })); 151 | assert.deepStrictEqual(result, [ 152 | { _rid: "id5", sortKey1: "b", sortKey2: "c" }, 153 | { _rid: "id4", sortKey1: "b", sortKey2: "b" } 154 | ]); 155 | assert.equal(continuation.token, "+RID:id3#RT:2#TRC:4#RTD:WyJiIiwiYSJd"); 156 | 157 | ({ result, continuation } = q.exec(collection4, { 158 | maxItemCount: 2, 159 | continuation 160 | })); 161 | assert.deepStrictEqual(result, [ 162 | { _rid: "id3", sortKey1: "b", sortKey2: "a" } 163 | ]); 164 | assert.equal(continuation, null); 165 | } 166 | -------------------------------------------------------------------------------- /test/partition-keys.ts: -------------------------------------------------------------------------------- 1 | import testPartitionKeys from "./utils/test-partition-keys"; 2 | 3 | export const contains = testPartitionKeys("SELECT * FROM c WHERE c.key = 1", [ 4 | "/key" 5 | ]); 6 | 7 | export const notContain = testPartitionKeys("SELECT * FROM c", ["/key"], false); 8 | 9 | export const containsAndExpression = testPartitionKeys( 10 | "SELECT * FROM c WHERE c.key = 1 AND c.foo = 2", 11 | ["/key"] 12 | ); 13 | 14 | export const notContainsOrExpression = testPartitionKeys( 15 | "SELECT * FROM c WHERE c.key = 1 OR c.foo = 2", 16 | ["/key"], 17 | false 18 | ); 19 | 20 | export const containsAndOrExpression = testPartitionKeys( 21 | "SELECT * FROM c WHERE c.key = 1 OR (c.key = 2 AND c.foo = 3)", 22 | ["/key"] 23 | ); 24 | 25 | export const notContainsAndOrExpression = testPartitionKeys( 26 | "SELECT * FROM c WHERE (c.key = 1 AND c.foo = 2) OR c.bar = 3", 27 | ["/key"], 28 | false 29 | ); 30 | 31 | export const containsNestedKey = testPartitionKeys( 32 | "SELECT * FROM c WHERE c.foo.bar = 1", 33 | ["/foo/bar"] 34 | ); 35 | 36 | export const containsMultipleKeys = testPartitionKeys( 37 | "SELECT * FROM c WHERE c.foo = 1 AND c.bar = 2", 38 | ["/foo", "/bar"] 39 | ); 40 | 41 | export const notContainsMultipleKeys = testPartitionKeys( 42 | "SELECT * FROM c WHERE c.foo = 1 OR c.bar = 2", 43 | ["/foo", "/bar"], 44 | false 45 | ); 46 | 47 | export const empty = testPartitionKeys("SELECT * FROM c", []); 48 | 49 | export const computedProperty = testPartitionKeys( 50 | 'SELECT * FROM c WHERE c["partition"] = @pkValue', 51 | ["/partition"], 52 | true 53 | ); 54 | -------------------------------------------------------------------------------- /test/range.ts: -------------------------------------------------------------------------------- 1 | import testQuery from "./utils/test-query"; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const root = testQuery( 5 | [ 6 | { 7 | id: 1, 8 | ts: 1 9 | }, 10 | { 11 | id: 2, 12 | ts: 2 13 | }, 14 | { 15 | id: 3, 16 | ts: 3 17 | } 18 | ], 19 | { 20 | query: "SELECT * FROM c WHERE c.ts < @a AND c.ts > @b", 21 | parameters: [{ name: "@a", value: 3 }, { name: "@b", value: 1 }] 22 | }, 23 | [ 24 | { 25 | id: 2, 26 | ts: 2 27 | } 28 | ] 29 | ); 30 | -------------------------------------------------------------------------------- /test/subquery.ts: -------------------------------------------------------------------------------- 1 | // test examples on https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery 2 | 3 | import * as assert from "assert"; 4 | import query from "../lib"; 5 | 6 | const collection = [ 7 | { 8 | id: "00001", 9 | description: "babyfood, infant formula", 10 | tags: [{ name: "babyfood" }, { name: "infant formula" }], 11 | nutrients: [ 12 | { 13 | id: "1", 14 | description: "A", 15 | nutritionValue: 1, 16 | units: "g" 17 | } 18 | ], 19 | servings: [{ amount: 1 }, { amount: 2 }] 20 | }, 21 | { 22 | id: "00002", 23 | description: "snacks, infant formula", 24 | tags: [{ name: "snacks" }, { name: "infant formula" }], 25 | nutrients: [ 26 | { id: "1", description: "A", nutritionValue: 1, units: "g" }, 27 | { id: "2", description: "B", nutritionValue: 2 }, 28 | { id: "3", description: "C", nutritionValue: 101, units: "mg" } 29 | ], 30 | servings: [{ amount: 3 }, { amount: 4 }] 31 | }, 32 | { 33 | id: "00003", 34 | description: "fruit, orange", 35 | tags: [{ name: "fruit" }, { name: "orange" }], 36 | nutrients: [ 37 | { id: "4", description: "C", nutritionValue: 70, units: "mg" }, 38 | { id: "5", description: "D", nutritionValue: 102, units: "mg" } 39 | ], 40 | servings: [{ amount: 3 }] 41 | } 42 | ]; 43 | 44 | export const optimizeJoinExpression = () => { 45 | const data1 = query(` 46 | SELECT Count(1) AS Count 47 | FROM c 48 | JOIN t IN c.tags 49 | JOIN n IN c.nutrients 50 | JOIN s IN c.servings 51 | WHERE t.name = 'infant formula' 52 | AND (n.nutritionValue > 0 53 | AND n.nutritionValue < 10) AND s.amount > 1 54 | `).exec(collection); 55 | 56 | const data2 = query(` 57 | SELECT Count(1) AS Count 58 | FROM c 59 | JOIN (SELECT VALUE t FROM t IN c.tags WHERE t.name = 'infant formula') 60 | JOIN (SELECT VALUE n FROM n IN c.nutrients WHERE n.nutritionValue > 0 AND n.nutritionValue < 10) 61 | JOIN (SELECT VALUE s FROM s IN c.servings WHERE s.amount > 1) 62 | `).exec(collection); 63 | 64 | assert.deepStrictEqual(data1, data2); 65 | assert.deepStrictEqual(data1.result, [{ Count: 5 }]); 66 | }; 67 | 68 | export const evaluateOnceAndReferenceManyTimes = () => { 69 | const udf = { 70 | GetMaxNutritionValue(nutrients: any) { 71 | if (!nutrients || !Array.isArray(nutrients) || !nutrients.length) 72 | return undefined; 73 | 74 | return nutrients.reduce( 75 | (max, n) => Math.max(max, n.nutritionValue), 76 | -Infinity 77 | ); 78 | } 79 | }; 80 | 81 | const data1 = query(` 82 | SELECT c.id, udf.GetMaxNutritionValue(c.nutrients) AS MaxNutritionValue 83 | FROM c 84 | WHERE udf.GetMaxNutritionValue(c.nutrients) > 100 85 | `).exec(collection, { udf }); 86 | 87 | const data2 = query(` 88 | SELECT TOP 1000 c.id, MaxNutritionValue 89 | FROM c 90 | JOIN (SELECT VALUE udf.GetMaxNutritionValue(c.nutrients)) MaxNutritionValue 91 | WHERE MaxNutritionValue > 100 92 | `).exec(collection, { udf }); 93 | 94 | const data3 = query(` 95 | SELECT TOP 1000 c.id, m.MaxNutritionValue 96 | FROM c 97 | JOIN (SELECT udf.GetMaxNutritionValue(c.nutrients) AS MaxNutritionValue) m 98 | WHERE m.MaxNutritionValue > 100 99 | `).exec(collection, { udf }); 100 | 101 | assert.deepStrictEqual(data1, data2); 102 | assert.deepStrictEqual(data1, data3); 103 | 104 | assert.deepStrictEqual(data1.result, [ 105 | { id: "00002", MaxNutritionValue: 101 }, 106 | { id: "00003", MaxNutritionValue: 102 } 107 | ]); 108 | }; 109 | 110 | export const evaluateOnceAndReferenceManyTimes2 = () => { 111 | const data = query(` 112 | SELECT TOP 1000 c.id, AvgNutritionValue 113 | FROM c 114 | JOIN (SELECT VALUE avg(n.nutritionValue) FROM n IN c.nutrients) AvgNutritionValue 115 | WHERE AvgNutritionValue > 80 116 | `).exec(collection); 117 | 118 | assert.deepStrictEqual(data.result, [{ id: "00003", AvgNutritionValue: 86 }]); 119 | }; 120 | 121 | export const mimicJoinWithExternalReferenceData = () => { 122 | const data = query(` 123 | SELECT TOP 10 n.id, n.description, n.nutritionValue, n.units, r.name 124 | FROM food 125 | JOIN n IN food.nutrients 126 | JOIN r IN ( 127 | SELECT VALUE [ 128 | {unit: 'ng', name: 'nanogram', multiplier: 0.000000001, baseUnit: 'gram'}, 129 | {unit: 'µg', name: 'microgram', multiplier: 0.000001, baseUnit: 'gram'}, 130 | {unit: 'mg', name: 'milligram', multiplier: 0.001, baseUnit: 'gram'}, 131 | {unit: 'g', name: 'gram', multiplier: 1, baseUnit: 'gram'}, 132 | {unit: 'kg', name: 'kilogram', multiplier: 1000, baseUnit: 'gram'}, 133 | {unit: 'Mg', name: 'megagram', multiplier: 1000000, baseUnit: 'gram'}, 134 | {unit: 'Gg', name: 'gigagram', multiplier: 1000000000, baseUnit: 'gram'}, 135 | {unit: 'nJ', name: 'nanojoule', multiplier: 0.000000001, baseUnit: 'joule'}, 136 | {unit: 'µJ', name: 'microjoule', multiplier: 0.000001, baseUnit: 'joule'}, 137 | {unit: 'mJ', name: 'millijoule', multiplier: 0.001, baseUnit: 'joule'}, 138 | {unit: 'J', name: 'joule', multiplier: 1, baseUnit: 'joule'}, 139 | {unit: 'kJ', name: 'kilojoule', multiplier: 1000, baseUnit: 'joule'}, 140 | {unit: 'MJ', name: 'megajoule', multiplier: 1000000, baseUnit: 'joule'}, 141 | {unit: 'GJ', name: 'gigajoule', multiplier: 1000000000, baseUnit: 'joule'}, 142 | {unit: 'cal', name: 'calorie', multiplier: 1, baseUnit: 'calorie'}, 143 | {unit: 'kcal', name: 'Calorie', multiplier: 1000, baseUnit: 'calorie'}, 144 | {unit: 'IU', name: 'International units'} 145 | ] 146 | ) 147 | WHERE n.units = r.unit 148 | `).exec(collection); 149 | 150 | assert.deepStrictEqual(data.result, [ 151 | { 152 | id: "1", 153 | description: "A", 154 | nutritionValue: 1, 155 | units: "g", 156 | name: "gram" 157 | }, 158 | { 159 | id: "1", 160 | description: "A", 161 | nutritionValue: 1, 162 | units: "g", 163 | name: "gram" 164 | }, 165 | { 166 | id: "3", 167 | description: "C", 168 | nutritionValue: 101, 169 | units: "mg", 170 | name: "milligram" 171 | }, 172 | { 173 | id: "4", 174 | description: "C", 175 | nutritionValue: 70, 176 | units: "mg", 177 | name: "milligram" 178 | }, 179 | { 180 | id: "5", 181 | description: "D", 182 | nutritionValue: 102, 183 | units: "mg", 184 | name: "milligram" 185 | } 186 | ]); 187 | }; 188 | 189 | export const simpleExpressionScalarSubqueries1 = () => { 190 | const data1 = query(` 191 | SELECT 1 AS a, 2 AS b 192 | `).exec(collection); 193 | 194 | const data2 = query(` 195 | SELECT (SELECT VALUE 1) AS a, (SELECT VALUE 2) AS b 196 | `).exec(collection); 197 | 198 | assert.deepStrictEqual(data1, data2); 199 | assert.deepStrictEqual(data1.result, [{ a: 1, b: 2 }]); 200 | }; 201 | 202 | export const simpleExpressionScalarSubqueries2 = () => { 203 | const data1 = query(` 204 | SELECT TOP 5 Concat('id_', f.id) AS id 205 | FROM food f 206 | `).exec(collection); 207 | 208 | const data2 = query(` 209 | SELECT TOP 5 (SELECT VALUE Concat('id_', f.id)) AS id 210 | FROM food f 211 | `).exec(collection); 212 | 213 | assert.deepStrictEqual(data1, data2); 214 | assert.deepStrictEqual(data1.result, [ 215 | { id: "id_00001" }, 216 | { id: "id_00002" }, 217 | { id: "id_00003" } 218 | ]); 219 | }; 220 | 221 | export const simpleExpressionScalarSubqueries3 = () => { 222 | const data1 = query(` 223 | SELECT TOP 5 f.id, Contains(f.description, 'fruit') = true ? f.description : undefined 224 | FROM food f 225 | `).exec(collection); 226 | 227 | // NOTE: The document says "You can rewrite this query" but they're a bit different 228 | assert.deepStrictEqual(data1.result, [ 229 | { id: "00001" }, 230 | { id: "00002" }, 231 | { id: "00003", $1: "fruit, orange" } 232 | ]); 233 | 234 | const data2 = query(` 235 | SELECT TOP 10 f.id, (SELECT f.description WHERE Contains(f.description, 'fruit')).description 236 | FROM food f 237 | `).exec(collection); 238 | 239 | assert.deepStrictEqual(data2.result, [ 240 | { id: "00001" }, 241 | { id: "00002" }, 242 | { id: "00003", description: "fruit, orange" } 243 | ]); 244 | }; 245 | 246 | export const aggregateScalarSubqueries1 = () => { 247 | const data = query(` 248 | SELECT TOP 5 249 | f.id, 250 | (SELECT VALUE Count(1) FROM n IN f.nutrients WHERE n.units = 'mg' 251 | ) AS count_mg 252 | FROM food f 253 | `).exec(collection); 254 | 255 | assert.deepStrictEqual(data.result, [ 256 | { id: "00001", count_mg: 0 }, 257 | { id: "00002", count_mg: 1 }, 258 | { id: "00003", count_mg: 2 } 259 | ]); 260 | }; 261 | 262 | export const aggregateScalarSubqueries2 = () => { 263 | const data = query(` 264 | SELECT TOP 5 f.id, ( 265 | SELECT Count(1) AS count, Sum(n.nutritionValue) AS sum 266 | FROM n IN f.nutrients 267 | WHERE n.units = 'mg' 268 | ) AS unit_mg 269 | FROM food f 270 | `).exec(collection); 271 | 272 | assert.deepStrictEqual(data.result, [ 273 | { id: "00001", unit_mg: { count: 0, sum: 0 } }, 274 | { id: "00002", unit_mg: { count: 1, sum: 101 } }, 275 | { id: "00003", unit_mg: { count: 2, sum: 172 } } 276 | ]); 277 | }; 278 | 279 | export const aggregateScalarSubqueries3 = () => { 280 | const data1 = query(` 281 | SELECT TOP 5 282 | f.id, 283 | (SELECT VALUE Count(1) FROM n IN f.nutrients WHERE n.units = 'mg') AS count_mg 284 | FROM food f 285 | WHERE (SELECT VALUE Count(1) FROM n IN f.nutrients WHERE n.units = 'mg') > 1 286 | `).exec(collection); 287 | 288 | const data2 = query(` 289 | SELECT TOP 5 f.id, count_mg 290 | FROM food f 291 | JOIN (SELECT VALUE Count(1) FROM n IN f.nutrients WHERE n.units = 'mg') AS count_mg 292 | WHERE count_mg > 1 293 | `).exec(collection); 294 | 295 | assert.deepStrictEqual(data1, data2); 296 | assert.deepStrictEqual(data1.result, [{ id: "00003", count_mg: 2 }]); 297 | }; 298 | 299 | export const existsExpression1 = () => { 300 | const data = query(` 301 | SELECT EXISTS (SELECT VALUE undefined) 302 | `).exec(collection); 303 | assert.deepStrictEqual(data.result, [{ $1: false }]); 304 | }; 305 | 306 | export const existsExpression2 = () => { 307 | const data = query(` 308 | SELECT EXISTS (SELECT undefined) 309 | `).exec(collection); 310 | assert.deepStrictEqual(data.result, [{ $1: true }]); 311 | }; 312 | 313 | export const rewritingArrayContainsAndJoinAsExists1 = () => { 314 | const data1 = query(` 315 | SELECT TOP 5 f.id, f.tags 316 | FROM food f 317 | WHERE ARRAY_CONTAINS(f.tags, {name: 'orange'}) 318 | `).exec(collection); 319 | 320 | const data2 = query(` 321 | SELECT TOP 5 f.id, f.tags 322 | FROM food f 323 | WHERE EXISTS(SELECT VALUE t FROM t IN f.tags WHERE t.name = 'orange') 324 | `).exec(collection); 325 | 326 | assert.deepStrictEqual(data1, data2); 327 | assert.deepStrictEqual(data1.result, [ 328 | { id: "00003", tags: [{ name: "fruit" }, { name: "orange" }] } 329 | ]); 330 | }; 331 | 332 | export const rewritingArrayContainsAndJoinAsExists2 = () => { 333 | const data = query(` 334 | SELECT VALUE c.description 335 | FROM c 336 | WHERE EXISTS( 337 | SELECT VALUE n 338 | FROM n IN c.nutrients 339 | WHERE n.units = "mg" AND n.nutritionValue > 0 340 | ) 341 | `).exec(collection); 342 | 343 | assert.deepStrictEqual(data.result, [ 344 | "snacks, infant formula", 345 | "fruit, orange" 346 | ]); 347 | }; 348 | 349 | export const rewritingArrayContainsAndJoinAsExists3 = () => { 350 | const data = query(` 351 | SELECT TOP 1 c.description, EXISTS( 352 | SELECT VALUE n 353 | FROM n IN c.nutrients 354 | WHERE n.units = "mg" AND n.nutritionValue > 0) as a 355 | FROM c 356 | `).exec(collection); 357 | 358 | assert.deepStrictEqual(data.result, [ 359 | { description: "babyfood, infant formula", a: false } 360 | ]); 361 | }; 362 | 363 | export const arrayExpression1 = () => { 364 | const data = query(` 365 | SELECT TOP 1 f.id, ARRAY(SELECT VALUE t.name FROM t in f.tags) AS tagNames 366 | FROM food f 367 | `).exec(collection); 368 | assert.deepStrictEqual(data.result, [ 369 | { 370 | id: "00001", 371 | tagNames: ["babyfood", "infant formula"] 372 | } 373 | ]); 374 | }; 375 | 376 | export const arrayExpression2 = () => { 377 | const data = query(` 378 | SELECT TOP 1 c.id, ARRAY(SELECT VALUE t FROM t in c.tags WHERE t.name != 'infant formula') AS tagNames 379 | FROM c 380 | `).exec(collection); 381 | assert.deepStrictEqual(data.result, [ 382 | { 383 | id: "00001", 384 | tagNames: [{ name: "babyfood" }] 385 | } 386 | ]); 387 | }; 388 | 389 | export const arrayExpression3 = () => { 390 | const data = query(` 391 | SELECT TOP 1 c.id, ARRAY(SELECT VALUE t.name FROM t in c.tags) as tagNames 392 | FROM c 393 | JOIN n IN (SELECT VALUE ARRAY(SELECT t FROM t in c.tags WHERE t.name != 'infant formula')) 394 | `).exec(collection); 395 | assert.deepStrictEqual(data.result, [ 396 | { 397 | id: "00001", 398 | tagNames: ["babyfood", "infant formula"] 399 | } 400 | ]); 401 | }; 402 | -------------------------------------------------------------------------------- /test/utils/test-partition-keys.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import query from "../../lib"; 3 | 4 | export default (q: string, keys: string[], expected: boolean = true) => () => { 5 | const result = query(q).containsPartitionKeys(keys); 6 | assert.strictEqual(result, expected); 7 | }; 8 | -------------------------------------------------------------------------------- /test/utils/test-query.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | // eslint-disable-next-line no-unused-vars 3 | import query, { CompositeIndex } from "../../lib"; 4 | 5 | export default ( 6 | collection: any[] | undefined | null, 7 | params: { 8 | query: string; 9 | parameters?: { 10 | name: string; 11 | value: any; 12 | }[]; 13 | udf?: { 14 | [x: string]: any; 15 | }; 16 | compositeIndexes?: CompositeIndex[][]; 17 | }, 18 | expected: 19 | | any 20 | | Error 21 | | { 22 | [x: string]: any; 23 | } 24 | ) => () => { 25 | const opts = { 26 | parameters: params.parameters, 27 | udf: params.udf, 28 | compositeIndexes: params.compositeIndexes 29 | }; 30 | 31 | if (expected instanceof Error || expected.prototype instanceof Error) { 32 | assert.throws( 33 | () => query(params.query).exec(collection, opts), 34 | (err: Error) => { 35 | if (expected instanceof Error) { 36 | const e = expected as Error; 37 | return err.message === e.message && err.name === e.name; 38 | } 39 | return err instanceof expected || err.name === expected.name; 40 | } 41 | ); 42 | return; 43 | } 44 | 45 | const { result } = query(params.query).exec(collection, opts); 46 | assert.deepStrictEqual(result, expected); 47 | }; 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "CommonJS", 5 | "noImplicitAny": true, 6 | "outDir": "lib", 7 | "target": "es2019" 8 | }, 9 | "include": [ 10 | "src/**/*.ts" 11 | ] 12 | } 13 | --------------------------------------------------------------------------------