Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,37 @@ Parameters are substituted into the MongoDB filter during execution, providing p
- **Logical operators**: `WHERE age > 18 AND status = 'active'`, `WHERE age < 30 OR role = 'admin'`
- **Nested field filtering**: `WHERE profile.status = 'active'`
- **Array filtering**: `WHERE items[0].price > 100`
- **Value Functions**: Apply transformations to values in WHERE clauses for filtering

#### Value Functions

PyMongoSQL supports value functions to transform and filter values in WHERE clauses. Built-in value functions include:

**str_to_datetime()** - Convert ISO 8601 or custom formatted strings to Python datetime objects

```python
# ISO 8601 format
cursor.execute("SELECT * FROM events WHERE created_at >= str_to_datetime('2024-01-15T10:30:00Z')")

# Custom format
cursor.execute("SELECT * FROM events WHERE created_at < str_to_datetime('03/15/2024', '%m/%d/%Y')")
```

**str_to_timestamp()** - Convert ISO 8601 or custom formatted strings to BSON Timestamp objects

```python
# ISO 8601 format
cursor.execute("SELECT * FROM logs WHERE timestamp > str_to_timestamp('2024-01-15T00:00:00Z')")

# Custom format
cursor.execute("SELECT * FROM logs WHERE timestamp < str_to_timestamp('01/15/2024', '%m/%d/%Y')")
```

Both functions:
- Support ISO 8601 strings with 'Z' timezone indicator
- Support custom format strings using Python strftime directives
- Return values with UTC timezone
- Can be combined with standard SQL operators (>, <, >=, <=, =, !=)

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

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

## Limitations & Roadmap

**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:

- **Advanced DML Operations**
- `REPLACE`, `MERGE`, `UPSERT`

These features are on our development roadmap and contributions are welcome!

## Contributing

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.
Expand Down
2 changes: 1 addition & 1 deletion pymongosql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
if TYPE_CHECKING:
from .connection import Connection

__version__: str = "0.4.0"
__version__: str = "0.4.1"

# Globals https://www.python.org/dev/peps/pep-0249/#globals
apilevel: str = "2.0"
Expand Down
88 changes: 87 additions & 1 deletion pymongosql/sql/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,99 @@ def _extract_value(self, ctx: Any) -> Any:
if operator:
parts = self._split_by_operator(text, operator)
if len(parts) >= 2:
return self._parse_value(parts[1].strip("()"))
value_text = parts[1].strip()
# Check if value is a function call
return self._extract_value_or_function(value_text)

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

def _extract_value_or_function(self, value_text: str) -> Any:
"""
Extract value, which could be a literal or a value function call.

Detects and executes value functions like str_to_datetime(...), str_to_timestamp(...).

Args:
value_text: Text representing the value, possibly a function call

Returns:
Processed value (result of function execution if function, otherwise parsed literal)
"""
value_text = value_text.strip()

# Check if this looks like a function call: func_name(...)
if "(" in value_text and value_text.endswith(")"):
# Extract function name and arguments
paren_pos = value_text.find("(")
func_name = value_text[:paren_pos].strip()

# Check if it's a valid identifier (function name)
if func_name.isidentifier():
try:
from .value_function_registry import get_default_registry

registry = get_default_registry()
if registry.has_function(func_name):
# It's a registered value function - execute it
args_text = value_text[paren_pos + 1 : -1]
args = self._parse_function_arguments(args_text)
result = registry.execute(func_name, args)
_logger.debug(f"Executed value function: {func_name}({args}) -> {result}")
return result
except Exception as e:
_logger.warning(f"Failed to execute value function '{func_name}': {e}")
# Fall through to treat as regular value

# Not a function call or function execution failed - treat as regular value
return self._parse_value(value_text)

def _parse_function_arguments(self, args_text: str) -> list:
"""
Parse function arguments from comma-separated string.

Handles string literals with quotes and nested structures.

Args:
args_text: Text of function arguments (e.g., "'2024-01-01', '%m/%d/%Y'")

Returns:
List of parsed argument values
"""
if not args_text.strip():
return []

args = []
current_arg = ""
in_quotes = False
quote_char = None

for char in args_text:
if char in ('"', "'") and not in_quotes:
in_quotes = True
quote_char = char
current_arg += char
elif char == quote_char and in_quotes:
in_quotes = False
quote_char = None
current_arg += char
elif char == "," and not in_quotes:
# End of argument
arg = current_arg.strip()
if arg:
args.append(self._parse_value(arg))
current_arg = ""
else:
current_arg += char

# Don't forget the last argument
if current_arg.strip():
args.append(self._parse_value(current_arg.strip()))

return args

def _extract_in_values(self, text: str) -> List[Any]:
"""Extract values from IN clause"""
# Handle both 'IN(' and 'IN (' patterns
Expand Down
Loading