Merge pull request #29 from ltworf/frozen_relation

Frozen relations
master
Salvo 'LtWorf' Tomaselli 2020-08-15 21:12:23 +07:00 committed by GitHub
commit ef28b7272e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 65 additions and 187 deletions

@ -1,4 +1,5 @@
3.0 3.0
- Relations now use frozenset internally and are immutable
- Refactored parser to use better typing - Refactored parser to use better typing
- Refactored and fixed some optimizations - Refactored and fixed some optimizations
- Added more test cases - Added more test cases

@ -57,7 +57,7 @@ def load_relations():
print ("Loading relation %s with name %s..." % (i, relname)) print ("Loading relation %s with name %s..." % (i, relname))
rels[relname] = relation.Relation('%s%s' % (examples_path, i)) rels[relname] = relation.Relation.load('%s%s' % (examples_path, i))
print('done') print('done')
@ -163,7 +163,8 @@ def run_py_test(testname):
'''Runs a python test, which evaluates expressions directly rather than queries''' '''Runs a python test, which evaluates expressions directly rather than queries'''
print ("Running expression python test: " + print ("Running expression python test: " +
colorize(testname, COLOR_MAGENTA)) colorize(testname, COLOR_MAGENTA))
exp_result = None
result = None
try: try:
expr = readfile('%s%s.python' % (tests_path, testname)) expr = readfile('%s%s.python' % (tests_path, testname))
@ -238,7 +239,7 @@ def run_test(testname):
o_result = None o_result = None
try: try:
result_rel = relation.Relation('%s%s.result' % (tests_path, testname)) result_rel = relation.Relation.load('%s%s.result' % (tests_path, testname))
query = readfile('%s%s.query' % (tests_path, testname)).strip() query = readfile('%s%s.query' % (tests_path, testname)).strip()
o_query = optimizer.optimize_all(query, rels) o_query = optimizer.optimize_all(query, rels)

@ -91,7 +91,7 @@ class UserInterface:
def load(self, filename: str, name: str) -> None: def load(self, filename: str, name: str) -> None:
'''Loads a relation from file, and gives it a name to '''Loads a relation from file, and gives it a name to
be used in subsequent queries.''' be used in subsequent queries.'''
rel = Relation(filename) rel = Relation.load(filename)
self.set_relation(name, rel) self.set_relation(name, rel)
def unload(self, name: str) -> None: def unload(self, name: str) -> None:
@ -204,7 +204,7 @@ class UserInterface:
[varname =] query [varname =] query
to assign the result to a new relation to assign the result to a new relation
''' '''
r = Relation() r = None
queries = query.split('\n') queries = query.split('\n')
for query in queries: for query in queries:
if query.strip() == '': if query.strip() == '':
@ -219,4 +219,6 @@ class UserInterface:
query, query,
str(e) str(e)
)) ))
if r is None:
raise Exception('No query executed')
return r return r

@ -20,7 +20,7 @@
# relational operations on them. # relational operations on them.
import csv import csv
from itertools import chain, repeat from itertools import chain, repeat, product as iproduct
from collections import deque from collections import deque
from typing import * from typing import *
from pathlib import Path from pathlib import Path
@ -34,8 +34,7 @@ __all__ = [
] ]
class Relation: class Relation(NamedTuple):
''' '''
This object defines a relation (as a group of consistent tuples) and operations. This object defines a relation (as a group of consistent tuples) and operations.
@ -58,41 +57,35 @@ class Relation:
An empty relation needs a header, and can be filled using the insert() An empty relation needs a header, and can be filled using the insert()
method. method.
''' '''
def __hash__(self): header: 'Header'
raise NotImplementedError() content: FrozenSet[Tuple[Rstring, ...]]
def __init__(self, filename: Optional[Union[str, Path]] = None) -> None: @staticmethod
self._readonly = False def load(filename: Union[str, Path]) -> 'Relation':
self.content: Set[tuple] = set() '''
Load a relation object from a csv file.
if filename is None: # Empty relation The 1st row is the header and the other rows are the content.
self.header = Header([]) '''
return
with open(filename) as fp: with open(filename) as fp:
reader = csv.reader(fp) # Creating a csv reader reader = csv.reader(fp) # Creating a csv reader
self.header = Header(next(reader)) # read 1st line header = Header(next(reader)) # read 1st line
iterator = ((self.insert(i) for i in reader)) return Relation.create_from(header, reader)
deque(iterator, maxlen=0)
def _make_duplicate(self, copy: 'Relation') -> None: @staticmethod
'''Flag that the relation "copy" is pointing def create_from(header: Iterable[str], content: Iterable[Iterable[str]]) -> 'Relation':
to the same set as this relation.''' '''
Iterator for the header, and iterator for the content.
'''
header = Header(header)
r_content: List[Tuple[Rstring, ...]] = []
for row in content:
content_row: Tuple[Rstring, ...] = tuple(Rstring(i) for i in row)
if len(content_row) != len(header):
raise ValueError(f'Line {row} contains an incorrect amount of values')
r_content.append(content_row)
return Relation(header, frozenset(r_content))
self._readonly = True
copy._readonly = True
def _make_writable(self, copy_content: bool = True) -> None:
'''If this relation is marked as readonly, this
method will copy the content to make it writable too
if copy_content is set to false, the caller must
separately copy the content.'''
if self._readonly:
self._readonly = False
if copy_content:
self.content = set(self.content)
def __iter__(self): def __iter__(self):
return iter(self.content) return iter(self.content)
@ -136,14 +129,14 @@ class Relation:
''' '''
Selection, expr must be a valid Python expression; can contain field names. Selection, expr must be a valid Python expression; can contain field names.
''' '''
newt = Relation() header = Header(self.header)
newt.header = Header(self.header)
try: try:
c_expr = compile(expr, 'selection', 'eval') c_expr = compile(expr, 'selection', 'eval')
except: except:
raise Exception('Failed to compile expression: %s' % expr) raise Exception('Failed to compile expression: %s' % expr)
content = []
for i in self.content: for i in self.content:
# Fills the attributes dictionary with the values of the tuple # Fills the attributes dictionary with the values of the tuple
attributes = {attr: i[j].autocast() attributes = {attr: i[j].autocast()
@ -152,11 +145,11 @@ class Relation:
try: try:
if eval(c_expr, attributes): if eval(c_expr, attributes):
newt.content.add(i) content.append(i)
except Exception as e: except Exception as e:
raise Exception( raise Exception(
"Failed to evaluate %s\n%s" % (expr, e.__str__())) "Failed to evaluate %s\n%s" % (expr, e.__str__()))
return newt return Relation(header, frozenset(content))
def product(self, other: 'Relation') -> 'Relation': def product(self, other: 'Relation') -> 'Relation':
''' '''
@ -169,13 +162,10 @@ class Relation:
raise Exception( raise Exception(
'Unable to perform product on relations with colliding attributes' 'Unable to perform product on relations with colliding attributes'
) )
newt = Relation() header = Header(self.header + other.header)
newt.header = Header(self.header + other.header)
for i in self.content: content = frozenset(i+j for i, j in iproduct(self.content, other.content))
for j in other.content: return Relation(header, content)
newt.content.add(i + j)
return newt
def projection(self, *attributes) -> 'Relation': def projection(self, *attributes) -> 'Relation':
''' '''
@ -197,16 +187,11 @@ class Relation:
if len(ids) == 0: if len(ids) == 0:
raise Exception('Invalid attributes for projection') raise Exception('Invalid attributes for projection')
newt = Relation() header = Header((self.header[i] for i in ids))
# Create the header
h = (self.header[i] for i in ids)
newt.header = Header(h)
# Create the body content = frozenset(tuple((i[j] for j in ids)) for i in self.content)
for i in self.content:
row = (i[j] for j in ids) return Relation(header, content)
newt.content.add(tuple(row))
return newt
def rename(self, params: Dict[str, str]) -> 'Relation': def rename(self, params: Dict[str, str]) -> 'Relation':
''' '''
@ -217,12 +202,8 @@ class Relation:
For example if you want to rename a to b, call For example if you want to rename a to b, call
rel.rename({'a':'b'}) rel.rename({'a':'b'})
''' '''
newt = Relation() header = self.header.rename(params)
newt.header = self.header.rename(params) return Relation(header, self.content)
newt.content = self.content
self._make_duplicate(newt)
return newt
def intersection(self, other: 'Relation') -> 'Relation': def intersection(self, other: 'Relation') -> 'Relation':
''' '''
@ -231,22 +212,14 @@ class Relation:
Will return an empty one if there are no common items. Will return an empty one if there are no common items.
''' '''
other = self._rearrange(other) # Rearranges attributes' order other = self._rearrange(other) # Rearranges attributes' order
newt = Relation() return Relation(self.header, self.content.intersection(other.content))
newt.header = Header(self.header)
newt.content = self.content.intersection(other.content)
return newt
def difference(self, other: 'Relation') -> 'Relation': def difference(self, other: 'Relation') -> 'Relation':
'''Difference operation. The result will contain items present in first '''Difference operation. The result will contain items present in first
operand but not in second one. operand but not in second one.
''' '''
other = self._rearrange(other) # Rearranges attributes' order other = self._rearrange(other) # Rearranges attributes' order
newt = Relation() return Relation(self.header, self.content.difference(other.content))
newt.header = Header(self.header)
newt.content = self.content.difference(other.content)
return newt
def division(self, other: 'Relation') -> 'Relation': def division(self, other: 'Relation') -> 'Relation':
'''Division operator '''Division operator
@ -279,11 +252,7 @@ class Relation:
and second operands. and second operands.
''' '''
other = self._rearrange(other) # Rearranges attributes' order other = self._rearrange(other) # Rearranges attributes' order
newt = Relation() return Relation(self.header, self.content.union(other.content))
newt.header = Header(self.header)
newt.content = self.content.union(other.content)
return newt
def thetajoin(self, other: 'Relation', expr: str) -> 'Relation': def thetajoin(self, other: 'Relation', expr: str) -> 'Relation':
'''Defined as product and then selection with the given expression.''' '''Defined as product and then selection with the given expression.'''
@ -313,11 +282,10 @@ class Relation:
shared = self.header.intersection(other.header) shared = self.header.intersection(other.header)
newt = Relation() # Creates the new relation
# Creating the header with all the fields, done like that because order is # Creating the header with all the fields, done like that because order is
# needed # needed
h = (i for i in other.header if i not in shared) h = (i for i in other.header if i not in shared)
newt.header = Header(chain(self.header, h)) header = Header(chain(self.header, h))
# Shared ids of self # Shared ids of self
sid = self.header.getAttributesId(shared) sid = self.header.getAttributesId(shared)
@ -327,6 +295,7 @@ class Relation:
# Non shared ids of the other relation # Non shared ids of the other relation
noid = [i for i in range(len(other.header)) if i not in oid] noid = [i for i in range(len(other.header)) if i not in oid]
content = []
for i in self.content: for i in self.content:
# Tuple partecipated to the join? # Tuple partecipated to the join?
added = False added = False
@ -338,14 +307,14 @@ class Relation:
if match: if match:
item = chain(i, (j[l] for l in noid)) item = chain(i, (j[l] for l in noid))
newt.content.add(tuple(item)) content.append(tuple(item))
added = True added = True
# If it didn't partecipate, adds it # If it didn't partecipate, adds it
if not added: if not added:
item = chain(i, repeat(Rstring('---'), len(noid))) item = chain(i, repeat(Rstring('---'), len(noid)))
newt.content.add(tuple(item)) content.append(tuple(item))
return newt return Relation(header, frozenset(content))
def join(self, other: 'Relation') -> 'Relation': def join(self, other: 'Relation') -> 'Relation':
''' '''
@ -356,12 +325,10 @@ class Relation:
# List of attributes in common between the relations # List of attributes in common between the relations
shared = self.header.intersection(other.header) shared = self.header.intersection(other.header)
newt = Relation() # Creates the new relation
# Creating the header with all the fields, done like that because order is # Creating the header with all the fields, done like that because order is
# needed # needed
h = (i for i in other.header if i not in shared) h = (i for i in other.header if i not in shared)
newt.header = Header(chain(self.header, h)) header = Header(chain(self.header, h))
# Shared ids of self # Shared ids of self
sid = self.header.getAttributesId(shared) sid = self.header.getAttributesId(shared)
@ -371,6 +338,7 @@ class Relation:
# Non shared ids of the other relation # Non shared ids of the other relation
noid = [i for i in range(len(other.header)) if i not in oid] noid = [i for i in range(len(other.header)) if i not in oid]
content = []
for i in self.content: for i in self.content:
for j in other.content: for j in other.content:
match = True match = True
@ -379,9 +347,9 @@ class Relation:
if match: if match:
item = chain(i, (j[l] for l in noid)) item = chain(i, (j[l] for l in noid))
newt.content.add(tuple(item)) content.append(tuple(item))
return newt return Relation(header, frozenset(content))
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Relation): if not isinstance(other, Relation):
@ -421,76 +389,6 @@ class Relation:
return res return res
def update(self, expr: str, dic: dict) -> int:
'''
Updates certain values of a relation.
expr must be a valid Python expression that can contain field names.
This operation will change the relation itself instead of generating a new one,
updating all the tuples where expr evaluates as True.
Dic must be a dictionary that has the form "field name":"new value". Every kind of value
will be converted into a string.
Returns the number of affected rows.
'''
self._make_writable(copy_content=False)
affected = self.selection(expr)
not_affected = self.difference(affected)
new_values = tuple(
zip(self.header.getAttributesId(dic.keys()), dic.values())
)
for i in set(affected.content):
li = list(i)
for column, value in new_values:
li[column] = value
not_affected.insert(li)
self.content = not_affected.content
return len(affected)
def insert(self, values: Union[list,tuple]) -> int:
'''
Inserts a tuple in the relation.
This function will not insert duplicate tuples.
All the values will be converted in string.
Will return the number of inserted rows.
Will fail if the tuple has the wrong amount of items.
'''
if len(self.header) != len(values):
raise Exception(
'Tuple has the wrong size. Expected %d, got %d' % (
len(self.header),
len(values)
)
)
self._make_writable()
prevlen = len(self.content)
self.content.add(tuple(map(Rstring, values)))
return len(self.content) - prevlen
def delete(self, expr: str) -> int:
'''
Delete, expr must be a valid Python expression; can contain field names.
This operation will change the relation itself instead of generating a new one,
deleting all the tuples where expr evaluates as True.
Returns the number of affected rows.'''
l = len(self.content)
self._make_writable(copy_content=False)
self.content = self.difference(self.selection(expr)).content
return len(self.content) - l
class Header(tuple): class Header(tuple):

@ -84,14 +84,13 @@ class creatorForm(QtWidgets.QDialog):
for i in range(self.table.columnCount())) for i in range(self.table.columnCount()))
try: try:
header = relation.header(h) header = relation.Header(h)
except Exception as e: except Exception as e:
QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("Form", "Error"), "%s\n%s" % ( QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate("Form", "Error"), "%s\n%s" % (
QtWidgets.QApplication.translate("Form", "Header error!"), e.__str__())) QtWidgets.QApplication.translate("Form", "Header error!"), e.__str__()))
return None return None
r = relation.relation()
r.header = header
content = []
for i in range(1, self.table.rowCount()): for i in range(1, self.table.rowCount()):
hlist = [] hlist = []
for j in range(self.table.columnCount()): for j in range(self.table.columnCount()):
@ -101,11 +100,10 @@ class creatorForm(QtWidgets.QDialog):
QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate( QtWidgets.QMessageBox.information(None, QtWidgets.QApplication.translate(
"Form", "Error"), QtWidgets.QApplication.translate("Form", "Unset value in %d,%d!" % (i + 1, j + 1))) "Form", "Error"), QtWidgets.QApplication.translate("Form", "Unset value in %d,%d!" % (i + 1, j + 1)))
return None return None
r.insert(hlist) content.append(hlist)
return r return relation.Relation.create_from(header, content)
def accept(self): def accept(self):
self.result_relation = self.create_relation() self.result_relation = self.create_relation()
# Doesn't close the window in case of errors # Doesn't close the window in case of errors

@ -1,4 +0,0 @@
p1=people.rename({"id":"ido"})
people.insert((123,"lala",0,31))
assert people!=p1
people.delete("id==123")

@ -1,4 +0,0 @@
p1=people.rename({"id":"ido"})
p1.insert((123,"lala",0,31))
assert people!=p1
people.delete("id==123")

@ -0,0 +1,2 @@
assert len(people) == 8
assert len(people.header) == 4

@ -1,16 +0,0 @@
p1=people
p2=p1.rename({'id':'i'})
p2=p2.rename({'i':'id'})
assert p1==p2
assert p1._readonly
assert p2._readonly
# It is VERY important to not change the original relations
# or other tests might fail randomly, since the relations are
# only loaded once
p2.update('age==20', {'age':50})
assert p2._readonly == False
assert p1!=p2
p3 = p2.selection('age!=50')
p4 = p1.selection('age!=20')
assert p3==p4