@steedos/odata-v4-sql 2.2.52-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@steedos/odata-v4-sql",
3
+ "version": "2.2.52-beta.40",
4
+ "description": "OData to SQL query compiler",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index",
7
+ "directories": {
8
+ "test": "test"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "pretest": "npm run build",
13
+ "test": "mocha",
14
+ "pretdd": "npm run build",
15
+ "tdd": "mocha -w",
16
+ "prepare": "npm run build"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/jaystack/odata-v4-sql.git"
21
+ },
22
+ "keywords": [
23
+ "OData",
24
+ "V4",
25
+ "sql"
26
+ ],
27
+ "author": "JayStack",
28
+ "license": "MIT",
29
+ "bugs": {
30
+ "url": "https://github.com/jaystack/odata-v4-sql/issues"
31
+ },
32
+ "homepage": "https://github.com/jaystack/odata-v4-sql#readme",
33
+ "dependencies": {
34
+ "odata-v4-literal": "^0.1.0",
35
+ "odata-v4-parser": "^0.1.29"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "devDependencies": {
41
+ "chai": "^3.5.0",
42
+ "mocha": "^3.1.2",
43
+ "typescript": "^2.5.1"
44
+ },
45
+ "gitHead": "2cb19790ec035728e2df8eacf78df5e1c0b4cb6a"
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { Visitor, SQLLang } from "./visitor";
2
+ export { SQLLang } from "./visitor";
3
+ import { filter, query } from "odata-v4-parser";
4
+ import { Token } from "odata-v4-parser/lib/lexer";
5
+
6
+ export interface SqlOptions{
7
+ useParameters?:boolean
8
+ type?:SQLLang
9
+ }
10
+
11
+ /**
12
+ * Creates an SQL query descriptor from an OData query string
13
+ * @param {string} odataQuery - An OData query string
14
+ * @return {string} SQL query descriptor
15
+ * @example
16
+ * const filter = createQuery("$filter=Size eq 4 and Age gt 18");
17
+ * let sqlQuery = `SELECT * FROM table WHERE ${filter.where}`;
18
+ */
19
+ export function createQuery(odataQuery:string, options?:SqlOptions):Visitor;
20
+ export function createQuery(odataQuery:string, options?:SqlOptions, type?:SQLLang):Visitor;
21
+ export function createQuery(odataQuery:Token, options?:SqlOptions):Visitor;
22
+ export function createQuery(odataQuery:Token, options?:SqlOptions, type?:SQLLang):Visitor;
23
+ export function createQuery(odataQuery:string | Token, options = <SqlOptions>{}, type?:SQLLang):Visitor{
24
+ if (typeof type != "undefined" && type) options.type = type;
25
+ let ast:Token = <Token>(typeof odataQuery == "string" ? query(<string>odataQuery) : odataQuery);
26
+ return new Visitor(options).Visit(ast).asType();
27
+ }
28
+
29
+ /**
30
+ * Creates an SQL WHERE clause from an OData filter expression string
31
+ * @param {string} odataFilter - A filter expression in OData $filter format
32
+ * @return {string} SQL WHERE clause
33
+ * @example
34
+ * const filter = createFilter("Size eq 4 and Age gt 18");
35
+ * let sqlQuery = `SELECT * FROM table WHERE ${filter}`;
36
+ */
37
+ export function createFilter(odataFilter:string, options?:SqlOptions):Visitor;
38
+ export function createFilter(odataFilter:string, options?:SqlOptions, type?:SQLLang):Visitor;
39
+ export function createFilter(odataFilter:Token, options?:SqlOptions):Visitor;
40
+ export function createFilter(odataFilter:Token, options?:SqlOptions, type?:SQLLang):Visitor;
41
+ export function createFilter(odataFilter:string | Token, options = <SqlOptions>{}, type?:SQLLang):Visitor{
42
+ if (typeof type != "undefined" && type) options.type = type;
43
+ let ast:Token = <Token>(typeof odataFilter == "string" ? filter(<string>odataFilter) : odataFilter);
44
+ return new Visitor(options).Visit(ast).asType();
45
+ }
package/src/visitor.ts ADDED
@@ -0,0 +1,411 @@
1
+ import { Token } from "odata-v4-parser/lib/lexer";
2
+ import { Literal } from "odata-v4-literal";
3
+ import { SqlOptions } from "./index";
4
+
5
+ export class SQLLiteral extends Literal{
6
+ static convert(type:string, value:string):any {
7
+ return (new SQLLiteral(type, value)).valueOf();
8
+ }
9
+ 'Edm.String'(value:string){ return "'" + decodeURIComponent(value).slice(1, -1).replace(/''/g, "'") + "'"; }
10
+ 'Edm.Guid'(value:string){ return "'" + decodeURIComponent(value) + "'"; }
11
+ 'Edm.Date'(value:string){ return "'" + value + "'"; }
12
+ 'Edm.DateTimeOffset'(value:string):any{ return "'" + value.replace("T", " ").replace("Z", " ").trim() + "'"; }
13
+ 'Edm.Boolean'(value:string):any{
14
+ value = value || '';
15
+ switch (value.toLowerCase()){
16
+ case 'true': return 1;
17
+ case 'false': return 0;
18
+ default: return "NULL";
19
+ }
20
+ }
21
+ 'null'(value:string){ return "NULL"; }
22
+ }
23
+
24
+ export enum SQLLang{
25
+ ANSI,
26
+ MsSql,
27
+ MySql,
28
+ PostgreSql,
29
+ Oracle
30
+ }
31
+
32
+ export class Visitor{
33
+ protected options:SqlOptions
34
+ type:SQLLang;
35
+ select:string = "";
36
+ where:string = "";
37
+ orderby:string = "";
38
+ skip:number
39
+ limit:number
40
+ inlinecount:boolean
41
+ navigationProperty:string
42
+ includes:Visitor[] = [];
43
+ parameters:any = new Map();
44
+ protected parameterSeed:number = 0;
45
+ protected originalWhere:string;
46
+ ast:Token
47
+
48
+ constructor(options = <SqlOptions>{}){
49
+ this.options = options;
50
+ if (this.options.useParameters != false) this.options.useParameters = true;
51
+ this.type = options.type || SQLLang.ANSI;
52
+ }
53
+
54
+ from(table:string){
55
+ let sql = `SELECT ${this.select} FROM [${table}] WHERE ${this.where} ORDER BY ${this.orderby}`;
56
+ switch (this.type){
57
+ case SQLLang.Oracle:
58
+ case SQLLang.MsSql:
59
+ if (typeof this.skip == "number") sql += ` OFFSET ${this.skip} ROWS`;
60
+ if (typeof this.limit == "number"){
61
+ if (typeof this.skip != "number") sql += " OFFSET 0 ROWS";
62
+ sql += ` FETCH NEXT ${this.limit} ROWS ONLY`;
63
+ }
64
+ break;
65
+ case SQLLang.MySql:
66
+ case SQLLang.PostgreSql:
67
+ default:
68
+ if (typeof this.limit == "number") sql += ` LIMIT ${this.limit}`;
69
+ if (typeof this.skip == "number") sql += ` OFFSET ${this.skip}`;
70
+ break;
71
+ }
72
+ return sql;
73
+ }
74
+
75
+ asMsSql(){
76
+ this.type = SQLLang.MsSql;
77
+ let rx = new RegExp("\\?", "g");
78
+ let keys = this.parameters.keys();
79
+ this.originalWhere = this.where;
80
+ this.where = this.where.replace(rx, () => `@${keys.next().value}`);
81
+ this.includes.forEach((item) => item.asMsSql());
82
+ return this;
83
+ }
84
+
85
+ asOracleSql(){
86
+ this.type = SQLLang.Oracle;
87
+ let rx = new RegExp("\\?", "g");
88
+ let keys = this.parameters.keys();
89
+ this.originalWhere = this.where;
90
+ this.where = this.where.replace(rx, () => `:${keys.next().value}`);
91
+ this.includes.forEach((item) => item.asOracleSql());
92
+ return this;
93
+ }
94
+
95
+ asAnsiSql(){
96
+ this.type = SQLLang.ANSI;
97
+ this.where = this.originalWhere || this.where;
98
+ this.includes.forEach((item) => item.asAnsiSql());
99
+ return this;
100
+ }
101
+
102
+ asType(){
103
+ switch (this.type){
104
+ case SQLLang.MsSql: return this.asMsSql();
105
+ case SQLLang.ANSI:
106
+ case SQLLang.MySql:
107
+ case SQLLang.PostgreSql: return this.asAnsiSql();
108
+ case SQLLang.Oracle: return this.asOracleSql();
109
+ default: return this;
110
+ }
111
+ }
112
+
113
+ Visit(node:Token, context?:any){
114
+ this.ast = this.ast || node;
115
+ context = context || { target: "where" };
116
+
117
+ if (node){
118
+ var visitor = this[`Visit${node.type}`];
119
+ if (visitor) visitor.call(this, node, context);
120
+ else console.log(`Unhandled node type: ${node.type}`, node);
121
+ }
122
+
123
+ if (node == this.ast){
124
+ if (!this.select) this.select = `*`;
125
+ if (!this.where) this.where = "1 = 1";
126
+ if (!this.orderby) this.orderby = "1";
127
+ }
128
+ return this;
129
+ }
130
+
131
+ protected VisitODataUri(node:Token, context:any){
132
+ this.Visit(node.value.resource, context);
133
+ this.Visit(node.value.query, context);
134
+ }
135
+
136
+ protected VisitExpand(node: Token, context: any) {
137
+ node.value.items.forEach((item) => {
138
+ let expandPath = item.value.path.raw;
139
+ let visitor = this.includes.filter(v => v.navigationProperty == expandPath)[0];
140
+ if (!visitor){
141
+ visitor = new Visitor(this.options);
142
+ visitor.parameterSeed = this.parameterSeed;
143
+ this.includes.push(visitor);
144
+ }
145
+ visitor.Visit(item);
146
+ this.parameterSeed = visitor.parameterSeed;
147
+ });
148
+ }
149
+
150
+ protected VisitExpandItem(node: Token, context: any) {
151
+ this.Visit(node.value.path, context);
152
+ if (node.value.options) node.value.options.forEach((item) => this.Visit(item, context));
153
+ }
154
+
155
+ protected VisitExpandPath(node: Token, context: any) {
156
+ this.navigationProperty = node.raw;
157
+ }
158
+
159
+ protected VisitQueryOptions(node:Token, context:any){
160
+ node.value.options.forEach((option) => this.Visit(option, context));
161
+ }
162
+
163
+ protected VisitInlineCount(node:Token, context:any){
164
+ this.inlinecount = Literal.convert(node.value.value, node.value.raw);
165
+ }
166
+
167
+ protected VisitFilter(node:Token, context:any){
168
+ context.target = "where";
169
+ this.Visit(node.value, context);
170
+ if (!this.where) this.where = "1 = 1";
171
+ }
172
+
173
+ protected VisitOrderBy(node:Token, context:any){
174
+ context.target = "orderby";
175
+ node.value.items.forEach((item, i) => {
176
+ this.Visit(item, context);
177
+ if (i < node.value.items.length - 1) this.orderby += ", ";
178
+ });
179
+ }
180
+
181
+ protected VisitOrderByItem(node:Token, context:any){
182
+ this.Visit(node.value.expr, context);
183
+ this.orderby += node.value.direction > 0 ? " ASC" : " DESC";
184
+ }
185
+
186
+ protected VisitSkip(node:Token, context:any){
187
+ this.skip = +node.value.raw;
188
+ }
189
+
190
+ protected VisitTop(node:Token, context:any){
191
+ this.limit = +node.value.raw;
192
+ }
193
+
194
+ protected VisitSelect(node:Token, context:any){
195
+ context.target = "select";
196
+ node.value.items.forEach((item, i) => {
197
+ this.Visit(item, context);
198
+ if (i < node.value.items.length - 1) this.select += ", ";
199
+ });
200
+ }
201
+
202
+ protected VisitSelectItem(node:Token, context:any){
203
+ let item = node.raw.replace(/\//g, '.');
204
+ this.select += `[${item}]`;
205
+ }
206
+
207
+ protected VisitAndExpression(node:Token, context:any){
208
+ this.Visit(node.value.left, context);
209
+ this.where += " AND ";
210
+ this.Visit(node.value.right, context);
211
+ }
212
+
213
+ protected VisitOrExpression(node:Token, context:any){
214
+ this.Visit(node.value.left, context);
215
+ this.where += " OR ";
216
+ this.Visit(node.value.right, context);
217
+ }
218
+
219
+ protected VisitBoolParenExpression(node:Token, context:any){
220
+ this.where += "(";
221
+ this.Visit(node.value, context);
222
+ this.where += ")";
223
+ }
224
+
225
+ protected VisitCommonExpression(node:Token, context:any){
226
+ this.Visit(node.value, context);
227
+ }
228
+
229
+ protected VisitFirstMemberExpression(node:Token, context:any){
230
+ this.Visit(node.value, context);
231
+ }
232
+
233
+ protected VisitMemberExpression(node:Token, context:any){
234
+ this.Visit(node.value, context);
235
+ }
236
+
237
+ protected VisitPropertyPathExpression(node:Token, context:any){
238
+ if (node.value.current && node.value.next){
239
+ this.Visit(node.value.current, context);
240
+ context.identifier += ".";
241
+ this.Visit(node.value.next, context);
242
+ }else this.Visit(node.value, context);
243
+ }
244
+
245
+ protected VisitSingleNavigationExpression(node:Token, context:any){
246
+ if (node.value.current && node.value.next){
247
+ this.Visit(node.value.current, context);
248
+ this.Visit(node.value.next, context);
249
+ }else this.Visit(node.value, context);
250
+ }
251
+
252
+ protected VisitODataIdentifier(node:Token, context:any){
253
+ this[context.target] += `[${node.value.name}]`;
254
+ context.identifier = node.value.name;
255
+ }
256
+
257
+ protected VisitEqualsExpression(node:Token, context:any){
258
+ this.Visit(node.value.left, context);
259
+ this.where += " = ";
260
+ this.Visit(node.value.right, context);
261
+ if (this.options.useParameters && context.literal == null){
262
+ this.where = this.where.replace(/= \?$/, "IS NULL").replace(new RegExp(`\\? = \\[${context.identifier}\\]$`), `[${context.identifier}] IS NULL`);
263
+ }else if (context.literal == "NULL"){
264
+ this.where = this.where.replace(/= NULL$/, "IS NULL").replace(new RegExp(`NULL = \\[${context.identifier}\\]$`), `[${context.identifier}] IS NULL`);
265
+ }
266
+ }
267
+
268
+ protected VisitNotEqualsExpression(node:Token, context:any){
269
+ this.Visit(node.value.left, context);
270
+ this.where += " <> ";
271
+ this.Visit(node.value.right, context);
272
+ if (this.options.useParameters && context.literal == null){
273
+ this.where = this.where.replace(/<> \?$/, "IS NOT NULL").replace(new RegExp(`\\? <> \\[${context.identifier}\\]$`), `[${context.identifier}] IS NOT NULL`);
274
+ }else if (context.literal == "NULL"){
275
+ this.where = this.where.replace(/<> NULL$/, "IS NOT NULL").replace(new RegExp(`NULL <> \\[${context.identifier}\\]$`), `[${context.identifier}] IS NOT NULL`);
276
+ }
277
+ }
278
+
279
+ protected VisitLesserThanExpression(node:Token, context:any){
280
+ this.Visit(node.value.left, context);
281
+ this.where += " < ";
282
+ this.Visit(node.value.right, context);
283
+ }
284
+
285
+ protected VisitLesserOrEqualsExpression(node:Token, context:any){
286
+ this.Visit(node.value.left, context);
287
+ this.where += " <= ";
288
+ this.Visit(node.value.right, context);
289
+ }
290
+
291
+ protected VisitGreaterThanExpression(node:Token, context:any){
292
+ this.Visit(node.value.left, context);
293
+ this.where += " > ";
294
+ this.Visit(node.value.right, context);
295
+ }
296
+
297
+ protected VisitGreaterOrEqualsExpression(node:Token, context:any){
298
+ this.Visit(node.value.left, context);
299
+ this.where += " >= ";
300
+ this.Visit(node.value.right, context);
301
+ }
302
+
303
+ protected VisitLiteral(node:Token, context:any){
304
+ if (this.options.useParameters){
305
+ let name = `p${this.parameterSeed++}`;
306
+ let value = Literal.convert(node.value, node.raw);
307
+ context.literal = value;
308
+ this.parameters.set(name, value);
309
+ this.where += "?";
310
+ }else this.where += (context.literal = SQLLiteral.convert(node.value, node.raw));
311
+ }
312
+
313
+ protected VisitMethodCallExpression(node:Token, context:any){
314
+ var method = node.value.method;
315
+ var params = node.value.parameters || [];
316
+ switch (method){
317
+ case "contains":
318
+ this.Visit(params[0], context);
319
+ if (this.options.useParameters){
320
+ let name = `p${this.parameterSeed++}`;
321
+ let value = Literal.convert(params[1].value, params[1].raw);
322
+ this.parameters.set(name, `%${value}%`);
323
+ this.where += " like ?";
324
+ }else this.where += ` like '%${SQLLiteral.convert(params[1].value, params[1].raw).slice(1, -1)}%'`;
325
+ break;
326
+ case "endswith":
327
+ this.Visit(params[0], context);
328
+ if (this.options.useParameters){
329
+ let name = `p${this.parameterSeed++}`;
330
+ let value = Literal.convert(params[1].value, params[1].raw);
331
+ this.parameters.set(name, `%${value}`);
332
+ this.where += " like ?";
333
+ }else this.where += ` like '%${SQLLiteral.convert(params[1].value, params[1].raw).slice(1, -1)}'`;
334
+ break;
335
+ case "startswith":
336
+ this.Visit(params[0], context);
337
+ if (this.options.useParameters){
338
+ let name = `p${this.parameterSeed++}`;
339
+ let value = Literal.convert(params[1].value, params[1].raw);
340
+ this.parameters.set(name, `${value}%`);
341
+ this.where += " like ?";
342
+ }else this.where += ` like '${SQLLiteral.convert(params[1].value, params[1].raw).slice(1, -1)}%'`;
343
+ break;
344
+ case "indexof":
345
+ let fn = "";
346
+ switch (this.type) {
347
+ case SQLLang.MsSql:
348
+ fn = "CHARINDEX";
349
+ break;
350
+ case SQLLang.ANSI:
351
+ case SQLLang.MySql:
352
+ case SQLLang.PostgreSql:
353
+ default:
354
+ fn = "INSTR";
355
+ break;
356
+ }
357
+ if (fn === "CHARINDEX"){
358
+ const tmp = params[0];
359
+ params[0] = params[1];
360
+ params[1] = tmp;
361
+ }
362
+ this.where += `${fn}(`;
363
+ this.Visit(params[0], context);
364
+ this.where += ', ';
365
+ this.Visit(params[1], context);
366
+ this.where += ") - 1";
367
+ break;
368
+ case "round":
369
+ this.where += "ROUND(";
370
+ this.Visit(params[0], context);
371
+ this.where += ")";
372
+ break;
373
+ case "length":
374
+ this.where += "LEN(";
375
+ this.Visit(params[0], context);
376
+ this.where += ")";
377
+ break;
378
+ case "tolower":
379
+ this.where += "LCASE(";
380
+ this.Visit(params[0], context);
381
+ this.where += ")";
382
+ break;
383
+ case "toupper":
384
+ this.where += "UCASE(";
385
+ this.Visit(params[0], context);
386
+ this.where += ")";
387
+ break;
388
+ case "floor":
389
+ case "ceiling":
390
+ case "year":
391
+ case "month":
392
+ case "day":
393
+ case "hour":
394
+ case "minute":
395
+ case "second":
396
+ this.where += `${method.toUpperCase()}(`;
397
+ this.Visit(params[0], context);
398
+ this.where += ")";
399
+ break;
400
+ case "now":
401
+ this.where += "NOW()";
402
+ break;
403
+ case "trim":
404
+ this.where += "TRIM(' ' FROM ";
405
+ this.Visit(params[0], context);
406
+ this.where += ")";
407
+ break;
408
+ }
409
+ }
410
+
411
+ }
@@ -0,0 +1,142 @@
1
+ var createFilter = require('../lib').createFilter
2
+ var expect = require('chai').expect
3
+
4
+ describe("SQL WHERE useParameters (MS-SQL)", () => {
5
+ var f;
6
+ beforeEach(function() {
7
+ var match;
8
+ if (match = this.currentTest.title.match(/expression[^\:]*\: ?(.*)/)) {
9
+ f = createFilter(match[1], {
10
+ useParameters: true,
11
+ type: 1
12
+ });
13
+ }
14
+ });
15
+
16
+ //all numbers are referencing this:
17
+ //http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part2-url-conventions/odata-v4.0-errata02-os-part2-url-conventions-complete.html#_Toc406398116
18
+
19
+ it("expression 5.1.1.6.1: NullValue eq null", () => {
20
+ expect(f.where).to.equal("[NullValue] IS NULL")
21
+ })
22
+
23
+ it("expression 5.1.1.6.1: TrueValue eq true", () => {
24
+ expect(f.where).to.equal("[TrueValue] = @p0")
25
+ })
26
+
27
+ it("expression 5.1.1.6.1: FalseValue eq false", () => {
28
+ expect(f.where).to.equal("[FalseValue] = @p0")
29
+ })
30
+
31
+ it("expression 5.1.1.6.1: IntegerValue lt -128", () => {
32
+ expect(f.where).to.equal("[IntegerValue] < @p0")
33
+ })
34
+
35
+ it("expression 5.1.1.6.1: DecimalValue eq 34.95", () => {
36
+ expect(f.where).to.equal("[DecimalValue] = @p0")
37
+ })
38
+
39
+ it("expression 5.1.1.6.1: StringValue eq 'Say Hello,then go'", () => {
40
+ expect(f.where).to.equal("[StringValue] = @p0")
41
+ })
42
+
43
+ it("expression 5.1.1.6.1: DurationValue eq duration'P12DT23H59M59.999999999999S'", () => {
44
+ expect(f.where).to.equal("[DurationValue] = @p0")
45
+ })
46
+
47
+ it("expression 5.1.1.6.1: DateValue eq 2012-12-03", () => {
48
+ expect(f.where).to.equal("[DateValue] = @p0")
49
+ })
50
+
51
+ it("expression 5.1.1.6.1: DateTimeOffsetValue eq 2012-12-03T07:16:23Z", () => {
52
+ expect(f.where).to.equal("[DateTimeOffsetValue] = @p0")
53
+ })
54
+
55
+ it("expression 5.1.1.6.1: GuidValue eq 01234567-89ab-cdef-0123-456789abcdef", () => {
56
+ expect(f.where).to.equal("[GuidValue] = @p0")
57
+ })
58
+
59
+ it("expression 5.1.1.6.1: Int64Value eq 0", () => {
60
+ expect(f.where).to.equal("[Int64Value] = @p0")
61
+ })
62
+
63
+ it("expression 5.1.1.6.1: A eq 0.31415926535897931e1", () => {
64
+ expect(f.where).to.equal("[A] = @p0")
65
+ })
66
+
67
+ it("expression 5.1.1.1.2: A ne 1", () => {
68
+ expect(f.where).to.equal("[A] <> @p0")
69
+ })
70
+
71
+ it("expression 5.1.1.1.3: A gt 2", () => {
72
+ expect(f.where).to.equal("[A] > @p0")
73
+ })
74
+
75
+ it("expression 5.1.1.1.4: A ge 3", () => {
76
+ expect(f.where).to.equal("[A] >= @p0")
77
+ })
78
+
79
+ it("expression 5.1.1.1.5: A lt 2", () => {
80
+ expect(f.where).to.equal("[A] < @p0")
81
+ })
82
+
83
+ it("expression 5.1.1.1.6: A le 2", () => {
84
+ expect(f.where).to.equal("[A] <= @p0")
85
+ })
86
+
87
+ it("expression 5.1.1.3: (A eq 2) or (B lt 4) and ((E gt 5) or (E lt -1))", () => {
88
+ expect(f.where).to.equal("([A] = @p0) OR ([B] < @p1) AND (([E] > @p2) OR ([E] < @p3))")
89
+ })
90
+
91
+ it("expression 5.1.1.4.1: contains(A, 'BC')", () => {
92
+ expect(f.where).to.equal("[A] like @p0");
93
+ })
94
+
95
+ it("expression 5.1.1.4.2: endswith(A, 'CD')", () => {
96
+ expect(f.where).to.equal("[A] like @p0");
97
+ })
98
+
99
+ it("expression 5.1.1.4.3: startswith(A, 'CD')", () => {
100
+ expect(f.where).to.equal("[A] like @p0");
101
+ })
102
+
103
+ it("expression 5.1.1.4.4: length(A) eq 3", () => {
104
+ expect(f.where).to.equal("LEN([A]) = @p0")
105
+ })
106
+
107
+ it("expression 5.1.1.4.5: indexof(A, 'BC') eq 1", () => {
108
+ expect(f.where).to.equal("CHARINDEX(@p0, [A]) - 1 = @p1")
109
+ })
110
+
111
+ it("expression 5.1.1.4.7: tolower(A) eq 'abc'", () => {
112
+ expect(f.where).to.equal("LCASE([A]) = @p0")
113
+ })
114
+
115
+ it("expression 5.1.1.4.8: toupper(A) eq 'ABC'", () => {
116
+ expect(f.where).to.equal("UCASE([A]) = @p0")
117
+ })
118
+
119
+ it("expression 5.1.1.4.9: trim(A) eq 'abc'", () => {
120
+ expect(f.where).to.equal("TRIM(' ' FROM [A]) = @p0")
121
+ })
122
+
123
+ it("expression 5.1.1.4.11: A eq year(2016-01-01T13:00Z)", () => {
124
+ expect(f.where).to.equal("[A] = YEAR(@p0)")
125
+ })
126
+
127
+ it("expression 5.1.1.4.21: year(now())", () => {
128
+ expect(f.where).to.equal("YEAR(NOW())")
129
+ })
130
+
131
+ it("expression 5.1.1.4.25: round(A) eq 42", () => {
132
+ expect(f.where).to.equal("ROUND([A]) = @p0")
133
+ })
134
+
135
+ it("expression 5.1.1.4.26: floor(A) eq 42", () => {
136
+ expect(f.where).to.equal("FLOOR([A]) = @p0")
137
+ })
138
+
139
+ it("expression 5.1.1.4.27: ceiling(A) eq 42", () => {
140
+ expect(f.where).to.equal("CEILING([A]) = @p0")
141
+ })
142
+ })