Skip to content

Commit 37c6ad3

Browse files
authored
0.4.1 - Value functions supported (#17)
* Bump version * Support value functions --------- Co-authored-by: Peng Ren
1 parent 1c76e22 commit 37c6ad3

6 files changed

Lines changed: 721 additions & 11 deletions

File tree

README.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,37 @@ Parameters are substituted into the MongoDB filter during execution, providing p
223223
- **Logical operators**: `WHERE age > 18 AND status = 'active'`, `WHERE age < 30 OR role = 'admin'`
224224
- **Nested field filtering**: `WHERE profile.status = 'active'`
225225
- **Array filtering**: `WHERE items[0].price > 100`
226+
- **Value Functions**: Apply transformations to values in WHERE clauses for filtering
227+
228+
#### Value Functions
229+
230+
PyMongoSQL supports value functions to transform and filter values in WHERE clauses. Built-in value functions include:
231+
232+
**str_to_datetime()** - Convert ISO 8601 or custom formatted strings to Python datetime objects
233+
234+
```python
235+
# ISO 8601 format
236+
cursor.execute("SELECT * FROM events WHERE created_at >= str_to_datetime('2024-01-15T10:30:00Z')")
237+
238+
# Custom format
239+
cursor.execute("SELECT * FROM events WHERE created_at < str_to_datetime('03/15/2024', '%m/%d/%Y')")
240+
```
241+
242+
**str_to_timestamp()** - Convert ISO 8601 or custom formatted strings to BSON Timestamp objects
243+
244+
```python
245+
# ISO 8601 format
246+
cursor.execute("SELECT * FROM logs WHERE timestamp > str_to_timestamp('2024-01-15T00:00:00Z')")
247+
248+
# Custom format
249+
cursor.execute("SELECT * FROM logs WHERE timestamp < str_to_timestamp('01/15/2024', '%m/%d/%Y')")
250+
```
251+
252+
Both functions:
253+
- Support ISO 8601 strings with 'Z' timezone indicator
254+
- Support custom format strings using Python strftime directives
255+
- Return values with UTC timezone
256+
- Can be combined with standard SQL operators (>, <, >=, <=, =, !=)
226257

227258
### Nested Field Support
228259
- **Single-level**: `profile.name`, `settings.theme`
@@ -505,15 +536,6 @@ PyMongoSQL can be used as a database driver in Apache Superset for querying and
505536

506537
This allows seamless integration between MongoDB data and Superset's BI capabilities without requiring data migration to traditional SQL databases.
507538

508-
## Limitations & Roadmap
509-
510-
**Note**: PyMongoSQL currently supports DQL (Data Query Language) and DML (Data Manipulation Language) operations. The following SQL features are **not yet supported** but are planned for future releases:
511-
512-
- **Advanced DML Operations**
513-
- `REPLACE`, `MERGE`, `UPSERT`
514-
515-
These features are on our development roadmap and contributions are welcome!
516-
517539
## Contributing
518540

519541
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

pymongosql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
if TYPE_CHECKING:
77
from .connection import Connection
88

9-
__version__: str = "0.4.0"
9+
__version__: str = "0.4.1"
1010

1111
# Globals https://www.python.org/dev/peps/pep-0249/#globals
1212
apilevel: str = "2.0"

pymongosql/sql/handler.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,99 @@ def _extract_value(self, ctx: Any) -> Any:
401401
if operator:
402402
parts = self._split_by_operator(text, operator)
403403
if len(parts) >= 2:
404-
return self._parse_value(parts[1].strip("()"))
404+
value_text = parts[1].strip()
405+
# Check if value is a function call
406+
return self._extract_value_or_function(value_text)
405407

406408
return None
407409
except Exception as e:
408410
_logger.debug(f"Failed to extract value: {e}")
409411
return None
410412

413+
def _extract_value_or_function(self, value_text: str) -> Any:
414+
"""
415+
Extract value, which could be a literal or a value function call.
416+
417+
Detects and executes value functions like str_to_datetime(...), str_to_timestamp(...).
418+
419+
Args:
420+
value_text: Text representing the value, possibly a function call
421+
422+
Returns:
423+
Processed value (result of function execution if function, otherwise parsed literal)
424+
"""
425+
value_text = value_text.strip()
426+
427+
# Check if this looks like a function call: func_name(...)
428+
if "(" in value_text and value_text.endswith(")"):
429+
# Extract function name and arguments
430+
paren_pos = value_text.find("(")
431+
func_name = value_text[:paren_pos].strip()
432+
433+
# Check if it's a valid identifier (function name)
434+
if func_name.isidentifier():
435+
try:
436+
from .value_function_registry import get_default_registry
437+
438+
registry = get_default_registry()
439+
if registry.has_function(func_name):
440+
# It's a registered value function - execute it
441+
args_text = value_text[paren_pos + 1 : -1]
442+
args = self._parse_function_arguments(args_text)
443+
result = registry.execute(func_name, args)
444+
_logger.debug(f"Executed value function: {func_name}({args}) -> {result}")
445+
return result
446+
except Exception as e:
447+
_logger.warning(f"Failed to execute value function '{func_name}': {e}")
448+
# Fall through to treat as regular value
449+
450+
# Not a function call or function execution failed - treat as regular value
451+
return self._parse_value(value_text)
452+
453+
def _parse_function_arguments(self, args_text: str) -> list:
454+
"""
455+
Parse function arguments from comma-separated string.
456+
457+
Handles string literals with quotes and nested structures.
458+
459+
Args:
460+
args_text: Text of function arguments (e.g., "'2024-01-01', '%m/%d/%Y'")
461+
462+
Returns:
463+
List of parsed argument values
464+
"""
465+
if not args_text.strip():
466+
return []
467+
468+
args = []
469+
current_arg = ""
470+
in_quotes = False
471+
quote_char = None
472+
473+
for char in args_text:
474+
if char in ('"', "'") and not in_quotes:
475+
in_quotes = True
476+
quote_char = char
477+
current_arg += char
478+
elif char == quote_char and in_quotes:
479+
in_quotes = False
480+
quote_char = None
481+
current_arg += char
482+
elif char == "," and not in_quotes:
483+
# End of argument
484+
arg = current_arg.strip()
485+
if arg:
486+
args.append(self._parse_value(arg))
487+
current_arg = ""
488+
else:
489+
current_arg += char
490+
491+
# Don't forget the last argument
492+
if current_arg.strip():
493+
args.append(self._parse_value(current_arg.strip()))
494+
495+
return args
496+
411497
def _extract_in_values(self, text: str) -> List[Any]:
412498
"""Extract values from IN clause"""
413499
# Handle both 'IN(' and 'IN (' patterns

0 commit comments

Comments
 (0)