Skip to content

Commit a784cfc

Browse files
authored
Merge pull request #30 from microsoft/new_functions
New functions
2 parents 97d0972 + d5d03da commit a784cfc

File tree

8 files changed

+224
-0
lines changed

8 files changed

+224
-0
lines changed

flowquery-py/src/parsing/functions/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
# Built-in functions
3838
from .sum import Sum
3939
from .to_json import ToJson
40+
from .to_lower import ToLower
41+
from .to_string import ToString
4042
from .type_ import Type
4143
from .value_holder import ValueHolder
4244

@@ -74,6 +76,8 @@
7476
"StringDistance",
7577
"Stringify",
7678
"ToJson",
79+
"ToLower",
80+
"ToString",
7781
"Type",
7882
"Functions",
7983
"Schema",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""ToLower function."""
2+
3+
from typing import Any
4+
5+
from .function import Function
6+
from .function_metadata import FunctionDef
7+
8+
9+
@FunctionDef({
10+
"description": "Converts a string to lowercase",
11+
"category": "scalar",
12+
"parameters": [
13+
{"name": "text", "description": "String to convert to lowercase", "type": "string"}
14+
],
15+
"output": {"description": "Lowercase string", "type": "string", "example": "hello world"},
16+
"examples": [
17+
"WITH 'Hello World' AS s RETURN toLower(s)",
18+
"WITH 'FOO' AS s RETURN toLower(s)"
19+
]
20+
})
21+
class ToLower(Function):
22+
"""ToLower function.
23+
24+
Converts a string to lowercase.
25+
"""
26+
27+
def __init__(self) -> None:
28+
super().__init__("tolower")
29+
self._expected_parameter_count = 1
30+
31+
def value(self) -> Any:
32+
val = self.get_children()[0].value()
33+
if not isinstance(val, str):
34+
raise ValueError("Invalid argument for toLower function: expected a string")
35+
return val.lower()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""ToString function."""
2+
3+
import json
4+
from typing import Any
5+
6+
from .function import Function
7+
from .function_metadata import FunctionDef
8+
9+
10+
@FunctionDef({
11+
"description": "Converts a value to its string representation",
12+
"category": "scalar",
13+
"parameters": [
14+
{"name": "value", "description": "Value to convert to a string", "type": "any"}
15+
],
16+
"output": {"description": "String representation of the value", "type": "string", "example": "42"},
17+
"examples": [
18+
"WITH 42 AS n RETURN toString(n)",
19+
"WITH true AS b RETURN toString(b)",
20+
"WITH [1, 2, 3] AS arr RETURN toString(arr)"
21+
]
22+
})
23+
class ToString(Function):
24+
"""ToString function.
25+
26+
Converts a value to its string representation.
27+
"""
28+
29+
def __init__(self) -> None:
30+
super().__init__("tostring")
31+
self._expected_parameter_count = 1
32+
33+
def value(self) -> Any:
34+
val = self.get_children()[0].value()
35+
if val is None:
36+
return "null"
37+
if isinstance(val, bool):
38+
return str(val).lower()
39+
if isinstance(val, (dict, list)):
40+
return json.dumps(val)
41+
return str(val)

flowquery-py/tests/compute/test_runner.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,51 @@ async def test_stringify_function(self):
636636
assert len(results) == 1
637637
assert results[0] == {"stringify": '{\n "a": 1,\n "b": 2\n}'}
638638

639+
@pytest.mark.asyncio
640+
async def test_to_string_function_with_number(self):
641+
"""Test toString function with a number."""
642+
runner = Runner("RETURN toString(42) as result")
643+
await runner.run()
644+
results = runner.results
645+
assert len(results) == 1
646+
assert results[0] == {"result": "42"}
647+
648+
@pytest.mark.asyncio
649+
async def test_to_string_function_with_boolean(self):
650+
"""Test toString function with a boolean."""
651+
runner = Runner("RETURN toString(true) as result")
652+
await runner.run()
653+
results = runner.results
654+
assert len(results) == 1
655+
assert results[0] == {"result": "true"}
656+
657+
@pytest.mark.asyncio
658+
async def test_to_string_function_with_object(self):
659+
"""Test toString function with an object."""
660+
runner = Runner("RETURN toString({a: 1}) as result")
661+
await runner.run()
662+
results = runner.results
663+
assert len(results) == 1
664+
assert results[0] == {"result": '{"a": 1}'}
665+
666+
@pytest.mark.asyncio
667+
async def test_to_lower_function(self):
668+
"""Test toLower function."""
669+
runner = Runner('RETURN toLower("Hello World") as result')
670+
await runner.run()
671+
results = runner.results
672+
assert len(results) == 1
673+
assert results[0] == {"result": "hello world"}
674+
675+
@pytest.mark.asyncio
676+
async def test_to_lower_function_with_all_uppercase(self):
677+
"""Test toLower function with all uppercase."""
678+
runner = Runner('RETURN toLower("FOO BAR") as result')
679+
await runner.run()
680+
results = runner.results
681+
assert len(results) == 1
682+
assert results[0] == {"result": "foo bar"}
683+
639684
@pytest.mark.asyncio
640685
async def test_associative_array_with_key_which_is_keyword(self):
641686
"""Test associative array with key which is keyword."""

src/parsing/functions/function_factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import "./stringify";
2727
// Import built-in functions to ensure their @FunctionDef decorators run
2828
import "./sum";
2929
import "./to_json";
30+
import "./to_lower";
31+
import "./to_string";
3032
import "./type";
3133

3234
// Re-export AsyncDataProvider for backwards compatibility

src/parsing/functions/to_lower.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Function from "./function";
2+
import { FunctionDef } from "./function_metadata";
3+
4+
@FunctionDef({
5+
description: "Converts a string to lowercase",
6+
category: "scalar",
7+
parameters: [{ name: "text", description: "String to convert to lowercase", type: "string" }],
8+
output: { description: "Lowercase string", type: "string", example: "hello world" },
9+
examples: ["WITH 'Hello World' AS s RETURN toLower(s)", "WITH 'FOO' AS s RETURN toLower(s)"],
10+
})
11+
class ToLower extends Function {
12+
constructor() {
13+
super("tolower");
14+
this._expectedParameterCount = 1;
15+
}
16+
public value(): any {
17+
const val = this.getChildren()[0].value();
18+
if (typeof val !== "string") {
19+
throw new Error("Invalid argument for toLower function: expected a string");
20+
}
21+
return val.toLowerCase();
22+
}
23+
}
24+
25+
export default ToLower;

src/parsing/functions/to_string.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Function from "./function";
2+
import { FunctionDef } from "./function_metadata";
3+
4+
@FunctionDef({
5+
description: "Converts a value to its string representation",
6+
category: "scalar",
7+
parameters: [{ name: "value", description: "Value to convert to a string", type: "any" }],
8+
output: { description: "String representation of the value", type: "string", example: "42" },
9+
examples: [
10+
"WITH 42 AS n RETURN toString(n)",
11+
"WITH true AS b RETURN toString(b)",
12+
"WITH [1, 2, 3] AS arr RETURN toString(arr)",
13+
],
14+
})
15+
class ToString extends Function {
16+
constructor() {
17+
super("tostring");
18+
this._expectedParameterCount = 1;
19+
}
20+
public value(): any {
21+
const val = this.getChildren()[0].value();
22+
if (val === null || val === undefined) {
23+
return String(val);
24+
}
25+
if (typeof val === "object") {
26+
return JSON.stringify(val);
27+
}
28+
return String(val);
29+
}
30+
}
31+
32+
export default ToString;

tests/compute/runner.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,46 @@ test("Test stringify function", async () => {
573573
});
574574
});
575575

576+
test("Test toString function with number", async () => {
577+
const runner = new Runner("RETURN toString(42) as result");
578+
await runner.run();
579+
const results = runner.results;
580+
expect(results.length).toBe(1);
581+
expect(results[0]).toEqual({ result: "42" });
582+
});
583+
584+
test("Test toString function with boolean", async () => {
585+
const runner = new Runner("RETURN toString(true) as result");
586+
await runner.run();
587+
const results = runner.results;
588+
expect(results.length).toBe(1);
589+
expect(results[0]).toEqual({ result: "true" });
590+
});
591+
592+
test("Test toString function with object", async () => {
593+
const runner = new Runner("RETURN toString({a: 1}) as result");
594+
await runner.run();
595+
const results = runner.results;
596+
expect(results.length).toBe(1);
597+
expect(results[0]).toEqual({ result: '{"a":1}' });
598+
});
599+
600+
test("Test toLower function", async () => {
601+
const runner = new Runner('RETURN toLower("Hello World") as result');
602+
await runner.run();
603+
const results = runner.results;
604+
expect(results.length).toBe(1);
605+
expect(results[0]).toEqual({ result: "hello world" });
606+
});
607+
608+
test("Test toLower function with all uppercase", async () => {
609+
const runner = new Runner('RETURN toLower("FOO BAR") as result');
610+
await runner.run();
611+
const results = runner.results;
612+
expect(results.length).toBe(1);
613+
expect(results[0]).toEqual({ result: "foo bar" });
614+
});
615+
576616
test("Test associative array with key which is keyword", async () => {
577617
const runner = new Runner("RETURN {return: 1} as aa");
578618
await runner.run();

0 commit comments

Comments
 (0)