diff --git a/src/calc2/views/help.tsx b/src/calc2/views/help.tsx index d4034725..1f38454b 100644 --- a/src/calc2/views/help.tsx +++ b/src/calc2/views/help.tsx @@ -2009,6 +2009,18 @@ export class Help extends React.Component {
This is not in the SQL standard but is a PostgreSQL extension. + + a:string REGEXP 'PATTERN'
+ a:string RLIKE 'PATTERN'
+ boolean + returns true if expression evaluating to a string a matches + the pattern given as the second operand, false otherwise. +
+ The pattern has to be given as a string literal and it can be an extended regular expression, the syntax for + which is discussed in Regular Expression Syntax. +
This might not be in the SQL standard but is supported in MySQL. + + a + b @@ -2124,6 +2136,24 @@ export class Help extends React.Component { converts the given string to lower-case + + repeat(str:string, count:number) + string + returns a string consisting of the string str repeated count times. If count is less than 1, returns an empty string. Returns null if str or count are null. + + + + replace(str:string, from_str:string, to_str:string) + string + returns the string str with all occurrences of the string from_str replaced by the string to_str. replace() performs a case-sensitive match when searching for from_str. + + + + reverse(a:string) + string + returns the given string with the order of the characters reversed. + + strlen(a:string) number @@ -2219,7 +2249,7 @@ export class Help extends React.Component { 5 - = (comparison), {'>'}=, {'>'}, {'<'}=, {'<'}, {'<'}{'>'}, !=, LIKE, ILIKE + = (comparison), {'>'}=, {'>'}, {'<'}=, {'<'}, {'<'}{'>'}, !=, LIKE, ILIKE, REGEXP, RLIKE 6 diff --git a/src/db/exec/ValueExpr.ts b/src/db/exec/ValueExpr.ts index 80099c20..6a5d18a0 100644 --- a/src/db/exec/ValueExpr.ts +++ b/src/db/exec/ValueExpr.ts @@ -447,6 +447,8 @@ export class ValueExprGeneric extends ValueExpr { return ValueExprGeneric._condition_compare(a, b, typeA, this._func); case 'like': case 'ilike': + case 'regexp': + case 'rlike': if(!this._regex){ throw new Error(`regex should have been set by check`); } @@ -598,6 +600,18 @@ export class ValueExprGeneric extends ValueExpr { this._regex = new RegExp('^' + regex_str + '$', flags); + break; + case 'regexp': + case 'rlike': + this._args[0].check(schemaA, schemaB); + if (this._args[1].getDataType() !== 'string' || this._args[1]._func !== 'constant') { + return false; + } + + // cache regex + const txt = this._args[1]._args[0]; // direct access of constant value + let regex_txt = txt; + this._regex = new RegExp(regex_txt); break; default: throw new Error('this should not happen!'); @@ -632,6 +646,30 @@ export class ValueExprGeneric extends ValueExpr { value += a; } return value; + case 'repeat': + const rep = this._args[0].evaluate(tupleA, tupleB, row, statementSession); + const count = this._args[1].evaluate(tupleA, tupleB, row, statementSession); + + if (rep === null || count === null) { + return null; + } + else { + return rep.repeat(count >= 0 ? count : 0); + } + case 'replace': + const str = this._args[0].evaluate(tupleA, tupleB, row, statementSession); + const from_str = this._args[1].evaluate(tupleA, tupleB, row, statementSession); + const to_str = this._args[2].evaluate(tupleA, tupleB, row, statementSession); + return str.replace(new RegExp(from_str, 'g'), to_str); + case 'reverse': + const r = this._args[0].evaluate(tupleA, tupleB, row, statementSession); + + if (r === null) { + return null; + } + else { + return r.split('').reverse().join(''); + } default: throw new Error('this should not happen!'); } @@ -868,7 +906,32 @@ export class ValueExprGeneric extends ValueExpr { return true; case 'lower': case 'upper': + case 'reverse': return this._checkArgsDataType(schemaA, schemaB, ['string']); + case 'replace': + return this._checkArgsDataType(schemaA, schemaB, ['string', 'string', 'string']); + case 'repeat': + //return this._checkArgsDataType(schemaA, schemaB, ['string', 'number']); + + if (this._args.length !== 2) { + throw new Error('this should not happen!'); + } + + // arguments must be of type string and number, or null + this._args[0].check(schemaA, schemaB); + const typeStr = this._args[0].getDataType(); + this._args[1].check(schemaA, schemaB); + const typeCount = this._args[1].getDataType(); + + if ( (typeStr !== 'string' && typeStr !== 'null') || + (typeCount !== 'number' && typeCount !== 'null') ) { + this.throwExecutionError(i18n.t('db.messages.exec.error-function-expects-type', { + func: 'repeat()', + expected: ['string', 'number'], + given: [typeStr, typeCount], + })); + } + break; case 'concat': if (this._args.length === 0) { throw new Error('this should not happen!'); @@ -1003,6 +1066,8 @@ export class ValueExprGeneric extends ValueExpr { case 'concat': case 'upper': case 'lower': + case 'replace': + case 'reverse': case 'date': return printFunction.call(this, _func.toUpperCase()); case 'strlen': @@ -1034,6 +1099,8 @@ export class ValueExprGeneric extends ValueExpr { case 'xor': case 'like': case 'ilike': + case 'regexp': + case 'rlike': case '=': return binary.call(this, _func); diff --git a/src/db/parser/grammar_ra.d.ts b/src/db/parser/grammar_ra.d.ts index 5aa1b7df..e0bb5b61 100644 --- a/src/db/parser/grammar_ra.d.ts +++ b/src/db/parser/grammar_ra.d.ts @@ -340,6 +340,8 @@ declare module relalgAst { | 'and' | 'like' | 'ilike' + | 'regexp' + | 'rlike' | 'add' | 'sub' | 'mul' @@ -353,6 +355,9 @@ declare module relalgAst { | 'subdate' | 'upper' | 'lower' + | 'repeat' + | 'replace' + | 'reverse' | 'strlen' | 'abs' | 'floor' diff --git a/src/db/parser/grammar_ra.pegjs b/src/db/parser/grammar_ra.pegjs index f14033c2..dfc30661 100644 --- a/src/db/parser/grammar_ra.pegjs +++ b/src/db/parser/grammar_ra.pegjs @@ -1419,7 +1419,7 @@ expr_rest_boolean_comparison codeInfo: getCodeInfo() }; } -/ _ o:('like'i / 'ilike'i) _ right:valueExprConstants +/ _ o:('like'i / 'ilike'i / 'regexp'i / 'rlike'i) _ right:valueExprConstants { if(right.datatype !== 'string'){ error(t('db.messages.parser.error-valueexpr-like-operand-no-string')); @@ -1505,6 +1505,7 @@ valueExprFunctionsNary = func:( ('coalesce'i { return ['coalesce', 'null']; }) / ('concat'i { return ['concat', 'string']; }) + / ('replace'i { return ['replace', 'string']; }) ) _ '(' _ arg0:valueExpr _ argn:(',' _ valueExpr _ )* ')' { @@ -1532,6 +1533,7 @@ valueExprFunctionsBinary / ('sub'i { return ['sub', 'number']; }) / ('mul'i { return ['mul', 'number']; }) / ('div'i { return ['div', 'number']; }) + / ('repeat'i { return ['repeat', 'string']; }) ) _ '(' _ arg0:valueExpr _ ',' _ arg1:valueExpr _ ')' { @@ -1551,6 +1553,7 @@ valueExprFunctionsUnary / ('ucase'i { return ['upper', 'string']; }) / ('lower'i { return ['lower', 'string']; }) / ('lcase'i { return ['lower', 'string']; }) + / ('reverse'i { return ['reverse', 'string']; }) / ('length'i { return ['strlen', 'number']; }) / ('abs'i { return ['abs', 'number']; }) / ('floor'i { return ['floor', 'number']; }) @@ -1705,7 +1708,7 @@ reference: https://dev.mysql.com/doc/refman/5.7/en/operator-precedence.html 2: - (unary minus) 3: *, /, % 4: -, + -5: = (comparison), >=, >, <=, <, <>, !=, IS, LIKE +5: = (comparison), >=, >, <=, <, <>, !=, IS, LIKE, REGEXP, RLIKE 6: CASE, WHEN, THEN, ELSE 7: AND 8: XOR diff --git a/src/db/parser/grammar_sql.pegjs b/src/db/parser/grammar_sql.pegjs index 239d3900..9f43664a 100644 --- a/src/db/parser/grammar_sql.pegjs +++ b/src/db/parser/grammar_sql.pegjs @@ -1252,7 +1252,7 @@ expr_rest_boolean_comparison codeInfo: getCodeInfo() }; } -/ _ o:('like'i / 'ilike'i) _ right:valueExprConstants +/ _ o:('like'i / 'ilike'i / 'regexp'i / 'rlike'i) _ right:valueExprConstants { if(right.datatype !== 'string'){ error(t('db.messages.parser.error-valueexpr-like-operand-no-string')); @@ -1338,6 +1338,7 @@ valueExprFunctionsNary = func:( ('coalesce'i { return ['coalesce', 'null']; }) / ('concat'i { return ['concat', 'string']; }) + / ('replace'i { return ['replace', 'string']; }) ) _ '(' _ arg0:valueExpr _ argn:(',' _ valueExpr _ )* ')' { @@ -1365,6 +1366,7 @@ valueExprFunctionsBinary / ('sub'i { return ['sub', 'number']; }) / ('mul'i { return ['mul', 'number']; }) / ('div'i { return ['div', 'number']; }) + / ('repeat'i { return ['repeat', 'string']; }) ) _ '(' _ arg0:valueExpr _ ',' _ arg1:valueExpr _ ')' { @@ -1384,6 +1386,7 @@ valueExprFunctionsUnary / ('ucase'i { return ['upper', 'string']; }) / ('lower'i { return ['lower', 'string']; }) / ('lcase'i { return ['lower', 'string']; }) + / ('reverse'i { return ['reverse', 'string']; }) / ('length'i { return ['strlen', 'number']; }) / ('abs'i { return ['abs', 'number']; }) / ('floor'i { return ['floor', 'number']; }) @@ -1540,7 +1543,7 @@ reference: https://dev.mysql.com/doc/refman/5.7/en/operator-precedence.html 2: - (unary minus) 3: *, /, % 4: -, + -5: = (comparison), >=, >, <=, <, <>, !=, IS, LIKE +5: = (comparison), >=, >, <=, <, <>, !=, IS, LIKE, REGEXP 6: CASE, WHEN, THEN, ELSE 7: AND 8: XOR diff --git a/src/db/tests/translate_tests_ra.ts b/src/db/tests/translate_tests_ra.ts index db136603..1202114c 100644 --- a/src/db/tests/translate_tests_ra.ts +++ b/src/db/tests/translate_tests_ra.ts @@ -1100,6 +1100,69 @@ QUnit.test('pi with eval: upper()', function (assert) { assert.deepEqual(result, reference); }); +QUnit.test('pi with eval: lower()', function (assert) { + const relations = getTestRelations(); + const result = exec_ra(" sigma y < 'd' (pi lower(x)->y (pi upper(S.b)->x S)) ", relations).getResult(); + result.eliminateDuplicateRows(); + + const reference = exec_ra(` + { + y:string + a + b + c + }`, {}).getResult(); + + assert.deepEqual(result, reference); +}); + +QUnit.test('pi with eval: repeat()', function (assert) { + const relations = getTestRelations(); + const result = exec_ra(" pi repeat(b, 3)->x (R) ", relations).getResult(); + result.eliminateDuplicateRows(); + + const reference = exec_ra('{x:string\n' + + 'aaa\n' + + 'ccc\n' + + 'ddd\n' + + 'eee\n' + + '}', {}).getResult(); + + assert.deepEqual(result, reference); +}); + +QUnit.test('pi with eval: replace()', function (assert) { + const relations = getTestRelations(); + const result = exec_ra(" pi replace(x, 'c', 'C')->y (pi concat(a, b, c)->x (R)) ", relations).getResult(); + result.eliminateDuplicateRows(); + + const reference = exec_ra('{y:string\n' + + '1ad\n' + + '3CC\n' + + '4df\n' + + '5db\n' + + '6ef\n' + + '}', {}).getResult(); + + assert.deepEqual(result, reference); +}); + +QUnit.test('pi with eval: reverse()', function (assert) { + const relations = getTestRelations(); + const result = exec_ra(" pi reverse(x)->y (pi concat(a, b, c)->x (R)) ", relations).getResult(); + result.eliminateDuplicateRows(); + + const reference = exec_ra('{y:string\n' + + 'da1\n' + + 'cc3\n' + + 'fd4\n' + + 'bd5\n' + + 'fe6\n' + + '}', {}).getResult(); + + assert.deepEqual(result, reference); +}); + QUnit.test('pi with eval: add()', function (assert) { const relations = getTestRelations(); const result = exec_ra(' pi a, add(a, a) ->x R ', relations).getResult(); @@ -1222,6 +1285,26 @@ QUnit.test('test like operator', function (assert) { assert.deepEqual(result, reference); }); +QUnit.test('test regexp operator', function (assert) { + const result = exec_ra(`pi x, x regexp '^(a|e)'->starts_a_or_e, x regexp '(a|e)$'->ends_a_or_e, x rlike '(a|e)'->has_a_or_e { + x + + abb + bba + bab + ebe + }`, {}).getResult(); + + const reference = exec_ra(`{ + x, starts_a_or_e, ends_a_or_e, has_a_or_e + + abb, true, false, true + bba, false, true, true + bab, false, false, true + ebe, true, true, true + }`, {}).getResult(); + assert.deepEqual(result, reference); +}); QUnit.test('groupby textgen', function (assert) { const ast = relalgjs.parseRelalg(`gamma a; sum(b)->c ({a, b