Skip to content

Commit

Permalink
Merge pull request #179 from rlaiola/stringexp
Browse files Browse the repository at this point in the history
Support other string functions and operators
  • Loading branch information
evazangerle authored Oct 10, 2024
2 parents 4f173bb + d766db3 commit 477ad7a
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 5 deletions.
32 changes: 31 additions & 1 deletion src/calc2/views/help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2009,6 +2009,18 @@ export class Help extends React.Component<Props> {
<br />This is not in the SQL standard but is a PostgreSQL extension.
</td>
</tr>
<tr>
<td><code>a:string REGEXP 'PATTERN'<br />
a:string RLIKE 'PATTERN'</code></td>
<td>boolean</td>
<td>returns true if expression evaluating to a string <code>a</code> matches
the pattern given as the second operand, false otherwise.
<br />
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 <a href="https://dev.mysql.com/doc/refman/8.0/en/regexp.html#regexp-syntax">Regular Expression Syntax</a>.
<br />This might not be in the SQL standard but is supported in MySQL.
</td>
</tr>
<tr>
<td>
<code>a + b
Expand Down Expand Up @@ -2124,6 +2136,24 @@ export class Help extends React.Component<Props> {
<td>converts the given string to lower-case</td>
</tr>

<tr>
<td><code>repeat(str:string, count:number)</code></td>
<td>string</td>
<td>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.</td>
</tr>

<tr>
<td><code>replace(str:string, from_str:string, to_str:string)</code></td>
<td>string</td>
<td>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.</td>
</tr>

<tr>
<td><code>reverse(a:string)</code></td>
<td>string</td>
<td>returns the given string with the order of the characters reversed.</td>
</tr>

<tr>
<td><code>strlen(a:string)</code></td>
<td>number</td>
Expand Down Expand Up @@ -2219,7 +2249,7 @@ export class Help extends React.Component<Props> {
</tr>
<tr>
<td>5</td>
<td>= (comparison), {'>'}=, {'>'}, {'<'}=, {'<'}, {'<'}{'>'}, !=, LIKE, ILIKE</td>
<td>= (comparison), {'>'}=, {'>'}, {'<'}=, {'<'}, {'<'}{'>'}, !=, LIKE, ILIKE, REGEXP, RLIKE</td>
</tr>
<tr>
<td>6</td>
Expand Down
67 changes: 67 additions & 0 deletions src/db/exec/ValueExpr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
Expand Down Expand Up @@ -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!');
Expand Down Expand Up @@ -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!');
}
Expand Down Expand Up @@ -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!');
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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);

Expand Down
5 changes: 5 additions & 0 deletions src/db/parser/grammar_ra.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ declare module relalgAst {
| 'and'
| 'like'
| 'ilike'
| 'regexp'
| 'rlike'
| 'add'
| 'sub'
| 'mul'
Expand All @@ -353,6 +355,9 @@ declare module relalgAst {
| 'subdate'
| 'upper'
| 'lower'
| 'repeat'
| 'replace'
| 'reverse'
| 'strlen'
| 'abs'
| 'floor'
Expand Down
7 changes: 5 additions & 2 deletions src/db/parser/grammar_ra.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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 _ )* ')'
{
Expand Down Expand Up @@ -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 _ ')'
{
Expand All @@ -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']; })
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/db/parser/grammar_sql.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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 _ )* ')'
{
Expand Down Expand Up @@ -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 _ ')'
{
Expand All @@ -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']; })
Expand Down Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions src/db/tests/translate_tests_ra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 477ad7a

Please sign in to comment.