Repository: bodo-ai/PyDough

Issue:
Add SIGN function in PyDough
Goal: add a `SIGN` function to PyDough with the following functionality, which should be tested e2e (see `test_pipeline.py` for examples):

- `SIGN(X)` is equivalent to the python string expression `0 if X == 0 else (1 if X > 0 else -1)`, or the SQL expression `CASE WHEN X == 0 THEN 0 WHEN X > 0 THEN 1 WHEN X < 0 THEN -1 END`

This feature need to be documented in the [function list documentation](https://github.com/bodo-ai/PyDough/blob/main/documentation/functions.md)

Discussion:
review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/bodo-ai/PyDough/pull/288"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
vineetg3: PR #284 included in this PR.

Patch:
diff --git a/demos/notebooks/5_what_if.ipynb b/demos/notebooks/5_what_if.ipynb
index 1581178..fd9305b 100644
--- a/demos/notebooks/5_what_if.ipynb
+++ b/demos/notebooks/5_what_if.ipynb
@@ -113,7 +113,7 @@
     "\n",
     "```python\n",
     "revenue_def = extended_price*(1-discount)\n",
-    "orders(total_line_price=SUM(lines(line_price=revenue_def).line_price)).lines(\n",
+    "orders.CALCULATE(total_line_price=SUM(lines.CALCULATE(line_price=revenue_def).line_price)).lines.CALCULATE(\n",
     "    revenue_ratio=revenue_def / total_line_price, \n",
     "    order_key=order_key, \n",
     "    line_number=line_number\n",
@@ -200,7 +200,7 @@
    "source": [
     "%%pydough\n",
     "\n",
-    "total_revenue = SUM(lines.CALCULATE(line_renvenue=revenue_def).line_renvenue)\n",
+    "total_revenue = SUM(lines.CALCULATE(line_revenue=revenue_def).line_revenue)\n",
     "total_revenue"
    ]
   },
@@ -296,7 +296,7 @@
    "id": "12ca1882-91f3-4da2-aa01-94212a67c750",
    "metadata": {},
    "source": [
-    "Now we will build our final question by moving from the context of the orders to the context of orders and lines together. Since Orders -> Lines is a Many to One relationship, this places us in the context of lines with some additional information.\n",
+    "Now we will build our final question by moving from the context of the orders to the context of orders and lines together. Since Orders -> Lines is a One to Many relationship, this places us in the context of lines with some additional information.\n",
     "\n",
     "For actually fully solving our prior question, we will compute the ratio and then select 5 smallest ratio value, breaking ties with a combination of the order number and line number."
    ]
diff --git a/documentation/functions.md b/documentation/functions.md
index 58771d8..a0fe819 100644
--- a/documentation/functions.md
+++ b/documentation/functions.md
@@ -23,6 +23,8 @@ Below is the list of every function/operator currently supported in PyDough as a
    * [JOIN_STRINGS](#join_strings)
    * [LPAD](#lpad)
    * [RPAD](#rpad)
+   * [FIND](#find)
+   * [STRIP](#strip)
 - [Datetime Functions](#datetime-functions)
    * [DATETIME](#datetime)
    * [YEAR](#year)
@@ -45,6 +47,7 @@ Below is the list of every function/operator currently supported in PyDough as a
    * [ROUND](#round)
    * [POWER](#power)
    * [SQRT](#sqrt)
+   * [SIGN](#sign)
 - [Aggregation Functions](#aggregation-functions)
    * [SUM](#sum)
    * [AVG](#avg)
@@ -332,6 +335,47 @@ Here are examples on how it pads on string literals:
 | `RPAD("123", 2, "0")` | `"12"` |
 | `RPAD("123", 0, "0")` | `""` |
 
+<!-- TOC --><a name="find"></a>
+
+### FIND
+
+The `FIND` function returns the position (0-indexed) of the first occurrence of a substring within a string, or -1 if the substring is not found. The first argument is the string to search within, and the second argument is the substring to search for.
+
+```py
+Customers.WHERE(name == "Alex Rodriguez")
+         .CALCULATE(
+            idx_Alex = FIND(name, "Alex"), # 0
+            idx_Rodriguez = FIND(name, "Rodriguez"), # 5
+            idx_bob = FIND(name, "bob"), # -1
+            idx_e = FIND(name, "e"), # 2
+            idx_space = FIND(name, " "), # 4
+            idx_of_R = FIND(name, "R"), # 5
+            idx_of_Alex_Rodriguez = FIND(name, "Alex Rodriguez"), # 0
+)
+```
+
+<!-- TOC --><a name="strip"></a>
+
+### STRIP
+
+The `STRIP` function returns the first argument with all leading and trailing whitespace removed, including newlines, tabs, and spaces.
+If the second argument is provided, it is used as the set of characters to remove from the leading and trailing ends of the first argument.
+It continues removing characters until it encounters a character that is not in the set.
+This function is equivalent to python's `str.strip()` method.
+Note: This function is case-sensitive.
+
+```py
+Customers.CALCULATE(stripped_name = STRIP(name)) # removes all leading and trailing whitespace
+Customers.CALCULATE(stripped_name = STRIP(name, "aeiou")) # removes all leading and trailing vowels
+```
+
+| **Input String (X)**       | **STRIP(X, Y)**                    | **Result**          |
+|-----------------------------|---------------------------------------|---------------------|
+| `'abcXYZcba'`              | `STRIP('abcXYZcba','abc')`        | `'XYZ'`            |
+| `'$$Hello$$'`              | `STRIP('$$Hello$$','$$')`          | `'Hello'`          |
+| `'---Test-String---'`      | `STRIP('---Test-String---','-')`  | `'Test-String'`    |
+| `'123456Hello654321'`      | `STRIP('123456Hello654321','123456')` | `'Hello'`         |
+
 <!-- TOC --><a name="datetime-functions"></a>
 
 ## Datetime Functions
@@ -610,6 +654,16 @@ The `SQRT` function takes the square root of its input. It's equivalent to `POWE
 Parts.CALCULATE(sqrt_price = SQRT(retail_price))
 ```
 
+<!-- TOC --><a name="sign"></a>
+
+### SIGN
+
+The `SIGN` function returns the sign of its input. It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
+
+```py
+Suppliers.CALCULATE(sign_of_acctbal = SIGN(account_balance))
+```
+
 <!-- TOC --><a name="aggregation-functions"></a>
 
 ## Aggregation Functions
diff --git a/pydough/pydough_operators/__init__.py b/pydough/pydough_operators/__init__.py
index 9676b59..9b2dbc1 100644
--- a/pydough/pydough_operators/__init__.py
+++ b/pydough/pydough_operators/__init__.py
@@ -27,6 +27,7 @@ __all__ = [
     "ExpressionFunctionOperator",
     "ExpressionTypeDeducer",
     "ExpressionWindowOperator",
+    "FIND",
     "GEQ",
     "GRT",
     "HAS",
@@ -63,9 +64,11 @@ __all__ = [
     "RPAD",
     "RequireNumArgs",
     "SECOND",
+    "SIGN",
     "SLICE",
     "SQRT",
     "STARTSWITH",
+    "STRIP",
     "SUB",
     "SUM",
     "SelectArgumentType",
@@ -94,6 +97,7 @@ from .expression_operators import (
     DIV,
     ENDSWITH,
     EQU,
+    FIND,
     GEQ,
     GRT,
     HAS,
@@ -127,9 +131,11 @@ from .expression_operators import (
     ROUND,
     RPAD,
     SECOND,
+    SIGN,
     SLICE,
     SQRT,
     STARTSWITH,
+    STRIP,
     SUB,
     SUM,
     UPPER,
diff --git a/pydough/pydough_operators/expression_operators/README.md b/pydough/pydough_operators/expression_operators/README.md
index 8510506..05d0b72 100644
--- a/pydough/pydough_operators/expression_operators/README.md
+++ b/pydough/pydough_operators/expression_operators/README.md
@@ -77,7 +77,8 @@ These functions must be called on singular data as a function.
 - `JOIN_STRINGS`: equivalent to the Python string join method, where the first argument is used as a delimiter to concatenate the remaining arguments.
 - `LPAD`: pads the first argument with the second argument to the left until the first argument is equal in length to the third argument.
 - `RPAD`: pads the first argument with the second argument to the right until the first argument is equal in length to the third argument.
-
+- `FIND`: returns the index(0-indexed) of the first occurrence of the second argument within the first argument, or -1 if the second argument is not found.
+- `STRIP`: returns the first argument with all leading and trailing whitespace removed, including newlines, tabs, and spaces. If the second argument is provided, it is used as the set of characters to remove from the leading and trailing ends of the first argument.
 ##### Datetime Functions
 
 - `DATETIME`: constructs a new datetime, either from an existing one or the current datetime, and augments it by adding/subtracting intervals of time and/or truncating it to various units.
@@ -110,6 +111,7 @@ These functions must be called on singular data as a function.
 - `ROUND`: rounds the first argument to a number of digits equal to the second argument. If second argument is not provided, the first argument is rounded to 0 decimal places.
 - `POWER`: exponentiates the first argument to the power of second argument.
 - `SQRT`: returns the square root of the input. 
+- `SIGN`: returns the sign of the input. It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
 
 #### Aggregation Functions
 
diff --git a/pydough/pydough_operators/expression_operators/__init__.py b/pydough/pydough_operators/expression_operators/__init__.py
index b333a59..72002fc 100644
--- a/pydough/pydough_operators/expression_operators/__init__.py
+++ b/pydough/pydough_operators/expression_operators/__init__.py
@@ -24,6 +24,7 @@ __all__ = [
     "EQU",
     "ExpressionFunctionOperator",
     "ExpressionWindowOperator",
+    "FIND",
     "GEQ",
     "GRT",
     "HAS",
@@ -58,9 +59,11 @@ __all__ = [
     "ROUND",
     "RPAD",
     "SECOND",
+    "SIGN",
     "SLICE",
     "SQRT",
     "STARTSWITH",
+    "STRIP",
     "SUB",
     "SUM",
     "UPPER",
@@ -88,6 +91,7 @@ from .registered_expression_operators import (
     DIV,
     ENDSWITH,
     EQU,
+    FIND,
     GEQ,
     GRT,
     HAS,
@@ -121,9 +125,11 @@ from .registered_expression_operators import (
     ROUND,
     RPAD,
     SECOND,
+    SIGN,
     SLICE,
     SQRT,
     STARTSWITH,
+    STRIP,
     SUB,
     SUM,
     UPPER,
diff --git a/pydough/pydough_operators/expression_operators/registered_expression_operators.py b/pydough/pydough_operators/expression_operators/registered_expression_operators.py
index b6d54aa..8f797ec 100644
--- a/pydough/pydough_operators/expression_operators/registered_expression_operators.py
+++ b/pydough/pydough_operators/expression_operators/registered_expression_operators.py
@@ -19,6 +19,7 @@ __all__ = [
     "DIV",
     "ENDSWITH",
     "EQU",
+    "FIND",
     "GEQ",
     "GRT",
     "HAS",
@@ -52,9 +53,11 @@ __all__ = [
     "ROUND",
     "RPAD",
     "SECOND",
+    "SIGN",
     "SLICE",
     "SQRT",
     "STARTSWITH",
+    "STRIP",
     "SUB",
     "SUM",
     "UPPER",
@@ -106,6 +109,9 @@ UPPER = ExpressionFunctionOperator(
 STARTSWITH = ExpressionFunctionOperator(
     "STARTSWITH", False, RequireNumArgs(2), ConstantType(BooleanType())
 )
+STRIP = ExpressionFunctionOperator(
+    "STRIP", False, RequireArgRange(1, 2), SelectArgumentType(0)
+)
 ENDSWITH = ExpressionFunctionOperator(
     "ENDSWITH", False, RequireNumArgs(2), ConstantType(BooleanType())
 )
@@ -125,6 +131,9 @@ POWER = ExpressionFunctionOperator(
 SQRT = ExpressionFunctionOperator(
     "SQRT", False, RequireNumArgs(1), ConstantType(Float64Type())
 )
+SIGN = ExpressionFunctionOperator(
+    "SIGN", False, RequireNumArgs(1), ConstantType(Int64Type())
+)
 COUNT = ExpressionFunctionOperator("COUNT", True, AllowAny(), ConstantType(Int64Type()))
 HAS = ExpressionFunctionOperator("HAS", True, AllowAny(), ConstantType(BooleanType()))
 HASNOT = ExpressionFunctionOperator(
@@ -169,6 +178,9 @@ LPAD = ExpressionFunctionOperator(
 RPAD = ExpressionFunctionOperator(
     "RPAD", False, RequireNumArgs(3), SelectArgumentType(0)
 )
+FIND = ExpressionFunctionOperator(
+    "FIND", False, RequireNumArgs(2), ConstantType(Int64Type())
+)
 NOT = ExpressionFunctionOperator(
     "NOT", False, RequireNumArgs(1), ConstantType(BooleanType())
 )
diff --git a/pydough/pydough_operators/type_inference/README.md b/pydough/pydough_operators/type_inference/README.md
index 80d1bfc..96b4977 100644
--- a/pydough/pydough_operators/type_inference/README.md
+++ b/pydough/pydough_operators/type_inference/README.md
@@ -17,6 +17,7 @@ This subdirectory of the PyDough operators directory deals with utilities used f
 - `AllowAny`: Type verifier implementation class that always accepts, no matter the arguments.
 - `RequireNumArgs`: Type verifier implementation class that requires an exact number of arguments.
 - `RequireMinArgs`: Type verifier implementation class that requires a minimum number of arguments.
+- `RequireArgRange`: Type verifier implementation class that requires the number of arguments to be between a minimum and maximum number inclusive on both ends.
 
 ## Usage
 
diff --git a/pydough/sqlglot/transform_bindings.py b/pydough/sqlglot/transform_bindings.py
index 6fe49d6..af453e8 100644
--- a/pydough/sqlglot/transform_bindings.py
+++ b/pydough/sqlglot/transform_bindings.py
@@ -1538,6 +1538,106 @@ def convert_round(
     )
 
 
+def convert_sign(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for getting the sign of the operand.
+    It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+    Returns:
+        The SQLGlot expression matching the functionality of `SIGN(X)`.
+    """
+    assert len(sql_glot_args) == 1
+    arg: SQLGlotExpression = sql_glot_args[0]
+    zero_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(0)
+    one_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(1)
+    minus_one_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(-1)
+    answer: SQLGlotExpression = convert_iff_case(
+        None,
+        [
+            sqlglot_expressions.EQ(this=arg, expression=zero_glot),
+            zero_glot,
+            apply_parens(
+                convert_iff_case(
+                    None,
+                    [
+                        sqlglot_expressions.LT(this=arg, expression=zero_glot),
+                        minus_one_glot,
+                        one_glot,
+                    ],
+                ),
+            ),
+        ],
+    )
+    return answer
+
+
+def convert_strip(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for removing all leading and trailing whitespace from a string.
+    If a second argument is provided, it is used as the set of characters
+    to remove from the leading and trailing ends of the first argument.
+
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+
+    Returns:
+        The SQLGlot expression matching the functionality of `STRIP(X, Y)`.
+        In Python, this is equivalent to `X.strip(Y)`.
+    """
+    assert 1 <= len(sql_glot_args) <= 2
+    to_strip: SQLGlotExpression = sql_glot_args[0]
+    strip_char_glot: SQLGlotExpression
+    if len(sql_glot_args) == 1:
+        strip_char_glot = sqlglot_expressions.Literal.string("\n\t ")
+    else:
+        strip_char_glot = sql_glot_args[1]
+    return sqlglot_expressions.Trim(
+        this=to_strip,
+        expression=strip_char_glot,
+    )
+
+
+def convert_find(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for getting the index of the first occurrence of a substring within a string.
+    The first argument is the string to search within,
+    and the second argument is the substring to search for.
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+    Returns:
+        The SQLGlot expression matching the functionality of `FIND(this, expression)`,
+        i.e the index of the first occurrence of the second argument within
+        the first argument, or -1 if the second argument is not found.
+    """
+    assert len(sql_glot_args) == 2
+    answer: SQLGlotExpression = sqlglot_expressions.Sub(
+        this=sqlglot_expressions.StrPosition(
+            this=sql_glot_args[0], substr=sql_glot_args[1]
+        ),
+        expression=sqlglot_expressions.Literal.number(1),
+    )
+    return answer
+
+
 class SqlGlotTransformBindings:
     """
     Binding infrastructure used to associate PyDough operators with a procedure
@@ -1693,9 +1793,13 @@ class SqlGlotTransformBindings:
         self.bindings[pydop.JOIN_STRINGS] = convert_concat_ws
         self.bindings[pydop.LPAD] = convert_lpad
         self.bindings[pydop.RPAD] = convert_rpad
+        self.bindings[pydop.FIND] = convert_find
+        self.bindings[pydop.STRIP] = convert_strip
 
         # Numeric functions
         self.bind_simple_function(pydop.ABS, sqlglot_expressions.Abs)
+        self.bind_simple_function(pydop.ROUND, sqlglot_expressions.Round)
+        self.bindings[pydop.SIGN] = convert_sign
         self.bindings[pydop.ROUND] = convert_round
 
         # Conditional functions


Test patch:
diff --git a/tests/simple_pydough_functions.py b/tests/simple_pydough_functions.py
index 61f6050..a7dcee3 100644
--- a/tests/simple_pydough_functions.py
+++ b/tests/simple_pydough_functions.py
@@ -973,3 +973,59 @@ def step_slicing():
         wo_step8=name[-4:-2],
         wo_step9=name[2:2],
     )
+
+
+def sign():
+    return (
+        DailyPrices.CALCULATE(
+            high,
+            high_neg=-1 * high,
+            high_zero=0 * high,
+        )
+        .TOP_K(5, by=high.ASC())
+        .CALCULATE(
+            high,
+            high_neg,
+            high_zero,
+            sign_high=SIGN(high),
+            sign_high_neg=SIGN(high_neg),
+            sign_high_zero=SIGN(high_zero),
+        )
+    )
+
+
+def find():
+    return Customers.WHERE(name == "Alex Rodriguez").CALCULATE(
+        name,
+        idx_Alex=FIND(name, "Alex"),
+        idx_Rodriguez=FIND(name, "Rodriguez"),
+        idx_bob=FIND(name, "bob"),
+        idx_e=FIND(name, "e"),
+        idx_space=FIND(name, " "),
+        idx_of_R=FIND(name, "R"),
+        idx_of_Alex_Rodriguez=FIND(name, "Alex Rodriguez"),
+    )
+
+
+def strip():
+    return (
+        Customers.WHERE(name == "Alex Rodriguez")
+        .CALCULATE(
+            name,
+            alt_name1="  Alex Rodriguez  ",
+            alt_name2="aeiAlex Rodriguezaeiou",
+            alt_name3=";;Alex Rodriguez;;",
+            alt_name4="""
+    Alex Rodriguez
+        """,  # equivalent to "\n\tAlex Rodriguez\n"
+        )
+        .CALCULATE(
+            stripped_name=STRIP(name, "Alex Rodriguez"),
+            stripped_name1=STRIP(name),
+            stripped_name_with_chars=STRIP(name, "lAez"),
+            stripped_alt_name1=STRIP(alt_name1),
+            stripped_alt_name2=STRIP(alt_name2, "aeiou"),
+            stripped_alt_name3=STRIP(alt_name3, ";"),
+            stripped_alt_name4=STRIP(alt_name4),
+        )
+    )
diff --git a/tests/test_pipeline_defog_custom.py b/tests/test_pipeline_defog_custom.py
index 92926e5..b0698c3 100644
--- a/tests/test_pipeline_defog_custom.py
+++ b/tests/test_pipeline_defog_custom.py
@@ -29,6 +29,7 @@ from bad_pydough_functions import (
 )
 from simple_pydough_functions import (
     exponentiation,
+    find,
     hour_minute_day,
     minutes_seconds_datediff,
     multi_partition_access_1,
@@ -38,7 +39,9 @@ from simple_pydough_functions import (
     multi_partition_access_5,
     multi_partition_access_6,
     padding_functions,
+    sign,
     step_slicing,
+    strip,
     years_months_days_hours_datediff,
 )
 from test_utils import (
@@ -595,8 +598,8 @@ from pydough.unqualified import (
                         ]
                     }
                 ).assign(
-                    ref_rpad=lambda x: "Cust0001**********************",
-                    ref_lpad=lambda x: "**********************Cust0001",
+                    ref_rpad="Cust0001**********************",
+                    ref_lpad="**********************Cust0001",
                     right_padded=lambda x: x.original_name.apply(
                         lambda s: (s + "*" * 30)[:30]
                     ),
@@ -709,6 +712,69 @@ from pydough.unqualified import (
             ),
             id="step_slicing",
         ),
+        pytest.param(
+            (
+                sign,
+                None,
+                "Broker",
+                "sign",
+                lambda: pd.DataFrame(
+                    {
+                        "high": [83.0, 83.6, 84.2, 84.8, 85.4],
+                    }
+                ).assign(
+                    high_neg=lambda x: x["high"] * -1,
+                    high_zero=lambda x: x["high"] * 0,
+                    sign_high=1,
+                    sign_high_neg=-1,
+                    sign_high_zero=0,
+                ),
+            ),
+            id="sign",
+        ),
+        pytest.param(
+            (
+                find,
+                None,
+                "Broker",
+                "find",
+                lambda: pd.DataFrame(
+                    {
+                        "name": ["Alex Rodriguez"],
+                        "idx_Alex": ["Alex Rodriguez".find("Alex")],
+                        "idx_Rodriguez": ["Alex Rodriguez".find("Rodriguez")],
+                        "idx_bob": ["Alex Rodriguez".find("bob")],
+                        "idx_e": ["Alex Rodriguez".find("e")],
+                        "idx_space": ["Alex Rodriguez".find(" ")],
+                        "idx_of_R": ["Alex Rodriguez".find("R")],
+                        "idx_of_Alex_Rodriguez": [
+                            "Alex Rodriguez".find("Alex Rodriguez")
+                        ],
+                    }
+                ),
+            ),
+            id="find",
+        ),
+        pytest.param(
+            (
+                strip,
+                None,
+                "Broker",
+                "strip",
+                lambda: pd.DataFrame(
+                    {
+                        "stripped_name": [""],
+                        "stripped_name1": ["Alex Rodriguez"],
+                        "stripped_name_with_chars": ["x Rodrigu"],
+                        "stripped_alt_name1": ["Alex Rodriguez"],
+                        "stripped_alt_name2": ["Alex Rodriguez"],
+                        "stripped_alt_name3": ["Alex Rodriguez"],
+                        "stripped_alt_name4": ["Alex Rodriguez"],
+                    }
+                ),
+            ),
+            id="strip",
+        ),
     ],
 )
 def custom_defog_test_data(
diff --git a/tests/test_plan_refsols/find.txt b/tests/test_plan_refsols/find.txt
new file mode 100644
index 0000000..fa344d0
--- /dev/null
+++ b/tests/test_plan_refsols/find.txt
@@ -0,0 +1,4 @@
+ROOT(columns=[('name', name), ('idx_Alex', idx_Alex), ('idx_Rodriguez', idx_Rodriguez), ('idx_bob', idx_bob), ('idx_e', idx_e), ('idx_space', idx_space), ('idx_of_R', idx_of_R), ('idx_of_Alex_Rodriguez', idx_of_Alex_Rodriguez)], orderings=[])
+ PROJECT(columns={'idx_Alex': FIND(name, 'Alex':string), 'idx_Rodriguez': FIND(name, 'Rodriguez':string), 'idx_bob': FIND(name, 'bob':string), 'idx_e': FIND(name, 'e':string), 'idx_of_Alex_Rodriguez': FIND(name, 'Alex Rodriguez':string), 'idx_of_R': FIND(name, 'R':string), 'idx_space': FIND(name, ' ':string), 'name': name})
+  FILTER(condition=name == 'Alex Rodriguez':string, columns={'name': name})
+   SCAN(table=main.sbCustomer, columns={'name': sbCustName})
diff --git a/tests/test_plan_refsols/sign.txt b/tests/test_plan_refsols/sign.txt
new file mode 100644
index 0000000..44f63f6
--- /dev/null
+++ b/tests/test_plan_refsols/sign.txt
@@ -0,0 +1,6 @@
+ROOT(columns=[('high', high), ('high_neg', high_neg), ('high_zero', high_zero), ('sign_high', sign_high), ('sign_high_neg', sign_high_neg), ('sign_high_zero', sign_high_zero)], orderings=[(ordering_0):asc_first])
+ PROJECT(columns={'high': high, 'high_neg': high_neg, 'high_zero': high_zero, 'ordering_0': ordering_0, 'sign_high': SIGN(high), 'sign_high_neg': SIGN(high_neg), 'sign_high_zero': SIGN(high_zero)})
+  LIMIT(limit=Literal(value=5, type=Int64Type()), columns={'high': high, 'high_neg': high_neg, 'high_zero': high_zero, 'ordering_0': ordering_0}, orderings=[(ordering_0):asc_first])
+   PROJECT(columns={'high': high, 'high_neg': high_neg, 'high_zero': high_zero, 'ordering_0': high})
+    PROJECT(columns={'high': high, 'high_neg': -1:int64 * high, 'high_zero': 0:int64 * high})
+     SCAN(table=main.sbDailyPrice, columns={'high': sbDpHigh})
diff --git a/tests/test_plan_refsols/strip.txt b/tests/test_plan_refsols/strip.txt
new file mode 100644
index 0000000..a07da36
--- /dev/null
+++ b/tests/test_plan_refsols/strip.txt
@@ -0,0 +1,5 @@
+ROOT(columns=[('stripped_name', stripped_name), ('stripped_name1', stripped_name1), ('stripped_name_with_chars', stripped_name_with_chars), ('stripped_alt_name1', stripped_alt_name1), ('stripped_alt_name2', stripped_alt_name2), ('stripped_alt_name3', stripped_alt_name3), ('stripped_alt_name4', stripped_alt_name4)], orderings=[])
+ PROJECT(columns={'stripped_alt_name1': STRIP(alt_name1), 'stripped_alt_name2': STRIP(alt_name2, 'aeiou':string), 'stripped_alt_name3': STRIP(alt_name3, ';':string), 'stripped_alt_name4': STRIP(alt_name4), 'stripped_name': STRIP(name, 'Alex Rodriguez':string), 'stripped_name1': STRIP(name), 'stripped_name_with_chars': STRIP(name, 'lAez':string)})
+  PROJECT(columns={'alt_name1': '  Alex Rodriguez  ':string, 'alt_name2': 'aeiAlex Rodriguezaeiou':string, 'alt_name3': ';;Alex Rodriguez;;':string, 'alt_name4': '\n    Alex Rodriguez\n        ':string, 'name': name})
+   FILTER(condition=name == 'Alex Rodriguez':string, columns={'name': name})
+    SCAN(table=main.sbCustomer, columns={'name': sbCustName})
diff --git a/tests/tpch_test_functions.py b/tests/tpch_test_functions.py
index a5176ff..62814aa 100644
--- a/tests/tpch_test_functions.py
+++ b/tests/tpch_test_functions.py
@@ -1,3 +1,7 @@
+"""
+PyDough implementation of TPCH queries.
+"""
+
 __all__ = [
     "impl_tpch_q1",
     "impl_tpch_q10",