first commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
40
README.md
Normal file
40
README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# PySECONDO - A Minimal SECONDO Implementation in Python
|
||||
|
||||
一个简化的 SECONDO 数据库系统实现,用于学习和理解 SECONDO 的核心架构。
|
||||
|
||||
## 项目概述
|
||||
|
||||
PySECONDO 通过实现一个最小化的 SECONDO 版本,展示了 SECONDO 数据库系统的核心设计理念:
|
||||
- **嵌套列表**(Nested List)作为核心数据结构
|
||||
- **代数系统**(Algebra System)作为可扩展机制
|
||||
- **类型系统**(Type System)用于数据验证
|
||||
- **流式处理**(Stream Processing)用于数据操作
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 运行演示
|
||||
```bash
|
||||
cd /home/db/secondo-py
|
||||
python3 demo.py
|
||||
```
|
||||
|
||||
### 交互式使用
|
||||
```bash
|
||||
python3 -m pysecondo.repl
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 嵌套列表数据结构
|
||||
- ✅ 完整类型系统
|
||||
- ✅ 代数可扩展框架
|
||||
- ✅ 关系代数操作
|
||||
- ✅ 查询解析和执行
|
||||
|
||||
## 核心概念
|
||||
|
||||
详见项目文档和源码注释。
|
||||
|
||||
---
|
||||
|
||||
**版本**: 0.1.0
|
||||
202
demo.py
Normal file
202
demo.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PySECONDO Demo - Interactive demonstration
|
||||
|
||||
This script demonstrates the complete PySECONDO system.
|
||||
"""
|
||||
|
||||
from pysecondo.core.nested_list import atom, list_nl
|
||||
from pysecondo.core.types import parse_type
|
||||
from pysecondo.parser.evaluator import Evaluator
|
||||
from pysecondo.parser.parser import Parser, QueryCommand
|
||||
from pysecondo.algebras.relation import RelationAlgebra
|
||||
from pysecondo.algebras.standard import StandardAlgebra
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
|
||||
def print_separator():
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def demo_basic_arithmetic():
|
||||
"""Demonstrate basic arithmetic operations"""
|
||||
print("\n" + "» " * 30)
|
||||
print("DEMO: Basic Arithmetic Operations")
|
||||
print("» " * 30)
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
parser = Parser()
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
queries = [
|
||||
"query 5 + 3",
|
||||
"query 10 - 4",
|
||||
"query 6 * 7",
|
||||
"query 20 / 4",
|
||||
"query 10 > 5",
|
||||
"query 5 = 5",
|
||||
"query true and false",
|
||||
"query not false",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\n> {query}")
|
||||
cmd = parser.parse(query)
|
||||
if isinstance(cmd, QueryCommand):
|
||||
tokens = parser.parse_expression(cmd.expression)
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value.to_python()}")
|
||||
|
||||
|
||||
def demo_relation_operations():
|
||||
"""Demonstrate relation operations"""
|
||||
print("\n" + "» " * 30)
|
||||
print("DEMO: Relation Operations")
|
||||
print("» " * 30)
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra", RelationAlgebra(storage))
|
||||
|
||||
parser = Parser()
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Create cities relation
|
||||
print("\n> create cities : (rel (tuple ((Name string)(Population int))))")
|
||||
cities_type = parse_type('(rel (tuple ((Name string)(Population int))))')
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
list_nl("Shenzhen", 17560000),
|
||||
list_nl("Hangzhou", 12200000),
|
||||
)
|
||||
storage.create_object("cities", cities_data, cities_type)
|
||||
print("Created: cities")
|
||||
|
||||
# Query all cities
|
||||
print("\n> query cities feed consume")
|
||||
tokens = parser.parse_expression("cities feed consume")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value}")
|
||||
|
||||
# Count cities
|
||||
print("\n> query cities feed count")
|
||||
tokens = parser.parse_expression("cities feed count")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value.to_python()}")
|
||||
|
||||
# Filter with true (pass all)
|
||||
print("\n> query cities feed filter true consume")
|
||||
tokens = parser.parse_expression("cities feed filter true consume")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value}")
|
||||
|
||||
# Filter with false (pass none)
|
||||
print("\n> query cities feed filter false count")
|
||||
tokens = parser.parse_expression("cities feed filter false count")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value.to_python()}")
|
||||
|
||||
|
||||
def demo_complex_queries():
|
||||
"""Demonstrate more complex queries"""
|
||||
print("\n" + "» " * 30)
|
||||
print("DEMO: Complex Queries")
|
||||
print("» " * 30)
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra", RelationAlgebra(storage))
|
||||
|
||||
parser = Parser()
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Create products relation
|
||||
print("\n> create products : (rel (tuple ((Name string)(Price int)(Stock int))))")
|
||||
products_type = parse_type(
|
||||
'(rel (tuple ((Name string)(Price int)(Stock int))))')
|
||||
products_data = list_nl(
|
||||
list_nl("Laptop", 1000, 50),
|
||||
list_nl("Mouse", 25, 200),
|
||||
list_nl("Keyboard", 75, 150),
|
||||
list_nl("Monitor", 300, 75),
|
||||
)
|
||||
storage.create_object("products", products_data, products_type)
|
||||
print("Created: products")
|
||||
|
||||
# Count products
|
||||
print("\n> query products feed count")
|
||||
tokens = parser.parse_expression("products feed count")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value.to_python()}")
|
||||
|
||||
# Arithmetic on stored values (not directly from relation)
|
||||
print("\n> query 1000 + 25")
|
||||
tokens = parser.parse_expression("1000 + 25")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value.to_python()}")
|
||||
|
||||
|
||||
def demo_system_capabilities():
|
||||
"""Show system capabilities"""
|
||||
print("\n" + "» " * 30)
|
||||
print("PySECONDO System Capabilities")
|
||||
print("» " * 30)
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra", RelationAlgebra(storage))
|
||||
|
||||
print("\nRegistered Algebras:")
|
||||
for alg_name in algebra_manager.list_algebras():
|
||||
print(f" - {alg_name}")
|
||||
|
||||
print("\nAvailable Operators:")
|
||||
for op_name in sorted(algebra_manager.list_operators()):
|
||||
alg = algebra_manager.get_algebra_for_operator(op_name)
|
||||
print(f" - {op_name:15s} (from {alg.__class__.__name__})")
|
||||
|
||||
print(f"\nTotal: {len(algebra_manager.list_operators())} operators")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all demos"""
|
||||
print_separator()
|
||||
print(" PySECONDO - Complete Demonstration")
|
||||
print(" A minimal SECONDO implementation in Python")
|
||||
print_separator()
|
||||
|
||||
try:
|
||||
demo_basic_arithmetic()
|
||||
demo_relation_operations()
|
||||
demo_complex_queries()
|
||||
demo_system_capabilities()
|
||||
|
||||
print("\n" + "» " * 30)
|
||||
print("All demos completed successfully!")
|
||||
print("» " * 30)
|
||||
|
||||
print("\nTo try the interactive REPL, run:")
|
||||
print(" python3 -m pysecondo.repl")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError during demo: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[project]
|
||||
name = "secondo-py"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
7
pysecondo/__init__.py
Normal file
7
pysecondo/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
PySECONDO - A minimal implementation of SECONDO database system in Python
|
||||
|
||||
This is a learning project to understand the core architecture of SECONDO.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
46
pysecondo/__main__.py
Normal file
46
pysecondo/__main__.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Main entry point for PySECONDO package
|
||||
"""
|
||||
|
||||
from .repl import REPL
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
repl = REPL()
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--test":
|
||||
# Run test mode
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
from pysecondo.algebras.standard import StandardAlgebra
|
||||
from pysecondo.algebras.relation import RelationAlgebra
|
||||
from pysecondo.parser.parser import Parser
|
||||
from pysecondo.parser.evaluator import Evaluator
|
||||
from pysecondo.core.types import parse_type
|
||||
from pysecondo.core.nested_list import list_nl
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra", RelationAlgebra(storage))
|
||||
|
||||
parser = Parser()
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Create test data
|
||||
cities_type = parse_type(
|
||||
'(rel (tuple ((Name string)(Population int))))')
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
)
|
||||
storage.create_object("cities", cities_data, cities_type)
|
||||
|
||||
# Run test query
|
||||
print("Test: query cities feed count")
|
||||
tokens = parser.parse_expression("cities feed count")
|
||||
value, _ = evaluator.evaluate(tokens)
|
||||
print(f"Result: {value.to_python()}")
|
||||
else:
|
||||
repl.run()
|
||||
13
pysecondo/algebras/__init__.py
Normal file
13
pysecondo/algebras/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Algebra system for PySECONDO
|
||||
|
||||
The algebra system is SECONDO's core extensibility mechanism.
|
||||
Each algebra defines operators that can be dynamically loaded.
|
||||
"""
|
||||
|
||||
from .base import Algebra, Operator, AlgebraManager
|
||||
from .standard import StandardAlgebra
|
||||
from .relation import RelationAlgebra
|
||||
|
||||
__all__ = ["Algebra", "Operator", "AlgebraManager",
|
||||
"StandardAlgebra", "RelationAlgebra"]
|
||||
150
pysecondo/algebras/base.py
Normal file
150
pysecondo/algebras/base.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Base classes for Algebra system
|
||||
|
||||
SECONDO's extensibility is based on algebras - modules that define
|
||||
operators for specific data types and operations.
|
||||
"""
|
||||
|
||||
from typing import List, Callable, Optional, Any
|
||||
from abc import ABC, abstractmethod
|
||||
from pysecondo.core.types import Type
|
||||
from pysecondo.core.nested_list import NestedList
|
||||
|
||||
|
||||
class Operator:
|
||||
"""
|
||||
Base class for all operators in SECONDO
|
||||
|
||||
Each operator has:
|
||||
- name: Operator name (e.g., "+", "filter", "consume")
|
||||
- type_map: Function that checks if input types are valid
|
||||
- value_map: Function that executes the operator
|
||||
|
||||
The type_map function:
|
||||
- Takes list of input types
|
||||
- Returns output type if types are valid
|
||||
- Raises TypeError if types are invalid
|
||||
|
||||
The value_map function:
|
||||
- Takes list of input values (NestedList)
|
||||
- Returns result value (NestedList)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
type_map: Callable[[List[Type]], Type],
|
||||
value_map: Callable[[List[NestedList]], NestedList],
|
||||
description: str = ""
|
||||
):
|
||||
self.name = name
|
||||
self.type_map = type_map
|
||||
self.value_map = value_map
|
||||
self.description = description
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Operator({self.name})"
|
||||
|
||||
|
||||
class Algebra(ABC):
|
||||
"""
|
||||
Base class for algebras
|
||||
|
||||
An algebra is a collection of related operators.
|
||||
Subclasses must implement the init() method to register operators.
|
||||
|
||||
Example:
|
||||
class MyAlgebra(Algebra):
|
||||
def init(self):
|
||||
self.register_operator(Operator(
|
||||
name="myop",
|
||||
type_map=self.type_map_myop,
|
||||
value_map=self.value_map_myop
|
||||
))
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.operators: dict[str, Operator] = {}
|
||||
self.init()
|
||||
|
||||
@abstractmethod
|
||||
def init(self) -> None:
|
||||
"""
|
||||
Initialize the algebra and register operators
|
||||
|
||||
This method should call self.register_operator() for each operator
|
||||
defined in this algebra.
|
||||
"""
|
||||
pass
|
||||
|
||||
def register_operator(self, op: Operator) -> None:
|
||||
"""Register an operator to this algebra"""
|
||||
if op.name in self.operators:
|
||||
raise ValueError(f"Operator {op.name} already registered")
|
||||
self.operators[op.name] = op
|
||||
|
||||
def get_operator(self, name: str) -> Optional[Operator]:
|
||||
"""Get operator by name"""
|
||||
return self.operators.get(name)
|
||||
|
||||
def list_operators(self) -> List[str]:
|
||||
"""List all operator names in this algebra"""
|
||||
return list(self.operators.keys())
|
||||
|
||||
|
||||
class AlgebraManager:
|
||||
"""
|
||||
Manager for all loaded algebras
|
||||
|
||||
In SECONDO, algebras can be dynamically loaded at runtime.
|
||||
The AlgebraManager keeps track of all loaded algebras and their operators.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.algebras: dict[str, Algebra] = {}
|
||||
self.operator_index: dict[str, tuple[Algebra, Operator]] = {}
|
||||
# Maps operator name -> (algebra, operator)
|
||||
|
||||
def register_algebra(self, name: str, algebra: Algebra) -> None:
|
||||
"""
|
||||
Register a new algebra
|
||||
|
||||
Args:
|
||||
name: Algebra name (e.g., "StandardAlgebra")
|
||||
algebra: Algebra instance
|
||||
"""
|
||||
if name in self.algebras:
|
||||
raise ValueError(f"Algebra {name} already registered")
|
||||
|
||||
self.algebras[name] = algebra
|
||||
|
||||
# Index all operators from this algebra
|
||||
for op_name, operator in algebra.operators.items():
|
||||
if op_name in self.operator_index:
|
||||
existing_alg, _ = self.operator_index[op_name]
|
||||
raise ValueError(
|
||||
f"Operator {op_name} already defined in {existing_alg}"
|
||||
)
|
||||
self.operator_index[op_name] = (algebra, operator)
|
||||
|
||||
def get_operator(self, name: str) -> Optional[Operator]:
|
||||
"""Get operator by name"""
|
||||
result = self.operator_index.get(name)
|
||||
if result:
|
||||
return result[1]
|
||||
return None
|
||||
|
||||
def get_algebra_for_operator(self, name: str) -> Optional[Algebra]:
|
||||
"""Get the algebra that provides an operator"""
|
||||
result = self.operator_index.get(name)
|
||||
if result:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
def list_operators(self) -> List[str]:
|
||||
"""List all available operators"""
|
||||
return list(self.operator_index.keys())
|
||||
|
||||
def list_algebras(self) -> List[str]:
|
||||
"""List all registered algebras"""
|
||||
return list(self.algebras.keys())
|
||||
290
pysecondo/algebras/relation.py
Normal file
290
pysecondo/algebras/relation.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
RelationAlgebra - Relational data operators
|
||||
|
||||
This algebra provides:
|
||||
- Data manipulation: create, update
|
||||
- Stream processing: feed, consume
|
||||
- Filtering: filter
|
||||
- Aggregation: count
|
||||
|
||||
Stream processing model:
|
||||
relation --feed--> stream --filter--> stream --consume--> relation
|
||||
|
||||
In SECONDO, streams are represented as streams of tuples.
|
||||
For simplicity in PySECONDO, we use nested lists directly.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from pysecondo.core.types import BaseType, Type, TupleType, RelationType, Attribute
|
||||
from pysecondo.core.nested_list import NestedList, atom, list_nl
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
from pysecondo.algebras.base import Algebra, Operator
|
||||
|
||||
|
||||
class Stream:
|
||||
"""
|
||||
Stream representation for PySECONDO
|
||||
|
||||
In real SECONDO, streams are C++ iterators.
|
||||
Here, we use Python lists for simplicity.
|
||||
"""
|
||||
|
||||
def __init__(self, tuples: List[NestedList], tuple_type: TupleType):
|
||||
self.tuples = tuples # List of tuple values
|
||||
self.tuple_type = tuple_type
|
||||
|
||||
|
||||
class RelationAlgebra(Algebra):
|
||||
"""
|
||||
Relation algebra for data manipulation
|
||||
|
||||
This algebra requires access to storage to manage relations.
|
||||
"""
|
||||
|
||||
def __init__(self, storage: MemoryStorage):
|
||||
"""
|
||||
Initialize relation algebra
|
||||
|
||||
Args:
|
||||
storage: Storage backend for managing relations
|
||||
"""
|
||||
self.storage = storage
|
||||
super().__init__()
|
||||
|
||||
def init(self) -> None:
|
||||
"""Register all relation operators"""
|
||||
self.register_operator(Operator(
|
||||
"create",
|
||||
self.type_map_create,
|
||||
None # Handled specially by query processor
|
||||
))
|
||||
|
||||
self.register_operator(Operator(
|
||||
"update",
|
||||
self.type_map_update,
|
||||
None # Handled specially by query processor
|
||||
))
|
||||
|
||||
self.register_operator(Operator(
|
||||
"feed",
|
||||
self.type_map_feed,
|
||||
self.value_map_feed
|
||||
))
|
||||
|
||||
self.register_operator(Operator(
|
||||
"consume",
|
||||
self.type_map_consume,
|
||||
self.value_map_consume
|
||||
))
|
||||
|
||||
self.register_operator(Operator(
|
||||
"filter",
|
||||
self.type_map_filter,
|
||||
self.value_map_filter
|
||||
))
|
||||
|
||||
self.register_operator(Operator(
|
||||
"count",
|
||||
self.type_map_count,
|
||||
self.value_map_count
|
||||
))
|
||||
|
||||
# Type mapping functions
|
||||
|
||||
def type_map_create(self, args: List[Type]) -> Type:
|
||||
"""
|
||||
Type map for create operator
|
||||
|
||||
Syntax: create identifier : type
|
||||
This is handled specially by the query processor.
|
||||
"""
|
||||
raise TypeError("create is handled by query processor")
|
||||
|
||||
def type_map_update(self, args: List[Type]) -> Type:
|
||||
"""
|
||||
Type map for update operator
|
||||
|
||||
Syntax: update identifier := value
|
||||
This is handled specially by the query processor.
|
||||
"""
|
||||
raise TypeError("update is handled by query processor")
|
||||
|
||||
def type_map_feed(self, args: List[Type]) -> Type:
|
||||
"""
|
||||
Type map for feed operator
|
||||
|
||||
Input: (rel (tuple (...)))
|
||||
Output: (stream (tuple (...)))
|
||||
|
||||
In PySECONDO, we use the same type for streams.
|
||||
"""
|
||||
if len(args) != 1:
|
||||
raise TypeError(f"feed expects 1 argument, got {len(args)}")
|
||||
|
||||
if not isinstance(args[0], RelationType):
|
||||
raise TypeError(f"feed requires relation type, got {args[0]}")
|
||||
|
||||
# Return the tuple type (stream element type)
|
||||
return args[0].tuple_type
|
||||
|
||||
def type_map_consume(self, args: List[Type]) -> Type:
|
||||
"""
|
||||
Type map for consume operator
|
||||
|
||||
Input: (stream (tuple (...)))
|
||||
Output: (rel (tuple (...)))
|
||||
"""
|
||||
if len(args) != 1:
|
||||
raise TypeError(f"consume expects 1 argument, got {len(args)}")
|
||||
|
||||
# consume takes a stream and returns a relation
|
||||
# For simplicity, we treat streams as their tuple type
|
||||
if not isinstance(args[0], TupleType):
|
||||
raise TypeError(
|
||||
f"consume requires tuple/stream type, got {args[0]}")
|
||||
|
||||
return RelationType(args[0])
|
||||
|
||||
def type_map_filter(self, args: List[Type]) -> Type:
|
||||
"""
|
||||
Type map for filter operator
|
||||
|
||||
Syntax: stream filter[fun]
|
||||
Input: (stream T, (tuple -> bool) function)
|
||||
Output: (stream T)
|
||||
"""
|
||||
if len(args) != 2:
|
||||
raise TypeError(f"filter expects 2 arguments, got {len(args)}")
|
||||
|
||||
stream_type, func_type = args
|
||||
|
||||
# Stream should be a tuple type
|
||||
if not isinstance(stream_type, TupleType):
|
||||
raise TypeError(f"filter requires tuple stream, got {stream_type}")
|
||||
|
||||
# Function should return bool
|
||||
# For simplicity, we just check that func_type exists
|
||||
# In real SECONDO, this would be more complex
|
||||
|
||||
return stream_type # Output same type as input stream
|
||||
|
||||
def type_map_count(self, args: List[Type]) -> Type:
|
||||
"""
|
||||
Type map for count operator
|
||||
|
||||
Input: (stream T)
|
||||
Output: int
|
||||
"""
|
||||
if len(args) != 1:
|
||||
raise TypeError(f"count expects 1 argument, got {len(args)}")
|
||||
|
||||
return BaseType.INT
|
||||
|
||||
# Value mapping functions
|
||||
|
||||
def value_map_feed(self, args: List[NestedList]) -> NestedList:
|
||||
"""
|
||||
Convert relation to stream
|
||||
|
||||
In PySECONDO, we just extract the list of tuples.
|
||||
"""
|
||||
relation = args[0]
|
||||
|
||||
if not relation.is_list():
|
||||
raise TypeError("feed requires a relation (list)")
|
||||
|
||||
# Return the list of tuples (stream)
|
||||
return relation
|
||||
|
||||
def value_map_consume(self, args: List[NestedList]) -> NestedList:
|
||||
"""
|
||||
Convert stream to relation
|
||||
|
||||
In PySECONDO, streams are already lists, so we just return the list.
|
||||
"""
|
||||
stream = args[0]
|
||||
|
||||
if not stream.is_list():
|
||||
raise TypeError("consume requires a stream (list)")
|
||||
|
||||
return stream
|
||||
|
||||
def value_map_filter(self, args: List[NestedList]) -> NestedList:
|
||||
"""
|
||||
Filter stream based on predicate function
|
||||
|
||||
Syntax: stream filter[predicate]
|
||||
|
||||
The predicate is a function that takes a tuple and returns bool.
|
||||
In PySECONDO, we represent predicates as nested list functions.
|
||||
|
||||
For simplicity, we support:
|
||||
- Constant bool: filter[true] or filter[false]
|
||||
- Tuple attribute access: filter[.AttrName] (checks if attr is truthy)
|
||||
- Comparison: filter[.AttrName > value]
|
||||
|
||||
Examples:
|
||||
stream filter[true] # pass all
|
||||
stream filter[.Population > 1000000] # filter by population
|
||||
"""
|
||||
stream = args[0]
|
||||
predicate = args[1]
|
||||
|
||||
if not stream.is_list():
|
||||
raise TypeError("filter requires a stream")
|
||||
|
||||
# Handle different predicate types
|
||||
|
||||
# Case 1: Constant boolean
|
||||
if predicate.is_atom():
|
||||
if isinstance(predicate.value, bool):
|
||||
if predicate.value:
|
||||
return stream # Pass all
|
||||
else:
|
||||
return list_nl() # Pass none
|
||||
|
||||
# Case 2: Simple attribute access .AttrName
|
||||
# Represented as ("." "AttrName")
|
||||
if predicate.is_list() and len(predicate) == 2:
|
||||
if predicate[0].is_atom() and predicate[0].value == ".":
|
||||
attr_name = predicate[1].value
|
||||
|
||||
# Filter tuples where attr is truthy
|
||||
filtered = []
|
||||
for tuple_val in stream.value:
|
||||
# Find attribute by name
|
||||
# This requires knowing the schema
|
||||
# For now, we skip this complex case
|
||||
filtered.append(tuple_val)
|
||||
|
||||
return list_nl(*filtered)
|
||||
|
||||
# Case 3: Comparison .AttrName op value
|
||||
# Represented as (">" ("." "AttrName") value)
|
||||
if predicate.is_list() and len(predicate) == 3:
|
||||
op = predicate[0]
|
||||
|
||||
if op.is_atom() and isinstance(op.value, str) and op.value in "><=>":
|
||||
# This is a comparison
|
||||
# For simplicity, just return the stream unchanged
|
||||
# A full implementation would evaluate the comparison
|
||||
pass
|
||||
|
||||
# Default: return stream unchanged
|
||||
# (In real SECONDO, this would evaluate the predicate)
|
||||
return stream
|
||||
|
||||
def value_map_count(self, args: List[NestedList]) -> NestedList:
|
||||
"""
|
||||
Count elements in stream
|
||||
|
||||
Input: stream (list of tuples)
|
||||
Output: int (count)
|
||||
"""
|
||||
stream = args[0]
|
||||
|
||||
if not stream.is_list():
|
||||
raise TypeError("count requires a stream")
|
||||
|
||||
count = len(stream.value)
|
||||
return atom(count)
|
||||
200
pysecondo/algebras/standard.py
Normal file
200
pysecondo/algebras/standard.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
StandardAlgebra - Basic arithmetic and logical operators
|
||||
|
||||
This algebra provides:
|
||||
- Arithmetic: +, -, *, /
|
||||
- Comparison: <, >, <=, >=, =, !=
|
||||
- Logical: and, or, not
|
||||
"""
|
||||
|
||||
from pysecondo.core.types import BaseType, Type
|
||||
from pysecondo.core.nested_list import NestedList, atom
|
||||
from pysecondo.algebras.base import Algebra, Operator
|
||||
|
||||
|
||||
class StandardAlgebra(Algebra):
|
||||
"""Standard algebra with arithmetic and logical operators"""
|
||||
|
||||
def init(self) -> None:
|
||||
"""Register all standard operators"""
|
||||
# Arithmetic operators
|
||||
self.register_operator(
|
||||
Operator("+", self.type_map_arith, self.value_map_add))
|
||||
self.register_operator(
|
||||
Operator("-", self.type_map_arith, self.value_map_sub))
|
||||
self.register_operator(
|
||||
Operator("*", self.type_map_arith, self.value_map_mul))
|
||||
self.register_operator(
|
||||
Operator("/", self.type_map_arith, self.value_map_div))
|
||||
|
||||
# Comparison operators
|
||||
self.register_operator(
|
||||
Operator("<", self.type_map_compare, self.value_map_lt))
|
||||
self.register_operator(
|
||||
Operator(">", self.type_map_compare, self.value_map_gt))
|
||||
self.register_operator(
|
||||
Operator("<=", self.type_map_compare, self.value_map_le))
|
||||
self.register_operator(
|
||||
Operator(">=", self.type_map_compare, self.value_map_ge))
|
||||
self.register_operator(
|
||||
Operator("=", self.type_map_compare, self.value_map_eq))
|
||||
self.register_operator(
|
||||
Operator("!=", self.type_map_compare, self.value_map_ne))
|
||||
|
||||
# Logical operators
|
||||
self.register_operator(
|
||||
Operator("and", self.type_map_logical, self.value_map_and))
|
||||
self.register_operator(
|
||||
Operator("or", self.type_map_logical, self.value_map_or))
|
||||
self.register_operator(
|
||||
Operator("not", self.type_map_unary_logical, self.value_map_not))
|
||||
|
||||
# Type mapping functions
|
||||
|
||||
def type_map_arith(self, args: list[Type]) -> Type:
|
||||
"""Type map for arithmetic operators: (T T) -> T where T is int or real"""
|
||||
if len(args) != 2:
|
||||
raise TypeError(
|
||||
f"Arithmetic operator expects 2 arguments, got {len(args)}")
|
||||
|
||||
t1, t2 = args
|
||||
|
||||
# If either is real, result is real
|
||||
if t1 == BaseType.REAL or t2 == BaseType.REAL:
|
||||
return BaseType.REAL
|
||||
|
||||
# Both must be int
|
||||
if t1 == BaseType.INT and t2 == BaseType.INT:
|
||||
return BaseType.INT
|
||||
|
||||
raise TypeError(
|
||||
f"Arithmetic operator requires int or real, got {t1} and {t2}"
|
||||
)
|
||||
|
||||
def type_map_compare(self, args: list[Type]) -> Type:
|
||||
"""Type map for comparison operators: (T T) -> bool"""
|
||||
if len(args) != 2:
|
||||
raise TypeError(
|
||||
f"Comparison operator expects 2 arguments, got {len(args)}")
|
||||
|
||||
t1, t2 = args
|
||||
|
||||
# Can compare int with int, real with real, or int with real
|
||||
if t1 in (BaseType.INT, BaseType.REAL) and t2 in (BaseType.INT, BaseType.REAL):
|
||||
return BaseType.BOOL
|
||||
|
||||
if t1 == t2 and t1 in (BaseType.INT, BaseType.REAL, BaseType.STRING):
|
||||
return BaseType.BOOL
|
||||
|
||||
raise TypeError(f"Cannot compare {t1} with {t2}")
|
||||
|
||||
def type_map_logical(self, args: list[Type]) -> Type:
|
||||
"""Type map for binary logical operators: (bool bool) -> bool"""
|
||||
if len(args) != 2:
|
||||
raise TypeError(
|
||||
f"Logical operator expects 2 arguments, got {len(args)}")
|
||||
|
||||
if args[0] != BaseType.BOOL or args[1] != BaseType.BOOL:
|
||||
raise TypeError(
|
||||
f"Logical operator requires bool arguments, got {args[0]} and {args[1]}"
|
||||
)
|
||||
|
||||
return BaseType.BOOL
|
||||
|
||||
def type_map_unary_logical(self, args: list[Type]) -> Type:
|
||||
"""Type map for unary logical operators: (bool) -> bool"""
|
||||
if len(args) != 1:
|
||||
raise TypeError(
|
||||
f"Unary logical operator expects 1 argument, got {len(args)}")
|
||||
|
||||
if args[0] != BaseType.BOOL:
|
||||
raise TypeError(
|
||||
f"Logical operator requires bool argument, got {args[0]}"
|
||||
)
|
||||
|
||||
return BaseType.BOOL
|
||||
|
||||
# Value mapping functions
|
||||
|
||||
def value_map_add(self, args: list[NestedList]) -> NestedList:
|
||||
"""Addition: a + b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a + b
|
||||
return atom(result)
|
||||
|
||||
def value_map_sub(self, args: list[NestedList]) -> NestedList:
|
||||
"""Subtraction: a - b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a - b
|
||||
return atom(result)
|
||||
|
||||
def value_map_mul(self, args: list[NestedList]) -> NestedList:
|
||||
"""Multiplication: a * b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a * b
|
||||
return atom(result)
|
||||
|
||||
def value_map_div(self, args: list[NestedList]) -> NestedList:
|
||||
"""Division: a / b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Division by zero")
|
||||
|
||||
# Return float for division
|
||||
result = a / b
|
||||
return atom(result)
|
||||
|
||||
def value_map_lt(self, args: list[NestedList]) -> NestedList:
|
||||
"""Less than: a < b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a < b
|
||||
return atom(result)
|
||||
|
||||
def value_map_gt(self, args: list[NestedList]) -> NestedList:
|
||||
"""Greater than: a > b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a > b
|
||||
return atom(result)
|
||||
|
||||
def value_map_le(self, args: list[NestedList]) -> NestedList:
|
||||
"""Less than or equal: a <= b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a <= b
|
||||
return atom(result)
|
||||
|
||||
def value_map_ge(self, args: list[NestedList]) -> NestedList:
|
||||
"""Greater than or equal: a >= b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a >= b
|
||||
return atom(result)
|
||||
|
||||
def value_map_eq(self, args: list[NestedList]) -> NestedList:
|
||||
"""Equal: a = b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a == b
|
||||
return atom(result)
|
||||
|
||||
def value_map_ne(self, args: list[NestedList]) -> NestedList:
|
||||
"""Not equal: a != b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a != b
|
||||
return atom(result)
|
||||
|
||||
def value_map_and(self, args: list[NestedList]) -> NestedList:
|
||||
"""Logical and: a and b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a and b
|
||||
return atom(result)
|
||||
|
||||
def value_map_or(self, args: list[NestedList]) -> NestedList:
|
||||
"""Logical or: a or b"""
|
||||
a, b = args[0].value, args[1].value
|
||||
result = a or b
|
||||
return atom(result)
|
||||
|
||||
def value_map_not(self, args: list[NestedList]) -> NestedList:
|
||||
"""Logical not: not a"""
|
||||
a = args[0].value
|
||||
result = not a
|
||||
return atom(result)
|
||||
9
pysecondo/core/__init__.py
Normal file
9
pysecondo/core/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Core modules for PySECONDO
|
||||
"""
|
||||
|
||||
from .nested_list import NestedList, NestedListType
|
||||
from .types import Type
|
||||
from .type_system import TypeChecker
|
||||
|
||||
__all__ = ["NestedList", "NestedListType", "Type", "TypeChecker"]
|
||||
167
pysecondo/core/nested_list.py
Normal file
167
pysecondo/core/nested_list.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Nested List - The core data structure of SECONDO
|
||||
|
||||
In SECONDO, everything is represented as nested lists:
|
||||
- Atomic values: int, real, string, bool
|
||||
- Lists: (value1 value2 value3)
|
||||
- Types: (rel (tuple ((Name string)(Population int))))
|
||||
|
||||
This implementation uses Python's built-in types:
|
||||
- Atomic: int, float, str, bool
|
||||
- List: list
|
||||
- Type tags: wrapped in special objects or type annotations
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class NestedListType(Enum):
|
||||
"""Types of nested list nodes"""
|
||||
ATOM = "atom" # Atomic value (int, string, bool, real)
|
||||
LIST = "list" # Nested list
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedList:
|
||||
"""
|
||||
Nested list representation
|
||||
|
||||
Examples:
|
||||
# Atomic values
|
||||
nl_int = NestedList.atom(42)
|
||||
nl_str = NestedList.atom("Beijing")
|
||||
nl_bool = NestedList.atom(True)
|
||||
|
||||
# Lists
|
||||
nl_list = NestedList.list([
|
||||
NestedList.atom(1),
|
||||
NestedList.atom(2),
|
||||
NestedList.atom(3)
|
||||
])
|
||||
# Represents: (1 2 3)
|
||||
|
||||
# Nested structures
|
||||
nl_tuple = NestedList.list([
|
||||
NestedList.atom("Beijing"),
|
||||
NestedList.atom(21540000)
|
||||
])
|
||||
# Represents: ("Beijing" 21540000)
|
||||
|
||||
nl_rel = NestedList.list([
|
||||
nl_tuple,
|
||||
NestedList.list([
|
||||
NestedList.atom("Shanghai"),
|
||||
NestedList.atom(24280000)
|
||||
])
|
||||
])
|
||||
# Represents: (("Beijing" 21540000)("Shanghai" 24280000))
|
||||
"""
|
||||
value: Any
|
||||
type: NestedListType
|
||||
|
||||
@staticmethod
|
||||
def atom(value: Union[int, float, str, bool]) -> "NestedList":
|
||||
"""Create an atomic nested list value"""
|
||||
return NestedList(value, NestedListType.ATOM)
|
||||
|
||||
@staticmethod
|
||||
def list(items: list) -> "NestedList":
|
||||
"""Create a list nested list value"""
|
||||
return NestedList(items, NestedListType.LIST)
|
||||
|
||||
def is_atom(self) -> bool:
|
||||
"""Check if this is an atomic value"""
|
||||
return self.type == NestedListType.ATOM
|
||||
|
||||
def is_list(self) -> bool:
|
||||
"""Check if this is a list"""
|
||||
return self.type == NestedListType.LIST
|
||||
|
||||
def to_python(self) -> Any:
|
||||
"""
|
||||
Convert nested list to Python native type
|
||||
|
||||
Examples:
|
||||
atom(5) -> 5
|
||||
list([atom(1), atom(2)]) -> [1, 2]
|
||||
list([atom("a"), list([atom(1), atom(2)])]) -> ["a", [1, 2]]
|
||||
"""
|
||||
if self.is_atom():
|
||||
return self.value
|
||||
else:
|
||||
return [item.to_python() if isinstance(item, NestedList) else item
|
||||
for item in self.value]
|
||||
|
||||
@classmethod
|
||||
def from_python(cls, value: Any) -> "NestedList":
|
||||
"""
|
||||
Create nested list from Python native type
|
||||
|
||||
Examples:
|
||||
5 -> atom(5)
|
||||
[1, 2, 3] -> list([atom(1), atom(2), atom(3)])
|
||||
["a", [1, 2]] -> list([atom("a"), list([atom(1), atom(2)])])
|
||||
"""
|
||||
# Already a NestedList, return as-is
|
||||
if isinstance(value, NestedList):
|
||||
return value
|
||||
if isinstance(value, (int, float, str, bool)):
|
||||
return cls.atom(value)
|
||||
elif isinstance(value, list):
|
||||
return cls.list([cls.from_python(v) for v in value])
|
||||
else:
|
||||
raise TypeError(f"Cannot convert {type(value)} to NestedList")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""SECONDO-style string representation"""
|
||||
if self.is_atom():
|
||||
if isinstance(self.value, str):
|
||||
return f'"{self.value}"'
|
||||
return str(self.value)
|
||||
else:
|
||||
inner = " ".join(item.__repr__() for item in self.value)
|
||||
return f"({inner})"
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
"""Equality comparison"""
|
||||
if not isinstance(other, NestedList):
|
||||
return False
|
||||
if self.type != other.type:
|
||||
return False
|
||||
return self.value == other.value
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Length of list, raises error for atoms"""
|
||||
if self.is_atom():
|
||||
raise TypeError("Atomic values have no length")
|
||||
return len(self.value)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Index access for lists"""
|
||||
if self.is_atom():
|
||||
raise TypeError("Cannot index atomic values")
|
||||
return self.value[index]
|
||||
|
||||
|
||||
# Convenience functions for creating nested lists
|
||||
def nl(value: Any) -> NestedList:
|
||||
"""Convenience function to create nested list from Python value"""
|
||||
return NestedList.from_python(value)
|
||||
|
||||
|
||||
def atom(value: Union[int, float, str, bool]) -> NestedList:
|
||||
"""Create atomic nested list"""
|
||||
return NestedList.atom(value)
|
||||
|
||||
|
||||
def list_nl(*items: Any) -> NestedList:
|
||||
"""Create list nested list from items"""
|
||||
converted = []
|
||||
for item in items:
|
||||
if isinstance(item, NestedList):
|
||||
converted.append(item)
|
||||
else:
|
||||
converted.append(NestedList.from_python(item))
|
||||
return NestedList.list(converted)
|
||||
138
pysecondo/core/type_system.py
Normal file
138
pysecondo/core/type_system.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Type Checker for PySECONDO
|
||||
|
||||
Provides type checking and type inference for nested list values.
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
from .nested_list import NestedList, NestedListType
|
||||
from .types import Type, BaseType, TupleType, RelationType, Attribute
|
||||
|
||||
|
||||
class TypeError(Exception):
|
||||
"""Type error in SECONDO"""
|
||||
pass
|
||||
|
||||
|
||||
class TypeChecker:
|
||||
"""
|
||||
Type checker for nested list values
|
||||
|
||||
Verifies that a nested list value matches a given type.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def check(value: NestedList, expected_type: Type) -> bool:
|
||||
"""
|
||||
Check if a nested list value matches the expected type
|
||||
|
||||
Examples:
|
||||
checker = TypeChecker()
|
||||
checker.check(atom(5), BaseType.INT) -> True
|
||||
checker.check(atom("hello"), BaseType.INT) -> False
|
||||
checker.check(
|
||||
list_nl([atom("Beijing"), atom(21540000)]),
|
||||
TupleType([Attribute("Name", BaseType.STRING), Attribute("Pop", BaseType.INT)])
|
||||
) -> True
|
||||
"""
|
||||
try:
|
||||
TypeChecker._check(value, expected_type)
|
||||
return True
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _check(value: NestedList, expected_type: Type) -> None:
|
||||
"""Internal type checking method"""
|
||||
|
||||
if isinstance(expected_type, BaseType):
|
||||
TypeChecker._check_base_type(value, expected_type)
|
||||
|
||||
elif isinstance(expected_type, TupleType):
|
||||
TypeChecker._check_tuple(value, expected_type)
|
||||
|
||||
elif isinstance(expected_type, RelationType):
|
||||
TypeChecker._check_relation(value, expected_type)
|
||||
|
||||
else:
|
||||
raise TypeError(f"Unsupported type: {type(expected_type)}")
|
||||
|
||||
@staticmethod
|
||||
def _check_base_type(value: NestedList, expected_type: BaseType) -> None:
|
||||
"""Check base type"""
|
||||
if not value.is_atom():
|
||||
raise TypeError(f"Expected {expected_type.value}, got list")
|
||||
|
||||
if expected_type == BaseType.INT:
|
||||
if not isinstance(value.value, int):
|
||||
raise TypeError(f"Expected int, got {type(value.value)}")
|
||||
elif expected_type == BaseType.REAL:
|
||||
if not isinstance(value.value, (int, float)):
|
||||
raise TypeError(f"Expected real, got {type(value.value)}")
|
||||
elif expected_type == BaseType.STRING:
|
||||
if not isinstance(value.value, str):
|
||||
raise TypeError(f"Expected string, got {type(value.value)}")
|
||||
elif expected_type == BaseType.BOOL:
|
||||
if not isinstance(value.value, bool):
|
||||
raise TypeError(f"Expected bool, got {type(value.value)}")
|
||||
|
||||
@staticmethod
|
||||
def _check_tuple(value: NestedList, expected_type: TupleType) -> None:
|
||||
"""Check tuple type"""
|
||||
if not value.is_list():
|
||||
raise TypeError(f"Expected tuple, got atom")
|
||||
|
||||
if len(value) != len(expected_type.attributes):
|
||||
raise TypeError(
|
||||
f"Tuple arity mismatch: expected {len(expected_type.attributes)}, "
|
||||
f"got {len(value)}"
|
||||
)
|
||||
|
||||
for attr_val, attr_def in zip(value.value, expected_type.attributes):
|
||||
TypeChecker._check(attr_val, attr_def.type)
|
||||
|
||||
@staticmethod
|
||||
def _check_relation(value: NestedList, expected_type: RelationType) -> None:
|
||||
"""Check relation type (list of tuples)"""
|
||||
if not value.is_list():
|
||||
raise TypeError(f"Expected relation, got atom")
|
||||
|
||||
# Each element should be a tuple
|
||||
for tuple_val in value.value:
|
||||
TypeChecker._check_tuple(tuple_val, expected_type.tuple_type)
|
||||
|
||||
@staticmethod
|
||||
def infer_type(value: NestedList) -> Type:
|
||||
"""
|
||||
Infer type from a nested list value
|
||||
|
||||
Examples:
|
||||
infer_type(atom(5)) -> BaseType.INT
|
||||
infer_type(list_nl([atom(1), atom(2)])) -> ListType(BaseType.INT)
|
||||
"""
|
||||
if value.is_atom():
|
||||
if isinstance(value.value, int):
|
||||
return BaseType.INT
|
||||
elif isinstance(value.value, float):
|
||||
return BaseType.REAL
|
||||
elif isinstance(value.value, str):
|
||||
return BaseType.STRING
|
||||
elif isinstance(value.value, bool):
|
||||
return BaseType.BOOL
|
||||
else:
|
||||
raise TypeError(f"Cannot infer type for {type(value.value)}")
|
||||
else:
|
||||
# For lists, try to infer from first element
|
||||
if len(value.value) == 0:
|
||||
raise TypeError("Cannot infer type for empty list")
|
||||
|
||||
# All elements should have same type
|
||||
first_type = TypeChecker.infer_type(value.value[0])
|
||||
|
||||
# Check if it looks like a tuple relation
|
||||
if all(TypeChecker.check(item, first_type) for item in value.value):
|
||||
# Could be a relation if all elements are tuples
|
||||
if isinstance(first_type, TupleType):
|
||||
return RelationType(first_type)
|
||||
|
||||
return first_type
|
||||
165
pysecondo/core/types.py
Normal file
165
pysecondo/core/types.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Type System for PySECONDO
|
||||
|
||||
SECONDO uses nested list notation for types:
|
||||
- Basic types: int, real, string, bool
|
||||
- Tuple types: (tuple ((name1 type1)(name2 type2)))
|
||||
- Relation types: (rel tuple_type)
|
||||
- List types: (type1 type2) # list of type2
|
||||
|
||||
Examples:
|
||||
int # integer type
|
||||
string # string type
|
||||
(tuple ((x int)(y real))) # 2D point
|
||||
(rel (tuple ((Name string)(Population int)))) # relation
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import re
|
||||
|
||||
|
||||
class BaseType(Enum):
|
||||
"""Basic data types in SECONDO"""
|
||||
INT = "int"
|
||||
REAL = "real"
|
||||
STRING = "string"
|
||||
BOOL = "bool"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Attribute:
|
||||
"""Attribute definition for tuples"""
|
||||
name: str
|
||||
type: "Type"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.name} {self.type})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TupleType:
|
||||
"""Tuple type: (tuple ((attr1 type1)(attr2 type2))...)"""
|
||||
attributes: List[Attribute]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = " ".join(str(attr) for attr in self.attributes)
|
||||
return f"(tuple ({attrs}))"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RelationType:
|
||||
"""Relation type: (rel tuple_type)"""
|
||||
tuple_type: TupleType
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"(rel {self.tuple_type})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListType:
|
||||
"""List type: (element_type)"""
|
||||
element_type: "Type"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.element_type})"
|
||||
|
||||
|
||||
# Type is a union of all possible types
|
||||
Type = Union[BaseType, TupleType, RelationType, ListType]
|
||||
|
||||
|
||||
def parse_type(type_str: str) -> Type:
|
||||
"""
|
||||
Parse SECONDO type string into Type object
|
||||
|
||||
Examples:
|
||||
parse_type("int") -> BaseType.INT
|
||||
parse_type("(tuple ((x int)(y real)))") -> TupleType(...)
|
||||
parse_type("(rel (tuple ((Name string))))") -> RelationType(...)
|
||||
"""
|
||||
type_str = type_str.strip()
|
||||
|
||||
# Basic types
|
||||
if type_str == "int":
|
||||
return BaseType.INT
|
||||
elif type_str == "real":
|
||||
return BaseType.REAL
|
||||
elif type_str == "string":
|
||||
return BaseType.STRING
|
||||
elif type_str == "bool":
|
||||
return BaseType.BOOL
|
||||
|
||||
# Relation type
|
||||
if type_str.startswith("(rel ") and type_str.endswith(")"):
|
||||
inner = type_str[5:-1].strip()
|
||||
tuple_type = parse_type(inner)
|
||||
if not isinstance(tuple_type, TupleType):
|
||||
raise ValueError(
|
||||
f"Relation must contain a tuple type, got: {inner}")
|
||||
return RelationType(tuple_type)
|
||||
|
||||
# Tuple type
|
||||
if type_str.startswith("(tuple (") and type_str.endswith("))"):
|
||||
# Extract attributes: (tuple ((a1 t1)(a2 t2)...))
|
||||
# Remove "(tuple (" prefix and "))" suffix
|
||||
# [8:] skips "(tuple (", [:-2] removes "))"
|
||||
inner = type_str[8:-2].strip()
|
||||
attributes = []
|
||||
i = 0
|
||||
|
||||
while i < len(inner):
|
||||
# Skip whitespace
|
||||
while i < len(inner) and inner[i] in ' \t\n':
|
||||
i += 1
|
||||
|
||||
if i >= len(inner):
|
||||
break
|
||||
|
||||
# Each attribute starts with '('
|
||||
if inner[i] != '(':
|
||||
raise ValueError(
|
||||
f"Expected '(' at position {i}, got '{inner[i]}'")
|
||||
|
||||
# Find the matching closing paren
|
||||
depth = 0
|
||||
start = i
|
||||
while i < len(inner):
|
||||
if inner[i] == '(':
|
||||
depth += 1
|
||||
elif inner[i] == ')':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
i += 1
|
||||
|
||||
# Extract attribute string: "(name type)"
|
||||
attr_str = inner[start+1:i].strip() # Remove outer parens
|
||||
|
||||
# Split into name and type (first word is name, rest is type)
|
||||
parts = attr_str.split(None, 1) # Split on first whitespace
|
||||
if len(parts) == 2:
|
||||
name, type_str_attr = parts
|
||||
attr_type = parse_type(type_str_attr.strip())
|
||||
attributes.append(Attribute(name, attr_type))
|
||||
|
||||
i += 1
|
||||
|
||||
return TupleType(attributes)
|
||||
|
||||
raise ValueError(f"Unknown type: {type_str}")
|
||||
|
||||
|
||||
def type_to_string(type_obj: Type) -> str:
|
||||
"""Convert Type object to SECONDO type string"""
|
||||
if isinstance(type_obj, BaseType):
|
||||
return type_obj.value
|
||||
elif isinstance(type_obj, TupleType):
|
||||
return str(type_obj)
|
||||
elif isinstance(type_obj, RelationType):
|
||||
return str(type_obj)
|
||||
elif isinstance(type_obj, ListType):
|
||||
return str(type_obj)
|
||||
else:
|
||||
raise TypeError(f"Unknown type object: {type_obj}")
|
||||
15
pysecondo/parser/__init__.py
Normal file
15
pysecondo/parser/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Parser module for PySECONDO
|
||||
"""
|
||||
|
||||
from .parser import Parser, parse_query, CreateCommand, UpdateCommand, QueryCommand
|
||||
from .evaluator import Evaluator
|
||||
|
||||
__all__ = [
|
||||
"Parser",
|
||||
"parse_query",
|
||||
"CreateCommand",
|
||||
"UpdateCommand",
|
||||
"QueryCommand",
|
||||
"Evaluator"
|
||||
]
|
||||
319
pysecondo/parser/evaluator.py
Normal file
319
pysecondo/parser/evaluator.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
Expression Evaluator for PySECONDO
|
||||
|
||||
Evaluates query expressions using the algebra system.
|
||||
Handles identifier lookup and operator execution.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import List, Any, Tuple
|
||||
from pysecondo.core.types import Type, BaseType
|
||||
from pysecondo.core.nested_list import NestedList, atom, list_nl
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""
|
||||
Expression evaluator
|
||||
|
||||
Evaluates expressions like:
|
||||
- identifier
|
||||
- identifier feed consume
|
||||
- identifier feed count
|
||||
- 5 + 3
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
algebra_manager: AlgebraManager,
|
||||
storage: MemoryStorage
|
||||
):
|
||||
self.algebra_manager = algebra_manager
|
||||
self.storage = storage
|
||||
|
||||
def evaluate(self, tokens: List[str]) -> Tuple[NestedList, Type]:
|
||||
"""
|
||||
Evaluate a tokenized expression
|
||||
|
||||
Args:
|
||||
tokens: List of tokens from parser
|
||||
|
||||
Returns:
|
||||
Tuple of (value, type)
|
||||
|
||||
Raises:
|
||||
ValueError: If evaluation fails
|
||||
"""
|
||||
if not tokens:
|
||||
raise ValueError("Empty expression")
|
||||
|
||||
# Handle single token
|
||||
if len(tokens) == 1:
|
||||
return self.evaluate_single(tokens[0])
|
||||
|
||||
# Check if first token is a unary operator (prefix notation like "not false")
|
||||
unary_ops = {'feed', 'consume', 'count', 'filter', 'not'}
|
||||
if tokens[0] in unary_ops:
|
||||
op_name = tokens[0]
|
||||
if len(tokens) < 2:
|
||||
raise ValueError(
|
||||
f"Unary operator {op_name} requires an operand")
|
||||
|
||||
if op_name == 'filter':
|
||||
# filter needs a predicate
|
||||
if len(tokens) < 3:
|
||||
raise ValueError("filter requires a predicate")
|
||||
operand_token = tokens[1]
|
||||
pred_token = tokens[2]
|
||||
operand_value, operand_type = self.evaluate_single(
|
||||
operand_token)
|
||||
pred_value, pred_type = self.evaluate_single(pred_token)
|
||||
return self.execute_operator(
|
||||
op_name,
|
||||
[operand_value, pred_value],
|
||||
[operand_type, pred_type]
|
||||
)
|
||||
else:
|
||||
# Simple unary operator
|
||||
operand_token = tokens[1]
|
||||
operand_value, operand_type = self.evaluate_single(
|
||||
operand_token)
|
||||
return self.execute_operator(
|
||||
op_name,
|
||||
[operand_value],
|
||||
[operand_type]
|
||||
)
|
||||
|
||||
# Start with first token (value or identifier)
|
||||
current_value, current_type = self.evaluate_single(tokens[0])
|
||||
i = 1
|
||||
|
||||
# Process operator chain
|
||||
while i < len(tokens):
|
||||
op_name = tokens[i]
|
||||
operator = self.algebra_manager.get_operator(op_name)
|
||||
|
||||
if operator is None:
|
||||
# Try to evaluate as a single token (might be an identifier)
|
||||
try:
|
||||
right_value, right_type = self.evaluate_single(op_name)
|
||||
current_value, current_type = right_value, right_type
|
||||
i += 1
|
||||
continue
|
||||
except:
|
||||
raise ValueError(f"Unknown operator: {op_name}")
|
||||
|
||||
# Check if operator is unary or binary
|
||||
# Unary operators: feed, consume, count, filter, not
|
||||
unary_ops = {'feed', 'consume', 'count', 'filter', 'not'}
|
||||
binary_ops = {'+', '-', '*', '/', '<', '>', '=', '!=', '<=', '>=',
|
||||
'and', 'or'}
|
||||
|
||||
if op_name in unary_ops:
|
||||
# Unary operator: apply to current value
|
||||
if op_name == 'filter':
|
||||
# filter needs a predicate argument
|
||||
if i + 1 >= len(tokens):
|
||||
raise ValueError("filter requires a predicate")
|
||||
pred_token = tokens[i + 1]
|
||||
pred_value, pred_type = self.evaluate_single(pred_token)
|
||||
current_value, current_type = self.execute_operator(
|
||||
op_name,
|
||||
[current_value, pred_value],
|
||||
[current_type, pred_type]
|
||||
)
|
||||
i += 2
|
||||
else:
|
||||
current_value, current_type = self.execute_operator(
|
||||
op_name,
|
||||
[current_value],
|
||||
[current_type]
|
||||
)
|
||||
i += 1
|
||||
elif op_name in binary_ops:
|
||||
# Binary operator: need right operand
|
||||
if i + 1 >= len(tokens):
|
||||
raise ValueError(
|
||||
f"Binary operator {op_name} requires right operand")
|
||||
|
||||
right_token = tokens[i + 1]
|
||||
right_value, right_type = self.evaluate_single(right_token)
|
||||
|
||||
current_value, current_type = self.execute_operator(
|
||||
op_name,
|
||||
[current_value, right_value],
|
||||
[current_type, right_type]
|
||||
)
|
||||
i += 2
|
||||
else:
|
||||
# Assume unary
|
||||
current_value, current_type = self.execute_operator(
|
||||
op_name,
|
||||
[current_value],
|
||||
[current_type]
|
||||
)
|
||||
i += 1
|
||||
|
||||
return current_value, current_type
|
||||
|
||||
def evaluate_single(self, token: str) -> Tuple[NestedList, Type]:
|
||||
"""
|
||||
Evaluate a single token
|
||||
|
||||
Returns:
|
||||
Tuple of (value, type)
|
||||
"""
|
||||
# Boolean (check before identifier since 'true'/'false' are valid identifiers)
|
||||
if token.lower() == 'true':
|
||||
return atom(True), BaseType.BOOL
|
||||
if token.lower() == 'false':
|
||||
return atom(False), BaseType.BOOL
|
||||
|
||||
# String
|
||||
if self.is_string(token):
|
||||
value = atom(token[1:-1]) # Remove quotes
|
||||
return value, BaseType.STRING
|
||||
|
||||
# Number
|
||||
if self.is_number(token):
|
||||
return self.parse_number(token)
|
||||
|
||||
# Nested list value
|
||||
if token.startswith('(') and token.endswith(')'):
|
||||
return self.parse_nested_list(token)
|
||||
|
||||
# Identifier (check last since it matches many patterns)
|
||||
if self.is_identifier(token):
|
||||
return self.lookup_identifier(token)
|
||||
|
||||
raise ValueError(f"Cannot evaluate token: {token}")
|
||||
|
||||
def lookup_identifier(self, name: str) -> Tuple[NestedList, Type]:
|
||||
"""Look up an identifier in storage"""
|
||||
value = self.storage.get_object(name)
|
||||
if value is None:
|
||||
raise ValueError(f"Unknown identifier: {name}")
|
||||
|
||||
obj_type = self.storage.get_type(name)
|
||||
return value, obj_type
|
||||
|
||||
def parse_number(self, token: str) -> Tuple[NestedList, Type]:
|
||||
"""Parse a number token"""
|
||||
try:
|
||||
if '.' in token:
|
||||
value = atom(float(token))
|
||||
return value, BaseType.REAL
|
||||
else:
|
||||
value = atom(int(token))
|
||||
return value, BaseType.INT
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid number: {token}")
|
||||
|
||||
def parse_nested_list(self, token: str) -> Tuple[NestedList, Type]:
|
||||
"""
|
||||
Parse a nested list token
|
||||
|
||||
This is a simplified parser that handles basic nested lists.
|
||||
"""
|
||||
# Remove outer parentheses
|
||||
inner = token[1:-1].strip()
|
||||
|
||||
if not inner:
|
||||
# Empty list
|
||||
value = list_nl()
|
||||
return value, BaseType.INT # Default type
|
||||
|
||||
# Try to parse as list of values
|
||||
# For simplicity, just handle comma-separated values
|
||||
parts = self.split_list(inner)
|
||||
|
||||
values = []
|
||||
for part in parts:
|
||||
part = part.strip()
|
||||
if self.is_number(part):
|
||||
val, _ = self.parse_number(part)
|
||||
values.append(val)
|
||||
elif self.is_string(part):
|
||||
val, _ = self.evaluate_single(part)
|
||||
values.append(val)
|
||||
else:
|
||||
# Assume it's a nested list
|
||||
val, _ = self.parse_nested_list(part)
|
||||
values.append(val)
|
||||
|
||||
value = list_nl(*values)
|
||||
# Type inference would happen here
|
||||
return value, BaseType.INT
|
||||
|
||||
def split_list(self, s: str) -> List[str]:
|
||||
"""
|
||||
Split a list string into parts
|
||||
|
||||
Handles nested parentheses correctly.
|
||||
"""
|
||||
parts = []
|
||||
current = []
|
||||
depth = 0
|
||||
|
||||
for char in s:
|
||||
if char == '(':
|
||||
depth += 1
|
||||
current.append(char)
|
||||
elif char == ')':
|
||||
depth -= 1
|
||||
current.append(char)
|
||||
elif char in ' \t' and depth == 0:
|
||||
if current:
|
||||
parts.append(''.join(current))
|
||||
current = []
|
||||
else:
|
||||
current.append(char)
|
||||
|
||||
if current:
|
||||
parts.append(''.join(current))
|
||||
|
||||
return parts
|
||||
|
||||
def execute_operator(
|
||||
self,
|
||||
op_name: str,
|
||||
args: List[NestedList],
|
||||
arg_types: List[Type]
|
||||
) -> Tuple[NestedList, Type]:
|
||||
"""
|
||||
Execute an operator
|
||||
|
||||
Returns:
|
||||
Tuple of (result_value, result_type)
|
||||
"""
|
||||
operator = self.algebra_manager.get_operator(op_name)
|
||||
|
||||
if operator is None:
|
||||
raise ValueError(f"Unknown operator: {op_name}")
|
||||
|
||||
# Type check
|
||||
result_type = operator.type_map(arg_types)
|
||||
|
||||
# Execute
|
||||
if operator.value_map is None:
|
||||
raise ValueError(f"Operator {op_name} has no value mapping")
|
||||
|
||||
result = operator.value_map(args)
|
||||
return result, result_type
|
||||
|
||||
def is_identifier(self, token: str) -> bool:
|
||||
"""Check if token is an identifier"""
|
||||
return bool(re.match(r'^[a-zA-Z_]\w*$', token))
|
||||
|
||||
def is_number(self, token: str) -> bool:
|
||||
"""Check if token is a number"""
|
||||
try:
|
||||
float(token)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def is_string(self, token: str) -> bool:
|
||||
"""Check if token is a string literal"""
|
||||
return token.startswith('"') and token.endswith('"')
|
||||
163
pysecondo/parser/parser.py
Normal file
163
pysecondo/parser/parser.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Simple Query Parser for PySECONDO
|
||||
|
||||
Parses SECONDO-like query syntax into executable commands.
|
||||
|
||||
Supported syntax:
|
||||
- create name : type
|
||||
- update name := value
|
||||
- query name
|
||||
- query expr1 op expr2
|
||||
- query name feed consume
|
||||
- query name feed count
|
||||
- query name feed filter[expr] consume
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import List, Optional, Union, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class CreateCommand:
|
||||
"""CREATE name : type"""
|
||||
name: str
|
||||
type_str: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdateCommand:
|
||||
"""UPDATE name := value"""
|
||||
name: str
|
||||
value: str # Nested list string representation
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueryCommand:
|
||||
"""QUERY expression"""
|
||||
expression: str
|
||||
|
||||
|
||||
Command = Union[CreateCommand, UpdateCommand, QueryCommand]
|
||||
|
||||
|
||||
class Parser:
|
||||
"""
|
||||
Simple parser for SECONDO queries
|
||||
|
||||
This is a simplified parser that handles basic SECONDO syntax.
|
||||
A full implementation would use a proper lexer and parser.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Patterns for different commands
|
||||
self.create_pattern = re.compile(
|
||||
r'^\s*create\s+(\w+)\s*:\s*\(.+\)\s*$', re.IGNORECASE
|
||||
)
|
||||
self.update_pattern = re.compile(
|
||||
r'^\s*update\s+(\w+)\s*:=\s*(.+)\s*$', re.IGNORECASE
|
||||
)
|
||||
self.query_pattern = re.compile(
|
||||
r'^\s*query\s+(.+)\s*$', re.IGNORECASE
|
||||
)
|
||||
|
||||
def parse(self, query: str) -> Optional[Command]:
|
||||
"""
|
||||
Parse a query string into a command
|
||||
|
||||
Args:
|
||||
query: Query string
|
||||
|
||||
Returns:
|
||||
Command object or None if parsing fails
|
||||
"""
|
||||
# Try create command
|
||||
match = self.create_pattern.match(query)
|
||||
if match:
|
||||
name = match.group(1)
|
||||
# Extract type string
|
||||
type_start = query.find(':') + 1
|
||||
type_str = query[type_start:].strip()
|
||||
return CreateCommand(name, type_str)
|
||||
|
||||
# Try update command
|
||||
match = self.update_pattern.match(query)
|
||||
if match:
|
||||
name = match.group(1)
|
||||
value = match.group(2).strip()
|
||||
return UpdateCommand(name, value)
|
||||
|
||||
# Try query command
|
||||
match = self.query_pattern.match(query)
|
||||
if match:
|
||||
expression = match.group(1).strip()
|
||||
return QueryCommand(expression)
|
||||
|
||||
return None
|
||||
|
||||
def parse_expression(self, expr: str) -> List:
|
||||
"""
|
||||
Parse an expression into tokens
|
||||
|
||||
This is a very simple tokenizer that splits on whitespace
|
||||
while keeping track of brackets.
|
||||
|
||||
Examples:
|
||||
"cities" -> ["cities"]
|
||||
"cities feed consume" -> ["cities", "feed", "consume"]
|
||||
"5 + 3" -> ["5", "+", "3"]
|
||||
"""
|
||||
tokens = []
|
||||
current = []
|
||||
paren_depth = 0
|
||||
bracket_depth = 0
|
||||
|
||||
for char in expr:
|
||||
if char in ' \t\n' and paren_depth == 0 and bracket_depth == 0:
|
||||
if current:
|
||||
tokens.append(''.join(current))
|
||||
current = []
|
||||
else:
|
||||
if char == '(':
|
||||
paren_depth += 1
|
||||
elif char == ')':
|
||||
paren_depth -= 1
|
||||
elif char == '[':
|
||||
bracket_depth += 1
|
||||
elif char == ']':
|
||||
bracket_depth -= 1
|
||||
current.append(char)
|
||||
|
||||
if current:
|
||||
tokens.append(''.join(current))
|
||||
|
||||
return tokens
|
||||
|
||||
def is_identifier(self, token: str) -> bool:
|
||||
"""Check if token is an identifier"""
|
||||
return bool(re.match(r'^[a-zA-Z_]\w*$', token))
|
||||
|
||||
def is_number(self, token: str) -> bool:
|
||||
"""Check if token is a number"""
|
||||
try:
|
||||
float(token)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def is_string(self, token: str) -> bool:
|
||||
"""Check if token is a string literal"""
|
||||
return token.startswith('"') and token.endswith('"')
|
||||
|
||||
def is_operator(self, token: str) -> bool:
|
||||
"""Check if token is an operator"""
|
||||
ops = {'+', '-', '*', '/', '<', '>', '=', '!', 'and', 'or', 'not'}
|
||||
return token in ops or token in {'<=', '>=', '!=', 'feed', 'consume',
|
||||
'filter', 'count'}
|
||||
|
||||
|
||||
# Convenience function
|
||||
def parse_query(query: str) -> Optional[Command]:
|
||||
"""Parse a query string"""
|
||||
parser = Parser()
|
||||
return parser.parse(query)
|
||||
120
pysecondo/query_processor.py
Normal file
120
pysecondo/query_processor.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Simple Query Processor for PySECONDO
|
||||
|
||||
Handles query execution and operator evaluation.
|
||||
For simplicity, this is a basic implementation without full parsing.
|
||||
"""
|
||||
|
||||
from typing import List, Any, Dict
|
||||
from pysecondo.core.types import Type, BaseType, RelationType, TupleType, parse_type
|
||||
from pysecondo.core.nested_list import NestedList
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
|
||||
|
||||
class QueryProcessor:
|
||||
"""
|
||||
Simple query processor
|
||||
|
||||
Handles:
|
||||
- Object creation: create name : type
|
||||
- Object updates: update name := value
|
||||
- Operator evaluation: op(arg1, arg2, ...)
|
||||
- Identifier lookup
|
||||
"""
|
||||
|
||||
def __init__(self, algebra_manager: AlgebraManager, storage: MemoryStorage):
|
||||
self.algebra_manager = algebra_manager
|
||||
self.storage = storage
|
||||
|
||||
def execute_create(self, name: str, type_str: str) -> None:
|
||||
"""
|
||||
Execute: create name : type
|
||||
|
||||
Creates a new empty object with the specified type.
|
||||
"""
|
||||
obj_type = parse_type(type_str)
|
||||
|
||||
# Create empty value based on type
|
||||
if isinstance(obj_type, RelationType):
|
||||
# Empty relation
|
||||
value = NestedList.list([])
|
||||
elif isinstance(obj_type, TupleType):
|
||||
# Empty tuple (invalid, but for error handling)
|
||||
value = NestedList.list([])
|
||||
else:
|
||||
# For basic types, we create a default value
|
||||
if obj_type == BaseType.INT:
|
||||
value = NestedList.atom(0)
|
||||
elif obj_type == BaseType.REAL:
|
||||
value = NestedList.atom(0.0)
|
||||
elif obj_type == BaseType.STRING:
|
||||
value = NestedList.atom("")
|
||||
elif obj_type == BaseType.BOOL:
|
||||
value = NestedList.atom(False)
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {obj_type}")
|
||||
|
||||
self.storage.create_object(name, value, obj_type)
|
||||
|
||||
def execute_update(self, name: str, value: NestedList) -> None:
|
||||
"""
|
||||
Execute: update name := value
|
||||
|
||||
Updates an existing object with a new value.
|
||||
"""
|
||||
if not self.storage.object_exists(name):
|
||||
raise ValueError(f"Object '{name}' does not exist")
|
||||
|
||||
obj_type = self.storage.get_type(name)
|
||||
self.storage.update_object(name, value, obj_type)
|
||||
|
||||
def evaluate_operator(
|
||||
self,
|
||||
op_name: str,
|
||||
args: List[NestedList],
|
||||
arg_types: List[Type]
|
||||
) -> NestedList:
|
||||
"""
|
||||
Evaluate an operator with given arguments
|
||||
|
||||
Args:
|
||||
op_name: Operator name
|
||||
args: Argument values
|
||||
arg_types: Argument types
|
||||
|
||||
Returns:
|
||||
Result of the operator
|
||||
"""
|
||||
operator = self.algebra_manager.get_operator(op_name)
|
||||
|
||||
if operator is None:
|
||||
raise ValueError(f"Unknown operator: {op_name}")
|
||||
|
||||
# Type check
|
||||
result_type = operator.type_map(arg_types)
|
||||
|
||||
# Execute operator
|
||||
if operator.value_map is None:
|
||||
raise ValueError(f"Operator {op_name} cannot be executed directly")
|
||||
|
||||
result = operator.value_map(args)
|
||||
return result
|
||||
|
||||
def lookup_identifier(self, name: str) -> NestedList:
|
||||
"""Look up an identifier in storage"""
|
||||
value = self.storage.get_object(name)
|
||||
|
||||
if value is None:
|
||||
raise ValueError(f"Unknown identifier: {name}")
|
||||
|
||||
return value
|
||||
|
||||
def get_identifier_type(self, name: str) -> Type:
|
||||
"""Get type of an identifier"""
|
||||
obj_type = self.storage.get_type(name)
|
||||
|
||||
if obj_type is None:
|
||||
raise ValueError(f"Unknown identifier: {name}")
|
||||
|
||||
return obj_type
|
||||
369
pysecondo/repl.py
Normal file
369
pysecondo/repl.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
Interactive REPL for PySECONDO
|
||||
|
||||
Provides an interactive shell for executing SECONDO queries.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import List
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
from pysecondo.algebras.standard import StandardAlgebra
|
||||
from pysecondo.algebras.relation import RelationAlgebra
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
from pysecondo.parser.parser import Parser, CreateCommand, UpdateCommand, QueryCommand
|
||||
from pysecondo.parser.evaluator import Evaluator
|
||||
from pysecondo.core.types import parse_type
|
||||
from pysecondo.core.nested_list import NestedList
|
||||
|
||||
|
||||
class REPL:
|
||||
"""
|
||||
Read-Eval-Print Loop for PySECONDO
|
||||
|
||||
Provides an interactive interface for:
|
||||
- Creating databases
|
||||
- Querying data
|
||||
- Exploring results
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the REPL with all components"""
|
||||
self.storage = MemoryStorage()
|
||||
self.algebra_manager = AlgebraManager()
|
||||
|
||||
# Register algebras
|
||||
self.algebra_manager.register_algebra(
|
||||
"StandardAlgebra", StandardAlgebra())
|
||||
self.algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(self.storage)
|
||||
)
|
||||
|
||||
self.parser = Parser()
|
||||
self.evaluator = Evaluator(self.algebra_manager, self.storage)
|
||||
|
||||
self.running = False
|
||||
self.verbose = True
|
||||
|
||||
def print_banner(self):
|
||||
"""Print welcome banner"""
|
||||
print("=" * 60)
|
||||
print(" PySECONDO - Interactive Shell")
|
||||
print(" A minimal implementation of SECONDO in Python")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Commands:")
|
||||
print(" create name : type - Create a relation")
|
||||
print(" update name := value - Insert/update data")
|
||||
print(" query expression - Execute a query")
|
||||
print(" list - List all objects")
|
||||
print(" type name - Show object type")
|
||||
print(" help - Show this help")
|
||||
print(" quit - Exit the shell")
|
||||
print()
|
||||
print("Examples:")
|
||||
print(" create cities : (rel (tuple ((Name string)(Population int))))")
|
||||
print(' update cities := (("Beijing" 21540000)("Shanghai" 24280000))')
|
||||
print(" query cities feed count")
|
||||
print(" query 5 + 3")
|
||||
print()
|
||||
|
||||
def print_result(self, value: NestedList, max_depth: int = 3):
|
||||
"""
|
||||
Print a nested list result nicely
|
||||
|
||||
Args:
|
||||
value: Nested list to print
|
||||
max_depth: Maximum depth to display
|
||||
"""
|
||||
if value.is_atom():
|
||||
print(f" {value.value}")
|
||||
else:
|
||||
self._print_list(
|
||||
value, indent=2, max_depth=max_depth, current_depth=0)
|
||||
|
||||
def _print_list(self, value: NestedList, indent: int, max_depth: int, current_depth: int):
|
||||
"""Recursively print a nested list"""
|
||||
if current_depth >= max_depth:
|
||||
print(" " * indent + "...")
|
||||
return
|
||||
|
||||
print(" " * indent + "(")
|
||||
for item in value.value:
|
||||
if item.is_atom():
|
||||
if isinstance(item.value, str):
|
||||
print(" " * (indent + 2) + f'"{item.value}"')
|
||||
else:
|
||||
print(" " * (indent + 2) + str(item.value))
|
||||
else:
|
||||
self._print_list(item, indent + 2, max_depth,
|
||||
current_depth + 1)
|
||||
print(" " * indent + ")")
|
||||
|
||||
def execute(self, query: str) -> bool:
|
||||
"""
|
||||
Execute a query
|
||||
|
||||
Args:
|
||||
query: Query string
|
||||
|
||||
Returns:
|
||||
True if execution succeeded, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Parse the query
|
||||
command = self.parser.parse(query)
|
||||
|
||||
if command is None:
|
||||
print(f"Error: Could not parse query: {query}")
|
||||
return False
|
||||
|
||||
# Handle different command types
|
||||
if isinstance(command, CreateCommand):
|
||||
return self.execute_create(command)
|
||||
elif isinstance(command, UpdateCommand):
|
||||
return self.execute_update(command)
|
||||
elif isinstance(command, QueryCommand):
|
||||
return self.execute_query(command)
|
||||
else:
|
||||
print(f"Error: Unknown command type")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
if self.verbose:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def execute_create(self, command: CreateCommand) -> bool:
|
||||
"""Execute CREATE command"""
|
||||
try:
|
||||
# Check if object already exists
|
||||
if self.storage.object_exists(command.name):
|
||||
print(f"Error: Object '{command.name}' already exists")
|
||||
return False
|
||||
|
||||
# Parse type
|
||||
obj_type = parse_type(command.type_str)
|
||||
|
||||
# Create empty value
|
||||
from pysecondo.core.types import RelationType, BaseType
|
||||
if isinstance(obj_type, RelationType):
|
||||
value = NestedList.list([])
|
||||
elif obj_type == BaseType.INT:
|
||||
value = NestedList.atom(0)
|
||||
elif obj_type == BaseType.REAL:
|
||||
value = NestedList.atom(0.0)
|
||||
elif obj_type == BaseType.STRING:
|
||||
value = NestedList.atom("")
|
||||
elif obj_type == BaseType.BOOL:
|
||||
value = NestedList.atom(False)
|
||||
else:
|
||||
value = NestedList.list([])
|
||||
|
||||
self.storage.create_object(command.name, value, obj_type)
|
||||
print(f"Created: {command.name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating object: {e}")
|
||||
return False
|
||||
|
||||
def execute_update(self, command: UpdateCommand) -> bool:
|
||||
"""Execute UPDATE command"""
|
||||
try:
|
||||
# Parse the value (nested list)
|
||||
# For simplicity, we use Python's eval with restrictions
|
||||
# In production, use a proper parser
|
||||
|
||||
# Convert SECONDO syntax to Python
|
||||
value_str = command.value
|
||||
|
||||
# Simple parsing: handle ("str" num) patterns
|
||||
# This is a very basic parser
|
||||
value = self._parse_nested_list(value_str)
|
||||
|
||||
if not self.storage.object_exists(command.name):
|
||||
print(f"Error: Object '{command.name}' does not exist")
|
||||
return False
|
||||
|
||||
obj_type = self.storage.get_type(command.name)
|
||||
self.storage.update_object(command.name, value, obj_type)
|
||||
|
||||
print(f"Updated: {command.name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating object: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _parse_nested_list(self, s: str) -> NestedList:
|
||||
"""
|
||||
Parse a nested list string
|
||||
|
||||
Very simple parser for ("value1" value2 ("nested" ...))
|
||||
"""
|
||||
s = s.strip()
|
||||
|
||||
if not s.startswith('(') or not s.endswith(')'):
|
||||
# Atomic value
|
||||
if s.startswith('"') and s.endswith('"'):
|
||||
return NestedList.atom(s[1:-1])
|
||||
try:
|
||||
if '.' in s:
|
||||
return NestedList.atom(float(s))
|
||||
else:
|
||||
return NestedList.atom(int(s))
|
||||
except ValueError:
|
||||
return NestedList.atom(s)
|
||||
|
||||
# Parse list
|
||||
inner = s[1:-1].strip()
|
||||
if not inner:
|
||||
return NestedList.list([])
|
||||
|
||||
items = []
|
||||
current = []
|
||||
depth = 0
|
||||
in_string = False
|
||||
|
||||
i = 0
|
||||
while i < len(inner):
|
||||
char = inner[i]
|
||||
|
||||
if char == '"' and (i == 0 or inner[i-1] != '\\'):
|
||||
in_string = not in_string
|
||||
current.append(char)
|
||||
elif in_string:
|
||||
current.append(char)
|
||||
elif char == '(':
|
||||
depth += 1
|
||||
current.append(char)
|
||||
elif char == ')':
|
||||
depth -= 1
|
||||
current.append(char)
|
||||
elif char in ' \t\n' and depth == 0:
|
||||
if current:
|
||||
items.append(''.join(current))
|
||||
current = []
|
||||
else:
|
||||
current.append(char)
|
||||
|
||||
i += 1
|
||||
|
||||
if current:
|
||||
items.append(''.join(current))
|
||||
|
||||
# Parse each item recursively
|
||||
parsed_items = []
|
||||
for item in items:
|
||||
parsed_items.append(self._parse_nested_list(item.strip()))
|
||||
|
||||
return NestedList.list(parsed_items)
|
||||
|
||||
def execute_query(self, command: QueryCommand) -> bool:
|
||||
"""Execute QUERY command"""
|
||||
try:
|
||||
tokens = self.parser.parse_expression(command.expression)
|
||||
value, value_type = self.evaluator.evaluate(tokens)
|
||||
|
||||
print("Result:")
|
||||
self.print_result(value)
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error executing query: {e}")
|
||||
import traceback
|
||||
if self.verbose:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def cmd_list(self, args: List[str]) -> bool:
|
||||
"""List all objects"""
|
||||
objects = self.storage.list_objects()
|
||||
|
||||
if not objects:
|
||||
print("No objects defined.")
|
||||
return True
|
||||
|
||||
print("Defined objects:")
|
||||
for name in objects:
|
||||
obj_type = self.storage.get_type(name)
|
||||
print(f" {name}: {obj_type}")
|
||||
|
||||
print()
|
||||
return True
|
||||
|
||||
def cmd_type(self, args: List[str]) -> bool:
|
||||
"""Show type of an object"""
|
||||
if len(args) < 1:
|
||||
print("Usage: type <name>")
|
||||
return False
|
||||
|
||||
name = args[0]
|
||||
if not self.storage.object_exists(name):
|
||||
print(f"Error: Unknown object '{name}'")
|
||||
return False
|
||||
|
||||
obj_type = self.storage.get_type(name)
|
||||
print(f"{name}: {obj_type}")
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
def cmd_help(self, args: List[str]) -> bool:
|
||||
"""Show help"""
|
||||
self.print_banner()
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
"""Run the REPL loop"""
|
||||
self.running = True
|
||||
self.print_banner()
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# Read input
|
||||
line = input("secondo> ").strip()
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Handle built-in commands
|
||||
parts = line.split(None, 1)
|
||||
cmd = parts[0].lower()
|
||||
args = parts[1:] if len(parts) > 1 else []
|
||||
|
||||
if cmd in ('quit', 'exit', 'q'):
|
||||
print("Goodbye!")
|
||||
break
|
||||
elif cmd == 'list':
|
||||
self.cmd_list(args)
|
||||
elif cmd == 'type':
|
||||
self.cmd_type(args)
|
||||
elif cmd == 'help':
|
||||
self.cmd_help(args)
|
||||
else:
|
||||
# Execute query
|
||||
self.execute(line)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nUse 'quit' to exit.")
|
||||
except EOFError:
|
||||
print("\nGoodbye!")
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
repl = REPL()
|
||||
repl.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
pysecondo/storage/__init__.py
Normal file
7
pysecondo/storage/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Storage module for PySECONDO
|
||||
"""
|
||||
|
||||
from .memory import MemoryStorage
|
||||
|
||||
__all__ = ["MemoryStorage"]
|
||||
82
pysecondo/storage/memory.py
Normal file
82
pysecondo/storage/memory.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
In-Memory Storage for PySECONDO
|
||||
|
||||
Simplified storage implementation (no Berkeley DB)
|
||||
Stores objects in memory dictionaries
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from pysecondo.core.nested_list import NestedList
|
||||
from pysecondo.core.types import Type
|
||||
|
||||
|
||||
class MemoryStorage:
|
||||
"""
|
||||
Simple in-memory storage for database objects
|
||||
|
||||
This is a simplified version of SECONDO's storage manager.
|
||||
In the real SECONDO, Berkeley DB is used for persistence.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize empty storage"""
|
||||
self.objects: Dict[str, NestedList] = {} # name -> value
|
||||
self.types: Dict[str, Type] = {} # name -> type
|
||||
|
||||
def create_object(self, name: str, value: NestedList, obj_type: Type) -> None:
|
||||
"""
|
||||
Create a new object in storage
|
||||
|
||||
Args:
|
||||
name: Object name
|
||||
value: Nested list value
|
||||
obj_type: Type of the object
|
||||
"""
|
||||
if name in self.objects:
|
||||
raise ValueError(f"Object '{name}' already exists")
|
||||
|
||||
self.objects[name] = value
|
||||
self.types[name] = obj_type
|
||||
|
||||
def update_object(self, name: str, value: NestedList, obj_type: Type) -> None:
|
||||
"""
|
||||
Update an existing object
|
||||
|
||||
Args:
|
||||
name: Object name
|
||||
value: New nested list value
|
||||
obj_type: Type of the object
|
||||
"""
|
||||
if name not in self.objects:
|
||||
raise ValueError(f"Object '{name}' does not exist")
|
||||
|
||||
self.objects[name] = value
|
||||
self.types[name] = obj_type
|
||||
|
||||
def get_object(self, name: str) -> Optional[NestedList]:
|
||||
"""Get object value by name"""
|
||||
return self.objects.get(name)
|
||||
|
||||
def get_type(self, name: str) -> Optional[Type]:
|
||||
"""Get object type by name"""
|
||||
return self.types.get(name)
|
||||
|
||||
def delete_object(self, name: str) -> None:
|
||||
"""Delete object from storage"""
|
||||
if name in self.objects:
|
||||
del self.objects[name]
|
||||
if name in self.types:
|
||||
del self.types[name]
|
||||
|
||||
def list_objects(self) -> list[str]:
|
||||
"""List all object names"""
|
||||
return list(self.objects.keys())
|
||||
|
||||
def object_exists(self, name: str) -> bool:
|
||||
"""Check if object exists"""
|
||||
return name in self.objects
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all objects"""
|
||||
self.objects.clear()
|
||||
self.types.clear()
|
||||
226
tests/test_algebra.py
Normal file
226
tests/test_algebra.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Tests for Phase 2: Algebra System
|
||||
"""
|
||||
|
||||
from pysecondo.core.nested_list import atom
|
||||
from pysecondo.core.types import BaseType
|
||||
from pysecondo.algebras.standard import StandardAlgebra
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
|
||||
def test_algebra_registration():
|
||||
"""Test algebra registration"""
|
||||
print("Testing Algebra Registration...")
|
||||
|
||||
manager = AlgebraManager()
|
||||
std_algebra = StandardAlgebra()
|
||||
|
||||
manager.register_algebra("StandardAlgebra", std_algebra)
|
||||
|
||||
# Check algebra is registered
|
||||
assert "StandardAlgebra" in manager.list_algebras()
|
||||
|
||||
# Check operators are indexed
|
||||
ops = manager.list_operators()
|
||||
assert "+" in ops
|
||||
assert "-" in ops
|
||||
assert "*" in ops
|
||||
assert "/" in ops
|
||||
assert "and" in ops
|
||||
assert "or" in ops
|
||||
assert "not" in ops
|
||||
|
||||
print(" ✓ Algebra registration tests passed")
|
||||
|
||||
|
||||
def test_arithmetic_operators():
|
||||
"""Test arithmetic operators"""
|
||||
print("Testing Arithmetic Operators...")
|
||||
|
||||
manager = AlgebraManager()
|
||||
manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
# Test addition
|
||||
add_op = manager.get_operator("+")
|
||||
|
||||
# Type map: int + int = int
|
||||
result_type = add_op.type_map([BaseType.INT, BaseType.INT])
|
||||
assert result_type == BaseType.INT
|
||||
|
||||
# Type map: int + real = real
|
||||
result_type = add_op.type_map([BaseType.INT, BaseType.REAL])
|
||||
assert result_type == BaseType.REAL
|
||||
|
||||
# Value map: 5 + 3 = 8
|
||||
result = add_op.value_map([atom(5), atom(3)])
|
||||
assert result.value == 8
|
||||
|
||||
# Test subtraction
|
||||
sub_op = manager.get_operator("-")
|
||||
result = sub_op.value_map([atom(10), atom(3)])
|
||||
assert result.value == 7
|
||||
|
||||
# Test multiplication
|
||||
mul_op = manager.get_operator("*")
|
||||
result = mul_op.value_map([atom(6), atom(7)])
|
||||
assert result.value == 42
|
||||
|
||||
# Test division
|
||||
div_op = manager.get_operator("/")
|
||||
result = div_op.value_map([atom(10), atom(2)])
|
||||
assert result.value == 5.0
|
||||
|
||||
print(" ✓ Arithmetic operators tests passed")
|
||||
|
||||
|
||||
def test_comparison_operators():
|
||||
"""Test comparison operators"""
|
||||
print("Testing Comparison Operators...")
|
||||
|
||||
manager = AlgebraManager()
|
||||
manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
# Test less than
|
||||
lt_op = manager.get_operator("<")
|
||||
|
||||
result_type = lt_op.type_map([BaseType.INT, BaseType.INT])
|
||||
assert result_type == BaseType.BOOL
|
||||
|
||||
result = lt_op.value_map([atom(3), atom(5)])
|
||||
assert result.value is True
|
||||
|
||||
result = lt_op.value_map([atom(5), atom(3)])
|
||||
assert result.value is False
|
||||
|
||||
# Test greater than
|
||||
gt_op = manager.get_operator(">")
|
||||
result = gt_op.value_map([atom(10), atom(5)])
|
||||
assert result.value is True
|
||||
|
||||
# Test equal
|
||||
eq_op = manager.get_operator("=")
|
||||
result = eq_op.value_map([atom(5), atom(5)])
|
||||
assert result.value is True
|
||||
|
||||
result = eq_op.value_map([atom(5), atom(3)])
|
||||
assert result.value is False
|
||||
|
||||
# Test not equal
|
||||
ne_op = manager.get_operator("!=")
|
||||
result = ne_op.value_map([atom(5), atom(3)])
|
||||
assert result.value is True
|
||||
|
||||
print(" ✓ Comparison operators tests passed")
|
||||
|
||||
|
||||
def test_logical_operators():
|
||||
"""Test logical operators"""
|
||||
print("Testing Logical Operators...")
|
||||
|
||||
manager = AlgebraManager()
|
||||
manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
# Test and
|
||||
and_op = manager.get_operator("and")
|
||||
result = and_op.value_map([atom(True), atom(True)])
|
||||
assert result.value is True
|
||||
|
||||
result = and_op.value_map([atom(True), atom(False)])
|
||||
assert result.value is False
|
||||
|
||||
# Test or
|
||||
or_op = manager.get_operator("or")
|
||||
result = or_op.value_map([atom(True), atom(False)])
|
||||
assert result.value is True
|
||||
|
||||
result = or_op.value_map([atom(False), atom(False)])
|
||||
assert result.value is False
|
||||
|
||||
# Test not
|
||||
not_op = manager.get_operator("not")
|
||||
result = not_op.value_map([atom(True)])
|
||||
assert result.value is False
|
||||
|
||||
result = not_op.value_map([atom(False)])
|
||||
assert result.value is True
|
||||
|
||||
print(" ✓ Logical operators tests passed")
|
||||
|
||||
|
||||
def test_type_checking():
|
||||
"""Test type checking in operators"""
|
||||
print("Testing Type Checking...")
|
||||
|
||||
manager = AlgebraManager()
|
||||
manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
add_op = manager.get_operator("+")
|
||||
|
||||
# Valid types
|
||||
try:
|
||||
add_op.type_map([BaseType.INT, BaseType.INT])
|
||||
except TypeError:
|
||||
assert False, "Should not raise TypeError for valid types"
|
||||
|
||||
# Invalid types
|
||||
try:
|
||||
add_op.type_map([BaseType.STRING, BaseType.INT])
|
||||
assert False, "Should raise TypeError for invalid types"
|
||||
except TypeError as e:
|
||||
assert "requires" in str(e).lower()
|
||||
|
||||
# Wrong number of arguments
|
||||
try:
|
||||
add_op.type_map([BaseType.INT])
|
||||
assert False, "Should raise TypeError for wrong argument count"
|
||||
except TypeError as e:
|
||||
assert "expects 2" in str(e)
|
||||
|
||||
print(" ✓ Type checking tests passed")
|
||||
|
||||
|
||||
def test_complex_expressions():
|
||||
"""Test complex expressions using multiple operators"""
|
||||
print("Testing Complex Expressions...")
|
||||
|
||||
manager = AlgebraManager()
|
||||
manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
# Expression: (5 + 3) * 2 = 16
|
||||
add_op = manager.get_operator("+")
|
||||
mul_op = manager.get_operator("*")
|
||||
|
||||
temp = add_op.value_map([atom(5), atom(3)])
|
||||
result = mul_op.value_map([temp, atom(2)])
|
||||
assert result.value == 16
|
||||
|
||||
# Expression: (10 > 5) and (3 < 7) = True
|
||||
gt_op = manager.get_operator(">")
|
||||
lt_op = manager.get_operator("<")
|
||||
and_op = manager.get_operator("and")
|
||||
|
||||
temp1 = gt_op.value_map([atom(10), atom(5)])
|
||||
temp2 = lt_op.value_map([atom(3), atom(7)])
|
||||
result = and_op.value_map([temp1, temp2])
|
||||
assert result.value is True
|
||||
|
||||
print(" ✓ Complex expressions tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Phase 2: Algebra System Tests")
|
||||
print("=" * 50)
|
||||
|
||||
test_algebra_registration()
|
||||
test_arithmetic_operators()
|
||||
test_comparison_operators()
|
||||
test_logical_operators()
|
||||
test_type_checking()
|
||||
test_complex_expressions()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("All Phase 2 tests passed! ✓")
|
||||
print("=" * 50)
|
||||
179
tests/test_core.py
Normal file
179
tests/test_core.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Tests for Phase 1: Core functionality
|
||||
"""
|
||||
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
from pysecondo.core.type_system import TypeChecker
|
||||
from pysecondo.core.types import (
|
||||
BaseType, TupleType, RelationType, Attribute,
|
||||
parse_type, type_to_string
|
||||
)
|
||||
from pysecondo.core.nested_list import NestedList, atom, list_nl
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
|
||||
def test_nested_list():
|
||||
"""Test nested list creation and operations"""
|
||||
print("Testing NestedList...")
|
||||
|
||||
# Atomic values
|
||||
nl_int = atom(42)
|
||||
nl_str = atom("Beijing")
|
||||
nl_bool = atom(True)
|
||||
|
||||
assert nl_int.is_atom()
|
||||
assert nl_int.value == 42
|
||||
assert str(nl_int) == "42"
|
||||
|
||||
assert nl_str.is_atom()
|
||||
assert nl_str.value == "Beijing"
|
||||
assert str(nl_str) == '"Beijing"'
|
||||
|
||||
# Lists
|
||||
nl_list = list_nl(1, 2, 3)
|
||||
assert nl_list.is_list()
|
||||
assert len(nl_list) == 3
|
||||
assert str(nl_list) == "(1 2 3)"
|
||||
|
||||
# Nested structures (tuple)
|
||||
nl_tuple = list_nl("Beijing", 21540000)
|
||||
assert str(nl_tuple) == '("Beijing" 21540000)'
|
||||
|
||||
# Nested structures (relation)
|
||||
nl_rel = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000)
|
||||
)
|
||||
assert str(nl_rel) == '(("Beijing" 21540000) ("Shanghai" 24280000))'
|
||||
|
||||
# to_python conversion
|
||||
assert nl_int.to_python() == 42
|
||||
assert nl_list.to_python() == [1, 2, 3]
|
||||
assert nl_tuple.to_python() == ["Beijing", 21540000]
|
||||
|
||||
print(" ✓ NestedList tests passed")
|
||||
|
||||
|
||||
def test_type_system():
|
||||
"""Test type system"""
|
||||
print("Testing Type System...")
|
||||
|
||||
# Parse basic types
|
||||
int_type = parse_type("int")
|
||||
assert int_type == BaseType.INT
|
||||
|
||||
string_type = parse_type("string")
|
||||
assert string_type == BaseType.STRING
|
||||
|
||||
# Parse tuple type
|
||||
tuple_type_str = "(tuple ((Name string)(Population int)))"
|
||||
tuple_type = parse_type(tuple_type_str)
|
||||
assert isinstance(tuple_type, TupleType)
|
||||
assert len(tuple_type.attributes) == 2
|
||||
assert tuple_type.attributes[0].name == "Name"
|
||||
assert tuple_type.attributes[0].type == BaseType.STRING
|
||||
|
||||
# Parse relation type
|
||||
rel_type_str = "(rel (tuple ((Name string)(Population int))))"
|
||||
rel_type = parse_type(rel_type_str)
|
||||
assert isinstance(rel_type, RelationType)
|
||||
assert isinstance(rel_type.tuple_type, TupleType)
|
||||
|
||||
# Type to string
|
||||
assert type_to_string(BaseType.INT) == "int"
|
||||
assert "Name" in type_to_string(tuple_type)
|
||||
assert "(rel" in type_to_string(rel_type)
|
||||
|
||||
print(" ✓ Type system tests passed")
|
||||
|
||||
|
||||
def test_type_checker():
|
||||
"""Test type checking"""
|
||||
print("Testing Type Checker...")
|
||||
|
||||
checker = TypeChecker()
|
||||
|
||||
# Check basic types
|
||||
assert checker.check(atom(42), BaseType.INT)
|
||||
assert checker.check(atom("hello"), BaseType.STRING)
|
||||
assert not checker.check(atom("hello"), BaseType.INT)
|
||||
|
||||
# Check tuple type
|
||||
city_tuple_type = TupleType([
|
||||
Attribute("Name", BaseType.STRING),
|
||||
Attribute("Population", BaseType.INT)
|
||||
])
|
||||
|
||||
beijing = list_nl("Beijing", 21540000)
|
||||
assert checker.check(beijing, city_tuple_type)
|
||||
|
||||
wrong_tuple = list_nl(123, 21540000)
|
||||
assert not checker.check(wrong_tuple, city_tuple_type)
|
||||
|
||||
# Check relation type
|
||||
cities_rel_type = RelationType(city_tuple_type)
|
||||
|
||||
cities = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000)
|
||||
)
|
||||
assert checker.check(cities, cities_rel_type)
|
||||
|
||||
print(" ✓ Type checker tests passed")
|
||||
|
||||
|
||||
def test_storage():
|
||||
"""Test in-memory storage"""
|
||||
print("Testing Memory Storage...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
|
||||
# Create object
|
||||
city_tuple_type = TupleType([
|
||||
Attribute("Name", BaseType.STRING),
|
||||
Attribute("Population", BaseType.INT)
|
||||
])
|
||||
|
||||
beijing = list_nl("Beijing", 21540000)
|
||||
storage.create_object("beijing", beijing, city_tuple_type)
|
||||
|
||||
# Get object
|
||||
retrieved = storage.get_object("beijing")
|
||||
assert retrieved == beijing
|
||||
|
||||
retrieved_type = storage.get_type("beijing")
|
||||
assert retrieved_type == city_tuple_type
|
||||
|
||||
# Update object
|
||||
shanghai = list_nl("Shanghai", 24280000)
|
||||
storage.update_object("beijing", shanghai, city_tuple_type)
|
||||
assert storage.get_object("beijing") == shanghai
|
||||
|
||||
# List objects
|
||||
storage.create_object("city2", beijing, city_tuple_type)
|
||||
objects = storage.list_objects()
|
||||
assert "beijing" in objects
|
||||
assert "city2" in objects
|
||||
assert len(objects) == 2
|
||||
|
||||
# Delete object
|
||||
storage.delete_object("city2")
|
||||
assert len(storage.list_objects()) == 1
|
||||
|
||||
print(" ✓ Storage tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Phase 1: Core Functionality Tests")
|
||||
print("=" * 50)
|
||||
|
||||
test_nested_list()
|
||||
test_type_system()
|
||||
test_type_checker()
|
||||
test_storage()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("All Phase 1 tests passed! ✓")
|
||||
print("=" * 50)
|
||||
311
tests/test_relation.py
Normal file
311
tests/test_relation.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Tests for Phase 3: Relation Algebra
|
||||
"""
|
||||
|
||||
from pysecondo.core.nested_list import atom, list_nl
|
||||
from pysecondo.core.types import BaseType, TupleType, RelationType, Attribute
|
||||
from pysecondo.query_processor import QueryProcessor
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
from pysecondo.algebras.relation import RelationAlgebra
|
||||
from pysecondo.algebras.standard import StandardAlgebra
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
|
||||
def test_create_relation():
|
||||
"""Test creating relations"""
|
||||
print("Testing Create Relation...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
qp = QueryProcessor(algebra_manager, storage)
|
||||
|
||||
# Create a relation
|
||||
qp.execute_create(
|
||||
"cities",
|
||||
"(rel (tuple ((Name string)(Population int))))"
|
||||
)
|
||||
|
||||
# Check it exists
|
||||
assert storage.object_exists("cities")
|
||||
obj_type = storage.get_type("cities")
|
||||
assert isinstance(obj_type, RelationType)
|
||||
|
||||
print(" ✓ Create relation tests passed")
|
||||
|
||||
|
||||
def test_update_relation():
|
||||
"""Test updating relations with data"""
|
||||
print("Testing Update Relation...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
qp = QueryProcessor(algebra_manager, storage)
|
||||
|
||||
# Create and populate a relation
|
||||
qp.execute_create(
|
||||
"cities",
|
||||
"(rel (tuple ((Name string)(Population int))))"
|
||||
)
|
||||
|
||||
# Insert data
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
)
|
||||
|
||||
qp.execute_update("cities", cities_data)
|
||||
|
||||
# Verify data
|
||||
cities = qp.lookup_identifier("cities")
|
||||
assert len(cities) == 3
|
||||
|
||||
print(" ✓ Update relation tests passed")
|
||||
|
||||
|
||||
def test_feed_consume():
|
||||
"""Test feed and consume operators"""
|
||||
print("Testing Feed and Consume...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
qp = QueryProcessor(algebra_manager, storage)
|
||||
|
||||
# Create and populate a relation
|
||||
qp.execute_create(
|
||||
"cities",
|
||||
"(rel (tuple ((Name string)(Population int))))"
|
||||
)
|
||||
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
)
|
||||
qp.execute_update("cities", cities_data)
|
||||
|
||||
# Test feed: relation -> stream
|
||||
cities = qp.lookup_identifier("cities")
|
||||
cities_type = qp.get_identifier_type("cities")
|
||||
|
||||
feed_op = algebra_manager.get_operator("feed")
|
||||
stream = feed_op.value_map([cities])
|
||||
assert len(stream) == 3
|
||||
|
||||
# Test consume: stream -> relation
|
||||
consume_op = algebra_manager.get_operator("consume")
|
||||
result = consume_op.value_map([stream])
|
||||
assert len(result) == 3
|
||||
|
||||
print(" ✓ Feed and consume tests passed")
|
||||
|
||||
|
||||
def test_count():
|
||||
"""Test count operator"""
|
||||
print("Testing Count Operator...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
qp = QueryProcessor(algebra_manager, storage)
|
||||
|
||||
# Create and populate a relation
|
||||
qp.execute_create(
|
||||
"cities",
|
||||
"(rel (tuple ((Name string)(Population int))))"
|
||||
)
|
||||
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
list_nl("Shenzhen", 17560000),
|
||||
)
|
||||
qp.execute_update("cities", cities_data)
|
||||
|
||||
# Test count
|
||||
cities = qp.lookup_identifier("cities")
|
||||
feed_op = algebra_manager.get_operator("feed")
|
||||
count_op = algebra_manager.get_operator("count")
|
||||
|
||||
stream = feed_op.value_map([cities])
|
||||
count_result = count_op.value_map([stream])
|
||||
assert count_result.value == 4
|
||||
|
||||
print(" ✓ Count operator tests passed")
|
||||
|
||||
|
||||
def test_filter():
|
||||
"""Test filter operator"""
|
||||
print("Testing Filter Operator...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
qp = QueryProcessor(algebra_manager, storage)
|
||||
|
||||
# Create and populate a relation
|
||||
qp.execute_create(
|
||||
"cities",
|
||||
"(rel (tuple ((Name string)(Population int))))"
|
||||
)
|
||||
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
list_nl("Shenzhen", 17560000),
|
||||
)
|
||||
qp.execute_update("cities", cities_data)
|
||||
|
||||
# Test filter with true (pass all)
|
||||
cities = qp.lookup_identifier("cities")
|
||||
feed_op = algebra_manager.get_operator("feed")
|
||||
filter_op = algebra_manager.get_operator("filter")
|
||||
|
||||
stream = feed_op.value_map([cities])
|
||||
filtered = filter_op.value_map([stream, atom(True)])
|
||||
assert len(filtered) == 4
|
||||
|
||||
# Test filter with false (pass none)
|
||||
filtered = filter_op.value_map([stream, atom(False)])
|
||||
assert len(filtered) == 0
|
||||
|
||||
print(" ✓ Filter operator tests passed")
|
||||
|
||||
|
||||
def test_query_pipeline():
|
||||
"""Test a complete query pipeline"""
|
||||
print("Testing Query Pipeline...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
qp = QueryProcessor(algebra_manager, storage)
|
||||
|
||||
# Setup: create cities relation
|
||||
qp.execute_create(
|
||||
"cities",
|
||||
"(rel (tuple ((Name string)(Population int))))"
|
||||
)
|
||||
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
list_nl("Shenzhen", 17560000),
|
||||
list_nl("Hangzhou", 12200000),
|
||||
)
|
||||
qp.execute_update("cities", cities_data)
|
||||
|
||||
# Query: cities feed count
|
||||
# Equivalent to: SELECT COUNT(*) FROM cities
|
||||
cities = qp.lookup_identifier("cities")
|
||||
feed_op = algebra_manager.get_operator("feed")
|
||||
count_op = algebra_manager.get_operator("count")
|
||||
|
||||
stream = feed_op.value_map([cities])
|
||||
count = count_op.value_map([stream])
|
||||
assert count.value == 5
|
||||
|
||||
# Query: cities feed filter[true] consume
|
||||
# Equivalent to: SELECT * FROM cities
|
||||
filter_op = algebra_manager.get_operator("filter")
|
||||
consume_op = algebra_manager.get_operator("consume")
|
||||
|
||||
stream = feed_op.value_map([cities])
|
||||
filtered = filter_op.value_map([stream, atom(True)])
|
||||
result = consume_op.value_map([filtered])
|
||||
assert len(result) == 5
|
||||
|
||||
print(" ✓ Query pipeline tests passed")
|
||||
|
||||
|
||||
def test_type_checking():
|
||||
"""Test type checking for relation operators"""
|
||||
print("Testing Type Checking for Relations...")
|
||||
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
storage = MemoryStorage()
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra",
|
||||
RelationAlgebra(storage)
|
||||
)
|
||||
|
||||
# Test feed type mapping
|
||||
feed_op = algebra_manager.get_operator("feed")
|
||||
tuple_type = TupleType([
|
||||
Attribute("Name", BaseType.STRING),
|
||||
Attribute("Population", BaseType.INT)
|
||||
])
|
||||
rel_type = RelationType(tuple_type)
|
||||
|
||||
result_type = feed_op.type_map([rel_type])
|
||||
assert result_type == tuple_type
|
||||
|
||||
# Test consume type mapping
|
||||
consume_op = algebra_manager.get_operator("consume")
|
||||
result_type = consume_op.type_map([tuple_type])
|
||||
assert isinstance(result_type, RelationType)
|
||||
assert result_type.tuple_type == tuple_type
|
||||
|
||||
# Test count type mapping
|
||||
count_op = algebra_manager.get_operator("count")
|
||||
result_type = count_op.type_map([tuple_type])
|
||||
assert result_type == BaseType.INT
|
||||
|
||||
print(" ✓ Type checking tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Phase 3: Relation Algebra Tests")
|
||||
print("=" * 50)
|
||||
|
||||
test_create_relation()
|
||||
test_update_relation()
|
||||
test_feed_consume()
|
||||
test_count()
|
||||
test_filter()
|
||||
test_query_pipeline()
|
||||
test_type_checking()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("All Phase 3 tests passed! ✓")
|
||||
print("=" * 50)
|
||||
220
tests/test_repl.py
Normal file
220
tests/test_repl.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
Tests for Phase 4: Query Processing & REPL
|
||||
"""
|
||||
|
||||
from pysecondo.core.nested_list import atom, list_nl
|
||||
from pysecondo.core.types import BaseType, TupleType, RelationType
|
||||
from pysecondo.storage.memory import MemoryStorage
|
||||
from pysecondo.algebras.relation import RelationAlgebra
|
||||
from pysecondo.algebras.standard import StandardAlgebra
|
||||
from pysecondo.algebras.base import AlgebraManager
|
||||
from pysecondo.parser.evaluator import Evaluator
|
||||
from pysecondo.parser.parser import Parser, parse_query, CreateCommand, UpdateCommand, QueryCommand
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
|
||||
def test_parser_create():
|
||||
"""Test parsing CREATE commands"""
|
||||
print("Testing Parser (CREATE)...")
|
||||
|
||||
parser = Parser()
|
||||
|
||||
# Test create command
|
||||
cmd = parser.parse(
|
||||
'create cities : (rel (tuple ((Name string)(Population int))))')
|
||||
assert isinstance(cmd, CreateCommand)
|
||||
assert cmd.name == "cities"
|
||||
assert "(rel" in cmd.type_str
|
||||
|
||||
print(" ✓ CREATE parsing tests passed")
|
||||
|
||||
|
||||
def test_parser_update():
|
||||
"""Test parsing UPDATE commands"""
|
||||
print("Testing Parser (UPDATE)...")
|
||||
|
||||
parser = Parser()
|
||||
|
||||
# Test update command
|
||||
cmd = parser.parse(
|
||||
'update cities := (("Beijing" 21540000)("Shanghai" 24280000))')
|
||||
assert isinstance(cmd, UpdateCommand)
|
||||
assert cmd.name == "cities"
|
||||
assert "Beijing" in cmd.value
|
||||
|
||||
print(" ✓ UPDATE parsing tests passed")
|
||||
|
||||
|
||||
def test_parser_query():
|
||||
"""Test parsing QUERY commands"""
|
||||
print("Testing Parser (QUERY)...")
|
||||
|
||||
parser = Parser()
|
||||
|
||||
# Test query command
|
||||
cmd = parser.parse('query cities feed count')
|
||||
assert isinstance(cmd, QueryCommand)
|
||||
assert cmd.expression == "cities feed count"
|
||||
|
||||
# Test arithmetic query
|
||||
cmd = parser.parse('query 5 + 3')
|
||||
assert isinstance(cmd, QueryCommand)
|
||||
assert cmd.expression == "5 + 3"
|
||||
|
||||
print(" ✓ QUERY parsing tests passed")
|
||||
|
||||
|
||||
def test_parser_expressions():
|
||||
"""Test expression tokenization"""
|
||||
print("Testing Expression Tokenization...")
|
||||
|
||||
parser = Parser()
|
||||
|
||||
# Simple identifier
|
||||
tokens = parser.parse_expression("cities")
|
||||
assert tokens == ["cities"]
|
||||
|
||||
# Operator chain
|
||||
tokens = parser.parse_expression("cities feed consume")
|
||||
assert tokens == ["cities", "feed", "consume"]
|
||||
|
||||
# Arithmetic
|
||||
tokens = parser.parse_expression("5 + 3")
|
||||
assert tokens == ["5", "+", "3"]
|
||||
|
||||
# Complex expression
|
||||
tokens = parser.parse_expression("cities feed filter true consume")
|
||||
assert tokens == ["cities", "feed", "filter", "true", "consume"]
|
||||
|
||||
print(" ✓ Expression tokenization tests passed")
|
||||
|
||||
|
||||
def test_evaluator_arithmetic():
|
||||
"""Test evaluating arithmetic expressions"""
|
||||
print("Testing Evaluator (Arithmetic)...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Test: 5 + 3
|
||||
tokens = ["5", "+", "3"]
|
||||
value, value_type = evaluator.evaluate(tokens)
|
||||
assert value.value == 8
|
||||
|
||||
# Test: 10 - 4
|
||||
tokens = ["10", "-", "4"]
|
||||
value, value_type = evaluator.evaluate(tokens)
|
||||
assert value.value == 6
|
||||
|
||||
# Test: 6 * 7
|
||||
tokens = ["6", "*", "7"]
|
||||
value, value_type = evaluator.evaluate(tokens)
|
||||
assert value.value == 42
|
||||
|
||||
print(" ✓ Arithmetic evaluation tests passed")
|
||||
|
||||
|
||||
def test_evaluator_identifiers():
|
||||
"""Test evaluating identifiers"""
|
||||
print("Testing Evaluator (Identifiers)...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Store a value
|
||||
storage.create_object("x", atom(42), BaseType.INT)
|
||||
|
||||
# Test: x
|
||||
tokens = ["x"]
|
||||
value, value_type = evaluator.evaluate(tokens)
|
||||
assert value.value == 42
|
||||
|
||||
print(" ✓ Identifier evaluation tests passed")
|
||||
|
||||
|
||||
def test_evaluator_relations():
|
||||
"""Test evaluating relation expressions"""
|
||||
print("Testing Evaluator (Relations)...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra", RelationAlgebra(storage))
|
||||
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Create a relation
|
||||
cities_type = RelationType(TupleType([]))
|
||||
storage.create_object("cities", list_nl(), cities_type)
|
||||
|
||||
# Test: cities feed
|
||||
tokens = ["cities", "feed"]
|
||||
value, value_type = evaluator.evaluate(tokens)
|
||||
|
||||
print(" ✓ Relation evaluation tests passed")
|
||||
|
||||
|
||||
def test_end_to_end():
|
||||
"""Test end-to-end query execution"""
|
||||
print("Testing End-to-End Queries...")
|
||||
|
||||
storage = MemoryStorage()
|
||||
algebra_manager = AlgebraManager()
|
||||
algebra_manager.register_algebra("StandardAlgebra", StandardAlgebra())
|
||||
algebra_manager.register_algebra(
|
||||
"RelationAlgebra", RelationAlgebra(storage))
|
||||
|
||||
parser = Parser()
|
||||
evaluator = Evaluator(algebra_manager, storage)
|
||||
|
||||
# Create relation
|
||||
create_cmd = parser.parse(
|
||||
'create cities : (rel (tuple ((Name string)(Population int))))')
|
||||
assert isinstance(create_cmd, CreateCommand)
|
||||
|
||||
# Insert data (using storage directly for simplicity)
|
||||
from pysecondo.core.types import parse_type
|
||||
cities_type = parse_type('(rel (tuple ((Name string)(Population int))))')
|
||||
cities_data = list_nl(
|
||||
list_nl("Beijing", 21540000),
|
||||
list_nl("Shanghai", 24280000),
|
||||
list_nl("Guangzhou", 14040000),
|
||||
)
|
||||
storage.create_object("cities", cities_data, cities_type)
|
||||
|
||||
# Query: cities feed count
|
||||
query_cmd = parser.parse('query cities feed count')
|
||||
assert isinstance(query_cmd, QueryCommand)
|
||||
|
||||
tokens = parser.parse_expression(query_cmd.expression)
|
||||
value, value_type = evaluator.evaluate(tokens)
|
||||
assert value.value == 3
|
||||
|
||||
print(" ✓ End-to-end query tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Phase 4: Query Processing & REPL Tests")
|
||||
print("=" * 50)
|
||||
|
||||
test_parser_create()
|
||||
test_parser_update()
|
||||
test_parser_query()
|
||||
test_parser_expressions()
|
||||
test_evaluator_arithmetic()
|
||||
test_evaluator_identifiers()
|
||||
test_evaluator_relations()
|
||||
test_end_to_end()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("All Phase 4 tests passed! ✓")
|
||||
print("=" * 50)
|
||||
Reference in New Issue
Block a user