From 190211b8ed0cd5431c44caa58376c7823e18859a Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 29 Dec 2022 09:11:25 +0200 Subject: [PATCH 01/83] add not equal operator --- miniDB/misc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miniDB/misc.py b/miniDB/misc.py index aefada74..0a078cb9 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -8,6 +8,7 @@ def get_op(op, a, b): '<': operator.lt, '>=': operator.ge, '<=': operator.le, + '!=': operator.ne, '=': operator.eq} try: @@ -18,6 +19,7 @@ def get_op(op, a, b): def split_condition(condition): ops = {'>=': operator.ge, '<=': operator.le, + '!=': operator.ne, # not equal operator (!=) '=': operator.eq, '>': operator.gt, '<': operator.lt} From 4a1052a63e7efb05a2f091d89343617ca0604b38 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 29 Dec 2022 10:59:36 +0200 Subject: [PATCH 02/83] add NOT operator --- .vscode/settings.json | 5 +++++ miniDB/misc.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f84a5f5e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.extraPaths": [ + "./miniDB" + ] +} \ No newline at end of file diff --git a/miniDB/misc.py b/miniDB/misc.py index 0a078cb9..dc75177b 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -8,8 +8,8 @@ def get_op(op, a, b): '<': operator.lt, '>=': operator.ge, '<=': operator.le, - '!=': operator.ne, - '=': operator.eq} + '=': operator.eq, + 'NOT': operator.ne} # not equal operator != try: return ops[op](a,b) @@ -19,10 +19,10 @@ def get_op(op, a, b): def split_condition(condition): ops = {'>=': operator.ge, '<=': operator.le, - '!=': operator.ne, # not equal operator (!=) '=': operator.eq, '>': operator.gt, - '<': operator.lt} + '<': operator.lt, + 'NOT': operator.ne} for op_key in ops.keys(): splt=condition.split(op_key) From 530a02fa4cd51497a46b04afdc5fc0f85b4d2127 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 29 Dec 2022 11:15:49 +0200 Subject: [PATCH 03/83] Change misc file --- miniDB/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index dc75177b..dd89c6f6 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -26,7 +26,7 @@ def split_condition(condition): for op_key in ops.keys(): splt=condition.split(op_key) - if len(splt)>1: + if (len(splt)>1 and len(splt)!= 3): left, right = splt[0].strip(), splt[1].strip() if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. From 2c4dadad066c4f36bdf3d0d80a0eeb912ede07d7 Mon Sep 17 00:00:00 2001 From: DeppieK Date: Thu, 29 Dec 2022 19:54:23 +0200 Subject: [PATCH 04/83] try --- docs/documentation.pdf | Bin 324323 -> 327899 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/documentation.pdf b/docs/documentation.pdf index c301663853fb43906825a548335e6783d3ff0dde..5ce97d3f433bb5a767b02ee501fc3b671d27dbd3 100644 GIT binary patch delta 70221 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 PSyooIFXZGRa-#naJ%TP@ From ad20aa3213f340c7f99bc8f146a7f362e219e26f Mon Sep 17 00:00:00 2001 From: DeppieK Date: Fri, 30 Dec 2022 00:00:02 +0200 Subject: [PATCH 05/83] change regarding BETWEEN stmt --- miniDB/misc.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index aefada74..8de95b52 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -1,5 +1,10 @@ import operator + +def bt(v,mn,mx): + if ((v >= mn) and (v <= mx)): + return True + def get_op(op, a, b): ''' Get op as a function of a and b by using a symbol @@ -8,7 +13,8 @@ def get_op(op, a, b): '<': operator.lt, '>=': operator.ge, '<=': operator.le, - '=': operator.eq} + '=': operator.eq, + 'BETWEEN': operator.bt} try: return ops[op](a,b) @@ -20,7 +26,8 @@ def split_condition(condition): '<=': operator.le, '=': operator.eq, '>': operator.gt, - '<': operator.lt} + '<': operator.lt, + 'BETWEEN': operator.bt} for op_key in ops.keys(): splt=condition.split(op_key) From 1e02ed97e83f53292beb26d8b688bb33ec0a00db Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 2 Jan 2023 16:07:57 +0200 Subject: [PATCH 06/83] not operator --- miniDB/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index dd89c6f6..52e2f8d1 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -26,8 +26,8 @@ def split_condition(condition): for op_key in ops.keys(): splt=condition.split(op_key) - if (len(splt)>1 and len(splt)!= 3): - left, right = splt[0].strip(), splt[1].strip() + if len(splt)>1: + left, right = splt[0].strip(), splt[1].strip() if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. right = right.strip('"') From d2f1c386734028ee3faa71ed40d06d3c668b49de Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 2 Jan 2023 16:25:53 +0200 Subject: [PATCH 07/83] operator --- miniDB/database.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miniDB/database.py b/miniDB/database.py index a3ac6be7..2fdd7d15 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -353,7 +353,9 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ ''' # print(table_name) + self.load_database() + print(condition) if isinstance(table_name,Table): return table_name._select_where(columns, condition, distinct, order_by, desc, limit) From c0167afd0a165664bd46a8dac8059d51e991543a Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 2 Jan 2023 16:28:28 +0200 Subject: [PATCH 08/83] not operator --- miniDB/misc.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index 52e2f8d1..b6d5ffb8 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -4,12 +4,12 @@ def get_op(op, a, b): ''' Get op as a function of a and b by using a symbol ''' - ops = {'>': operator.gt, + ops = { '>': operator.gt, '<': operator.lt, '>=': operator.ge, '<=': operator.le, - '=': operator.eq, - 'NOT': operator.ne} # not equal operator != + '!=': operator.ne, # not equal operator != + '=': operator.eq,} try: return ops[op](a,b) @@ -19,14 +19,15 @@ def get_op(op, a, b): def split_condition(condition): ops = {'>=': operator.ge, '<=': operator.le, + '!=': operator.ne, '=': operator.eq, '>': operator.gt, - '<': operator.lt, - 'NOT': operator.ne} + '<': operator.lt,} for op_key in ops.keys(): splt=condition.split(op_key) - if len(splt)>1: + print(splt) + if len(splt)>1: # operator has been found left, right = splt[0].strip(), splt[1].strip() if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. From 38d923bfe22825103ea93dd00142e9eb9d5c21e8 Mon Sep 17 00:00:00 2001 From: DeppieK Date: Wed, 4 Jan 2023 17:43:59 +0200 Subject: [PATCH 09/83] still working on between stmt --- miniDB/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index a3ac6be7..90e4ac68 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -343,7 +343,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ 'column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. - Operatores supported: (<,<=,==,>=,>) + Operators supported: (<,<=,==,>=,>) order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). desc: boolean. If True, order_by will return results in descending order (True by default). limit: int. An integer that defines the number of rows that will be returned (all rows if None). From b5543188cdc381f9f29709c02e2ddffe5583b128 Mon Sep 17 00:00:00 2001 From: DeppieK Date: Wed, 4 Jan 2023 17:44:33 +0200 Subject: [PATCH 10/83] between... --- mdb.py | 3 ++- miniDB/misc.py | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/mdb.py b/mdb.py index a981e5be..30152230 100644 --- a/mdb.py +++ b/mdb.py @@ -1,3 +1,4 @@ + import os import re from pprint import pprint @@ -170,7 +171,7 @@ def interpret(query): 'import': ['import', 'from'], 'export': ['export', 'to'], 'insert into': ['insert into', 'values'], - 'select': ['select', 'from', 'where', 'distinct', 'order by', 'limit'], + 'select': ['select', 'from', 'where', 'distinct', 'order by', 'limit','and'], 'lock table': ['lock table', 'mode'], 'unlock table': ['unlock table', 'force'], 'delete from': ['delete from', 'where'], diff --git a/miniDB/misc.py b/miniDB/misc.py index 8de95b52..06df5aeb 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -1,20 +1,27 @@ import operator -def bt(v,mn,mx): - if ((v >= mn) and (v <= mx)): - return True +def bt(col_name,v1,v2): + list = [] + for i in col_name: + if (i >= v1) and (i <= v2): + list.append(i) + return list + + def get_op(op, a, b): ''' Get op as a function of a and b by using a symbol ''' + #BETWEEN = bt ops = {'>': operator.gt, '<': operator.lt, '>=': operator.ge, '<=': operator.le, '=': operator.eq, - 'BETWEEN': operator.bt} + 'between' : bt() + } try: return ops[op](a,b) @@ -22,12 +29,14 @@ def get_op(op, a, b): return False def split_condition(condition): + ops = {'>=': operator.ge, '<=': operator.le, '=': operator.eq, '>': operator.gt, '<': operator.lt, - 'BETWEEN': operator.bt} + 'between' : bt() + } for op_key in ops.keys(): splt=condition.split(op_key) From 21834b1386c3564cd701d9449730448fc7b14404 Mon Sep 17 00:00:00 2001 From: DeppieK Date: Sun, 15 Jan 2023 17:51:47 +0200 Subject: [PATCH 11/83] between operator implementation --- mdb.py | 2 +- miniDB/misc.py | 53 +++++++++++++++++++++++++++++++++++++++---------- miniDB/table.py | 5 ++++- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/mdb.py b/mdb.py index 30152230..035a285d 100644 --- a/mdb.py +++ b/mdb.py @@ -171,7 +171,7 @@ def interpret(query): 'import': ['import', 'from'], 'export': ['export', 'to'], 'insert into': ['insert into', 'values'], - 'select': ['select', 'from', 'where', 'distinct', 'order by', 'limit','and'], + 'select': ['select', 'from', 'where', 'distinct', 'order by', 'limit'], 'lock table': ['lock table', 'mode'], 'unlock table': ['unlock table', 'force'], 'delete from': ['delete from', 'where'], diff --git a/miniDB/misc.py b/miniDB/misc.py index 06df5aeb..bc37f02e 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -1,26 +1,40 @@ import operator +def operator_between(value, condition): + begin, end = condition.split('and') -def bt(col_name,v1,v2): - list = [] - for i in col_name: - if (i >= v1) and (i <= v2): - list.append(i) - return list + begin = begin.strip() + end = end.strip() + begin = begin.replace("'","") + end = end.replace("'","") + + if (begin.isnumeric() and end.isnumeric()): + begin = int(begin) + end = int(end) + if (value >= begin) and (value <= end): + return True + + elif (not(begin.isnumeric() or end.isnumeric())): + if (value >= begin) and (value <= end): + return True + + else: + raise Exception("Values must be of the same type!") + + return False def get_op(op, a, b): ''' Get op as a function of a and b by using a symbol ''' - #BETWEEN = bt ops = {'>': operator.gt, '<': operator.lt, '>=': operator.ge, '<=': operator.le, '=': operator.eq, - 'between' : bt() + 'between': operator_between } try: @@ -35,14 +49,30 @@ def split_condition(condition): '=': operator.eq, '>': operator.gt, '<': operator.lt, - 'between' : bt() + 'between': operator_between } for op_key in ops.keys(): splt=condition.split(op_key) + #print(splt) + #print(">>",condition) if len(splt)>1: left, right = splt[0].strip(), splt[1].strip() + #print(">>>",left,right) + if 'and' in right: + begin,end = right.split('and') + begin = begin.strip() + end = end.strip() + if (begin[0] == '"' == begin[-1]) or (end[0] == '"' == end[-1]): + begin = begin.strip('"',"") + end = end.strip('"',"") + elif ( ' ' in begin) or ( ' ' in end): + raise ValueError(f'Invalid condition: {condition}\nValue must be enclosed in double quotation marks to include whitespaces.') + if (begin.find('"') != -1) or (end.find('"') != -1): # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) + raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') + return left, op_key, right + if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. right = right.strip('"') elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. @@ -50,9 +80,9 @@ def split_condition(condition): if right.find('"') != -1: # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') - return left, op_key, right + def reverse_op(op): ''' Reverse the operator given @@ -62,5 +92,6 @@ def reverse_op(op): '>=' : '<=', '<' : '>', '<=' : '>=', - '=' : '=' + '=' : '=', + 'between': operator_between }.get(op) diff --git a/miniDB/table.py b/miniDB/table.py index f5c7d937..2909c313 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -217,7 +217,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by 'column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. - Operatores supported: (<,<=,==,>=,>) + Operators supported: (<,<=,==,>=,>) distinct: boolean. If True, the resulting table will contain only unique rows (False by default). order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). desc: boolean. If True, order_by will return results in descending order (False by default). @@ -235,6 +235,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by if condition is not None: column_name, operator, value = self._parse_condition(condition) column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] else: rows = [i for i in range(len(self.data))] @@ -562,6 +563,8 @@ def _parse_condition(self, condition, join=False): raise ValueError(f'Condition is not valid (cant find column name)') coltype = self.column_types[self.column_names.index(left)] + if op == 'between': + return left, op, right return left, op, coltype(right) From a6a99f30078c39b59bbc113f9ed4da1e9d980310 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 00:29:57 +0200 Subject: [PATCH 12/83] or and not op --- miniDB/database.py | 124 +++++++++++++-- miniDB/misc.py | 43 +++++ miniDB/table.py | 383 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 537 insertions(+), 13 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 2fdd7d15..196496b5 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -14,6 +14,7 @@ from joins import Inlj, Smj from btree import Btree from misc import split_condition +from misc import split_not_condition from table import Table @@ -292,6 +293,7 @@ def update_table(self, table_name, set_args, condition): set_column: string. The column to be altered. condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operatores supported: (<,<=,==,>=,>) @@ -314,6 +316,7 @@ def delete_from(self, table_name, condition): table_name: string. Name of table (must be part of database). condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operatores supported: (<,<=,==,>=,>) @@ -341,6 +344,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ columns: list. The columns that will be part of the output table (use '*' to select all available columns) condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operatores supported: (<,<=,==,>=,>) @@ -359,31 +363,123 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ if isinstance(table_name,Table): return table_name._select_where(columns, condition, distinct, order_by, desc, limit) + flag = 0 if condition is not None: - condition_column = split_condition(condition)[0] + + initial_condition = condition + print("\nCondition is: " + condition) + + # or operator: + operator = ' or ' + if (operator in condition): # salary = 2000 or salary > 6000 + flag = 1 + #self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) + #flag =1 + + splt = condition.split(operator) # salary = 20000, salary > 6000 + sum = 0 + s_btree = 0 + s = 0 + for condition in splt: + print("splt condition is: " + condition) + #self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) + + + if(condition[:4] == 'not '): + + print("Not has been found in condition!") + condition_column = split_not_condition(condition)[0] + print("Column is: " + condition_column) + print("Operator is: " + split_not_condition(condition)[1]) + print("Value is: " + split_not_condition(condition)[2] + '\n') + + else: + print("i am here") + print(condition) + condition_column = split_condition(condition)[0] + print("Column is: " + condition_column) + print("Operator is: " + split_condition(condition)[1]) + print("Value is: " + split_condition(condition)[2] + '\n') + + + if self.is_locked(table_name): + return + if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + bt = self._load_idx(index_name) + s_btree += 1 + #table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + else: + s += 1 + #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + # self.unlock_table(table_name) + #print(s) + + if (s_btree == 2): + table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + elif (s == 2): + #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + table = self.tables[table_name]._select_where_or(columns, initial_condition, distinct, order_by, desc, limit) + + + if save_as is not None: + table._name = save_as + self.table_from_object(table) + else: + if return_object: + #sum +=1 + return table + else: + #continue + #sum += 0 + return table.show() + + else: + + if(condition[:4] == 'not '): + + #print("condition[0] is: " + condition[0]) + #print("condition[1] is: " + condition[1]) + #print("condition[2] is: " + condition[2]) + #print("condition[3] is: " + condition[3]) + print("Not has been found in condition!") + condition_column = split_not_condition(condition)[0] + print("Column is: " + condition_column) + print("Operator is: " + split_not_condition(condition)[1]) + print("Value is: " + split_not_condition(condition)[2] + '\n') + + else: + print("i am here") + print(condition) + condition_column = split_condition(condition)[0] + print("Column is: " + condition_column) + print("Operator is: " + split_condition(condition)[1]) + print("Value is: " + split_condition(condition)[2] + '\n') + else: condition_column = '' - - # self.lock_table(table_name, mode='x') - if self.is_locked(table_name): + if (flag == 0): # not or in condition + # self.lock_table(table_name, mode='x') + if self.is_locked(table_name): return - if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] bt = self._load_idx(index_name) table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - else: + else: table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) - # self.unlock_table(table_name) - if save_as is not None: + # self.unlock_table(table_name) + if save_as is not None: table._name = save_as self.table_from_object(table) - else: + else: if return_object: return table else: return table.show() - + def show_table(self, table_name, no_of_rows=None): ''' @@ -436,6 +532,7 @@ def join(self, mode, left_table, right_table, condition, save_as=None, return_ob right_table: string. Name of the right table (must be in DB) or Table obj. condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -747,4 +844,9 @@ def drop_index(self, index_name): warnings.warn(f'"{self.savedir}/indexes/meta_{index_name}_index.pkl" not found.') self.save_database() - \ No newline at end of file + + def handle_or_op(self, columns, table_name, s, distinct=None, order_by=None, \ + limit=True, desc=None, save_as=None, return_object=True): + + self.select(columns, table_name, s, distinct, order_by, desc, limit) + \ No newline at end of file diff --git a/miniDB/misc.py b/miniDB/misc.py index b6d5ffb8..78f1fdd9 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -8,8 +8,13 @@ def get_op(op, a, b): '<': operator.lt, '>=': operator.ge, '<=': operator.le, +<<<<<<< HEAD '!=': operator.ne, # not equal operator != '=': operator.eq,} +======= + '=': operator.eq, + '<>': operator.ne} +>>>>>>> bcace31 (or and not op) try: return ops[op](a,b) @@ -26,9 +31,16 @@ def split_condition(condition): for op_key in ops.keys(): splt=condition.split(op_key) +<<<<<<< HEAD print(splt) if len(splt)>1: # operator has been found left, right = splt[0].strip(), splt[1].strip() +======= + #print("split is: ") + #print(splt) + if len(splt)>1: + left, right = splt[0].strip(), splt[1].strip() +>>>>>>> bcace31 (or and not op) if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. right = right.strip('"') @@ -51,3 +63,34 @@ def reverse_op(op): '<=' : '>=', '=' : '=' }.get(op) + + +def not_op(op): + ''' + Handle operator not, by changing the operator given + ''' + return { + '>' : '<=', + '>=' : '<', + '<' : '>=', + '<=' : '>', + '=' : '<>' + }.get(op) + +def split_not_condition(condition): # not salary > 50000 + #condition = condition[4:] # salary > 50000 + splt = condition.split(' ') + #print(splt) + op_key = not_op(splt[2]) + left, right = splt[1].strip(), splt[3].strip() + + if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. + right = right.strip('"') + elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. + raise ValueError(f'Invalid condition: {condition}\nValue must be enclosed in double quotation marks to include whitespaces.') + + if right.find('"') != -1: # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) + raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') + + return left, op_key, right + diff --git a/miniDB/table.py b/miniDB/table.py index f5c7d937..12f70538 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -6,7 +6,7 @@ sys.path.append(f'{os.path.dirname(os.path.dirname(os.path.abspath(__file__)))}/miniDB') -from misc import get_op, split_condition +from misc import get_op, split_condition, split_not_condition class Table: @@ -146,6 +146,7 @@ def _update_rows(self, set_value, set_column, condition): set_column: string. The column to be altered. condition: string. A condition using the following format: 'column[<,<=,=,>=,>]value' or + 'not column[<,<=,=,>=,>]value' or 'value[<,<=,=,>=,>]column'. Operatores supported: (<,<=,=,>=,>) @@ -178,6 +179,7 @@ def _delete_where(self, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operatores supported: (<,<=,==,>=,>) @@ -215,6 +217,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by return_columns: list. The columns to be returned. condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operatores supported: (<,<=,==,>=,>) @@ -233,6 +236,25 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by # if condition is None, return all rows # if not, return the rows with values where condition is met for value if condition is not None: + #print("condition is: " + condition) + # if or in condition: + ''' + operator = ' or ' + if (operator in condition): # salary = 2000 or salary > 6000 + splt = condition.split(operator) # salary = 20000, salary > 6000 + #column_name = self._parse_condition(splt[0])[0] + #column = self.column_by_name(column_name) + + for s in splt: + + self._select_where(return_columns, s, distinct, order_by, desc, limit) + + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + + ''' + #elif (' or ' not in condition): column_name, operator, value = self._parse_condition(condition) column = self.column_by_name(column_name) rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] @@ -267,6 +289,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by if isinstance(limit,str): s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + #print(s_table.data) return s_table @@ -346,6 +369,7 @@ def _general_join_processing(self, table_right:Table, condition, join_type): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -390,6 +414,7 @@ def _inner_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -419,6 +444,7 @@ def _left_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -449,6 +475,7 @@ def _right_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -479,6 +506,7 @@ def _full_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -547,6 +575,7 @@ def _parse_condition(self, condition, join=False): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operatores supported: (<,<=,==,>=,>) @@ -557,7 +586,13 @@ def _parse_condition(self, condition, join=False): return split_condition(condition) # cast the value with the specified column's type and return the column name, the operator and the casted value - left, op, right = split_condition(condition) + + if (condition[:4] == 'not '): + left, op, right = split_not_condition(condition) + + else: + left, op, right = split_condition(condition) + if left not in self.column_names: raise ValueError(f'Condition is not valid (cant find column name)') coltype = self.column_types[self.column_names.index(left)] @@ -577,3 +612,347 @@ def _load_from_file(self, filename): f.close() self.__dict__.update(tmp_dict.__dict__) + +# try to handle or operator + def _select_where_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, data=None): + ''' + Select and return a table containing specified columns and rows where condition is met. + + Args: + return_columns: list. The columns to be returned. + condition: string. A condition using the following format: + 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or + 'value[<,<=,==,>=,>]column'. + + Operatores supported: (<,<=,==,>=,>) + distinct: boolean. If True, the resulting table will contain only unique rows (False by default). + order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). + desc: boolean. If True, order_by will return results in descending order (False by default). + limit: int. An integer that defines the number of rows that will be returned (all rows if None). + ''' + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + # if condition is None, return all rows + # if not, return the rows with values where condition is met for value + if condition is not None: + flag = 0 # flag 4 or operator + operator = ' or ' + # if or in condition + if (operator in condition): # salary = 2000 or salary > 6000 + print("Inside or function.\n") + flag = 1 + splt = condition.split(operator) # [salary = 20000, salary > 6000] + + list = [] + #build temp variable + + for s in splt: + + #self._select_where(return_columns, s, distinct, order_by, desc, limit) + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + + + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + #s_table_copy = Table(load=dict) # copy of table + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + #list = list(set(map(lambda x: tuple(x), list))) if distinct else list + #print("list with data") + #print(list) + + if order_by: + s_table.order_by(order_by, desc) + + # if isinstance(limit, str): + # try: + # k = int(limit) + # except ValueError: + # raise Exception("The value following 'top' in the query should be a number.") + + # # Remove from the table's data all the None-filled rows, as they are not shown by default + # # Then, show the first k rows + # s_table.data.remove(len(s_table.column_names) * [None]) + # s_table.data = s_table.data[:k] + + if isinstance(limit,str): + #list = list.append([row for row in s_table.data if any(row)][:int(limit)]) + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + + #print("table data with or: ") + #print(s_table.data) + + #s_table_copy.data.append(s_table.data) + + + list.append(s_table.data) + print("Data table in list: ") + print(list) + print("\n\n\n") + #s_table.data = list + #print("list data") + #print(list) + #print("-----------") + #print("\nTable data with operator or are: ") + #print(s_table.data) + + #print("-----------") + #print("\nTable copy data with operator or are: ") + #print(s_table_copy.data) + #print("-----------") + + + #s_table.data = list + #return s_table_copy + return s_table + + + else: # condition is null + rows = [i for i in range(len(self.data))] + + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + + # if isinstance(limit, str): + # try: + # k = int(limit) + # except ValueError: + # raise Exception("The value following 'top' in the query should be a number.") + + # # Remove from the table's data all the None-filled rows, as they are not shown by default + # # Then, show the first k rows + # s_table.data.remove(len(s_table.column_names) * [None]) + # s_table.data = s_table.data[:k] + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + + #print(s_table.data) + return s_table + + + + + + def _select_where_or2(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): + ''' + Select and return a table containing specified columns and rows where condition is met. + + Args: + return_columns: list. The columns to be returned. + condition: string. A condition using the following format: + 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or + 'value[<,<=,==,>=,>]column'. + + Operatores supported: (<,<=,==,>=,>) + distinct: boolean. If True, the resulting table will contain only unique rows (False by default). + order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). + desc: boolean. If True, order_by will return results in descending order (False by default). + limit: int. An integer that defines the number of rows that will be returned (all rows if None). + ''' + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + # if condition is None, return all rows + # if not, return the rows with values where condition is met for value + if condition is not None: + #print("condition is: " + condition) + # if or in condition: + flag = 0 + operator = ' or ' + if (operator in condition): # salary = 2000 or salary > 6000 + print("Inside or function.") + flag = 1 + splt = condition.split(operator) # salary = 20000, salary > 6000 + + list = [] + #build temp variable + + for s in splt: + + self._select_where(return_columns, s, distinct, order_by, desc, limit) + + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + + + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + #list = list(set(map(lambda x: tuple(x), list))) if distinct else list + #print("list with data") + #print(list) + + if order_by: + s_table.order_by(order_by, desc) + + # if isinstance(limit, str): + # try: + # k = int(limit) + # except ValueError: + # raise Exception("The value following 'top' in the query should be a number.") + + # # Remove from the table's data all the None-filled rows, as they are not shown by default + # # Then, show the first k rows + # s_table.data.remove(len(s_table.column_names) * [None]) + # s_table.data = s_table.data[:k] + if isinstance(limit,str): + #list = list.append([row for row in s_table.data if any(row)][:int(limit)]) + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + + #print("table data with or: ") + #print(s_table.data) + + + list.append(s_table.data) + #print("or data list are: ") + #print(list) + #s_table.data = list + print("list data") + print(list) + print("-----------") + print("\nTable data with operator or are: ") + print(s_table.data) + s_table.data = list + return s_table + + elif (' or ' not in condition): + column_name, operator, value = self._parse_condition(condition) + column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + + else: + rows = [i for i in range(len(self.data))] + + if (flag == 0): + print("i am here") + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + + # if isinstance(limit, str): + # try: + # k = int(limit) + # except ValueError: + # raise Exception("The value following 'top' in the query should be a number.") + + # # Remove from the table's data all the None-filled rows, as they are not shown by default + # # Then, show the first k rows + # s_table.data.remove(len(s_table.column_names) * [None]) + # s_table.data = s_table.data[:k] + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + + #print(s_table.data) + return s_table + + def _select_where_or3(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, data=None): + ''' + Select and return a table containing specified columns and rows where condition is met. + + Args: + return_columns: list. The columns to be returned. + condition: string. A condition using the following format: + 'column[<,<=,==,>=,>]value' or + 'not column[<,<=,==,>=,>]value' or + 'value[<,<=,==,>=,>]column'. + + Operatores supported: (<,<=,==,>=,>) + distinct: boolean. If True, the resulting table will contain only unique rows (False by default). + order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). + desc: boolean. If True, order_by will return results in descending order (False by default). + limit: int. An integer that defines the number of rows that will be returned (all rows if None). + ''' + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + # if condition is None, return all rows + # if not, return the rows with values where condition is met for value + if condition is not None: + # build a loop and check rows line 925 + column_name, operator, value = self._parse_condition(condition) + column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] #with the next oneee + + else: # condition is null + rows = [i for i in range(len(self.data))] + + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + + + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + #print(s_table.data) + + + return s_table + + + + From e310925e892524f690650f86d31623d233191a86 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 00:49:35 +0200 Subject: [PATCH 13/83] and and or op --- miniDB/table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/miniDB/table.py b/miniDB/table.py index 12f70538..49600a99 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -701,6 +701,7 @@ def _select_where_or(self, return_columns, condition=None, distinct=False, order list.append(s_table.data) print("Data table in list: ") + print(list) print("\n\n\n") #s_table.data = list From c330351fbe5e40593df998993c4de35c725480a0 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 01:03:31 +0200 Subject: [PATCH 14/83] not and or --- miniDB/database.py | 2 +- miniDB/misc.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index 196496b5..c891c4ee 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -375,7 +375,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ flag = 1 #self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) - #flag =1 + splt = condition.split(operator) # salary = 20000, salary > 6000 sum = 0 diff --git a/miniDB/misc.py b/miniDB/misc.py index 78f1fdd9..344ca76c 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -94,3 +94,4 @@ def split_not_condition(condition): # not salary > 50000 return left, op_key, right + From c35fdad4bcee8d7d7caff1f934f8247cfcf1020f Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 01:21:46 +0200 Subject: [PATCH 15/83] not op --- miniDB/database.py | 1 - miniDB/misc.py | 1 + miniDB/table.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index c891c4ee..a94216ed 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -375,7 +375,6 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ flag = 1 #self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) - splt = condition.split(operator) # salary = 20000, salary > 6000 sum = 0 diff --git a/miniDB/misc.py b/miniDB/misc.py index 344ca76c..817a074e 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -89,6 +89,7 @@ def split_not_condition(condition): # not salary > 50000 elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. raise ValueError(f'Invalid condition: {condition}\nValue must be enclosed in double quotation marks to include whitespaces.') + if right.find('"') != -1: # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') diff --git a/miniDB/table.py b/miniDB/table.py index 49600a99..e213666e 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -914,6 +914,7 @@ def _select_where_or3(self, return_columns, condition=None, distinct=False, orde limit: int. An integer that defines the number of rows that will be returned (all rows if None). ''' + # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] From 196af6e5f48a7ca693d19d45de921f451184e648 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 01:43:07 +0200 Subject: [PATCH 16/83] . --- miniDB/database.py | 3 +-- miniDB/misc.py | 1 - miniDB/table.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index a94216ed..fe48bed1 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -374,8 +374,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ if (operator in condition): # salary = 2000 or salary > 6000 flag = 1 #self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) - + #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) splt = condition.split(operator) # salary = 20000, salary > 6000 sum = 0 s_btree = 0 diff --git a/miniDB/misc.py b/miniDB/misc.py index 817a074e..344ca76c 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -89,7 +89,6 @@ def split_not_condition(condition): # not salary > 50000 elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. raise ValueError(f'Invalid condition: {condition}\nValue must be enclosed in double quotation marks to include whitespaces.') - if right.find('"') != -1: # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') diff --git a/miniDB/table.py b/miniDB/table.py index e213666e..49600a99 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -914,7 +914,6 @@ def _select_where_or3(self, return_columns, condition=None, distinct=False, orde limit: int. An integer that defines the number of rows that will be returned (all rows if None). ''' - # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] From 2f688ce6d018ca81ede619c266ad4083ddebfaec Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 01:50:17 +0200 Subject: [PATCH 17/83] . --- miniDB/misc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index 344ca76c..d0f36887 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -88,7 +88,6 @@ def split_not_condition(condition): # not salary > 50000 right = right.strip('"') elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. raise ValueError(f'Invalid condition: {condition}\nValue must be enclosed in double quotation marks to include whitespaces.') - if right.find('"') != -1: # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') From d62a1af0185b495fa7205eb4ba0103da82eab5ac Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 01:53:15 +0200 Subject: [PATCH 18/83] . --- miniDB/database.py | 3 ++- miniDB/misc.py | 2 ++ miniDB/table.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index fe48bed1..a94216ed 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -374,7 +374,8 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ if (operator in condition): # salary = 2000 or salary > 6000 flag = 1 #self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) + #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) + splt = condition.split(operator) # salary = 20000, salary > 6000 sum = 0 s_btree = 0 diff --git a/miniDB/misc.py b/miniDB/misc.py index d0f36887..817a074e 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -88,6 +88,8 @@ def split_not_condition(condition): # not salary > 50000 right = right.strip('"') elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. raise ValueError(f'Invalid condition: {condition}\nValue must be enclosed in double quotation marks to include whitespaces.') + + if right.find('"') != -1: # If there are any double quotes in the value, throw. (Notice we've already removed the leading and trailing ones) raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') diff --git a/miniDB/table.py b/miniDB/table.py index 49600a99..e213666e 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -914,6 +914,7 @@ def _select_where_or3(self, return_columns, condition=None, distinct=False, orde limit: int. An integer that defines the number of rows that will be returned (all rows if None). ''' + # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] From d0af9f0f8d5ed1a3acfe930129dfdb6ecc06044d Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 17 Jan 2023 02:02:56 +0200 Subject: [PATCH 19/83] . --- miniDB/misc.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index 817a074e..4d4842f8 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -8,13 +8,9 @@ def get_op(op, a, b): '<': operator.lt, '>=': operator.ge, '<=': operator.le, -<<<<<<< HEAD - '!=': operator.ne, # not equal operator != - '=': operator.eq,} -======= '=': operator.eq, '<>': operator.ne} ->>>>>>> bcace31 (or and not op) + try: return ops[op](a,b) @@ -31,16 +27,16 @@ def split_condition(condition): for op_key in ops.keys(): splt=condition.split(op_key) -<<<<<<< HEAD - print(splt) + + #print(splt) if len(splt)>1: # operator has been found left, right = splt[0].strip(), splt[1].strip() -======= + #print("split is: ") #print(splt) if len(splt)>1: left, right = splt[0].strip(), splt[1].strip() ->>>>>>> bcace31 (or and not op) + if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. right = right.strip('"') From dae085a0c7cd942c7db4c82b19b4b7c2f10bbc27 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 28 Jan 2023 19:35:48 +0200 Subject: [PATCH 20/83] Add not and and functionality --- miniDB/database.py | 129 ++++++++++++++++----------------------------- miniDB/misc.py | 3 +- 2 files changed, 46 insertions(+), 86 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index a94216ed..27da8408 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -334,6 +334,8 @@ def delete_from(self, table_name, condition): self._add_to_insert_stack(table_name, deleted) self.save_database() + + def select(self, columns, table_name, condition, distinct=None, order_by=None, \ limit=True, desc=None, save_as=None, return_object=True): ''' @@ -357,86 +359,45 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ ''' # print(table_name) - self.load_database() - print(condition) if isinstance(table_name,Table): return table_name._select_where(columns, condition, distinct, order_by, desc, limit) flag = 0 if condition is not None: - initial_condition = condition - print("\nCondition is: " + condition) + print("Condition is: " + condition+"\n") - # or operator: - operator = ' or ' - if (operator in condition): # salary = 2000 or salary > 6000 - flag = 1 - #self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - #return self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) - - splt = condition.split(operator) # salary = 20000, salary > 6000 - sum = 0 - s_btree = 0 - s = 0 - for condition in splt: - print("splt condition is: " + condition) - #self.handle_or_op(columns, table_name, s, distinct, order_by, desc, limit) - - - if(condition[:4] == 'not '): - - print("Not has been found in condition!") - condition_column = split_not_condition(condition)[0] - print("Column is: " + condition_column) - print("Operator is: " + split_not_condition(condition)[1]) - print("Value is: " + split_not_condition(condition)[2] + '\n') + operator = ' or ' + if (operator in condition): # e.g salary = 2000 or salary > 6000 - else: - print("i am here") - print(condition) - condition_column = split_condition(condition)[0] - print("Column is: " + condition_column) - print("Operator is: " + split_condition(condition)[1]) - print("Value is: " + split_condition(condition)[2] + '\n') - - - if self.is_locked(table_name): - return - if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: - index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] - bt = self._load_idx(index_name) - s_btree += 1 - #table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - else: - s += 1 - #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - # self.unlock_table(table_name) - #print(s) - - if (s_btree == 2): + flag = 1 # found + splt = condition.split(operator) + lst = [item[0] for item in splt] # condition_column list + + #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + + if self.is_locked(table_name): + return + if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + bt = self._load_idx(index_name) table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - elif (s == 2): - #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - table = self.tables[table_name]._select_where_or(columns, initial_condition, distinct, order_by, desc, limit) - - + else: + table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + # self.unlock_table(table_name) if save_as is not None: - table._name = save_as - self.table_from_object(table) + table._name = save_as + self.table_from_object(table) else: - if return_object: - #sum +=1 - return table - else: - #continue - #sum += 0 - return table.show() - + if return_object: + return table + else: + return table.show() + else: - if(condition[:4] == 'not '): + if(condition[:4] == 'not '): #print("condition[0] is: " + condition[0]) #print("condition[1] is: " + condition[1]) @@ -448,7 +409,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ print("Operator is: " + split_not_condition(condition)[1]) print("Value is: " + split_not_condition(condition)[2] + '\n') - else: + else: print("i am here") print(condition) condition_column = split_condition(condition)[0] @@ -459,26 +420,26 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ else: condition_column = '' - if (flag == 0): # not or in condition + if (flag == 0): # or has not been found # self.lock_table(table_name, mode='x') - if self.is_locked(table_name): - return - if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: - index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] - bt = self._load_idx(index_name) - table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - else: - table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) + if self.is_locked(table_name): + return + if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + bt = self._load_idx(index_name) + table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + else: + table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) # self.unlock_table(table_name) - if save_as is not None: - table._name = save_as - self.table_from_object(table) - else: - if return_object: - return table + if save_as is not None: + table._name = save_as + self.table_from_object(table) else: - return table.show() - + if return_object: + return table + else: + return table.show() + def show_table(self, table_name, no_of_rows=None): ''' diff --git a/miniDB/misc.py b/miniDB/misc.py index 4d4842f8..a6895b61 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -10,8 +10,7 @@ def get_op(op, a, b): '<=': operator.le, '=': operator.eq, '<>': operator.ne} - - + try: return ops[op](a,b) except TypeError: # if a or b is None (deleted record), python3 raises typerror From 0c0578a8423c999dd143a117c8cb6aa977ac880b Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 28 Jan 2023 19:38:04 +0200 Subject: [PATCH 21/83] Add not and and functionality --- miniDB/table.py | 447 +++++++++--------------------------------------- 1 file changed, 82 insertions(+), 365 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index e213666e..9b49c4e5 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -151,22 +151,39 @@ def _update_rows(self, set_value, set_column, condition): Operatores supported: (<,<=,=,>=,>) ''' - # parse the condition - column_name, operator, value = self._parse_condition(condition) - - # get the condition and the set column - column = self.column_by_name(column_name) - set_column_idx = self.column_names.index(set_column) - # set_columns_indx = [self.column_names.index(set_column_name) for set_column_name in set_column_names] + operator = ' or ' + if (operator in condition): + splt = condition.split(operator) + for s in splt: + # parse the condition + column_name, operator, value = self._parse_condition(s) - # for each value in column, if condition, replace it with set_value - for row_ind, column_value in enumerate(column): - if get_op(operator, column_value, value): - self.data[row_ind][set_column_idx] = set_value + # get the condition and the set column + column = self.column_by_name(column_name) + set_column_idx = self.column_names.index(set_column) - # self._update() - # print(f"Updated {len(indexes_to_del)} rows") + # for each value in column, if condition, replace it with set_value + for row_ind, column_value in enumerate(column): + if get_op(operator, column_value, value): + self.data[row_ind][set_column_idx] = set_value + else: + # parse the condition + column_name, operator, value = self._parse_condition(condition) + + # get the condition and the set column + column = self.column_by_name(column_name) + set_column_idx = self.column_names.index(set_column) + + # set_columns_indx = [self.column_names.index(set_column_name) for set_column_name in set_column_names] + + # for each value in column, if condition, replace it with set_value + for row_ind, column_value in enumerate(column): + if get_op(operator, column_value, value): + self.data[row_ind][set_column_idx] = set_value + + # self._update() + # print(f"Updated {len(indexes_to_del)} rows") def _delete_where(self, condition): @@ -184,14 +201,22 @@ def _delete_where(self, condition): Operatores supported: (<,<=,==,>=,>) ''' - column_name, operator, value = self._parse_condition(condition) - indexes_to_del = [] - - column = self.column_by_name(column_name) - for index, row_value in enumerate(column): - if get_op(operator, row_value, value): - indexes_to_del.append(index) + operator = ' or ' + if (operator in condition): + splt = condition.split(operator) + for s in splt: + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + for index, row_value in enumerate(column): + if get_op(operator, row_value, value): + indexes_to_del.append(index) + else: + column_name, operator, value = self._parse_condition(condition) + column = self.column_by_name(column_name) + for index, row_value in enumerate(column): + if get_op(operator, row_value, value): + indexes_to_del.append(index) # we pop from highest to lowest index in order to avoid removing the wrong item # since we dont delete, we dont have to to pop in that order, but since delete is used @@ -236,28 +261,11 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by # if condition is None, return all rows # if not, return the rows with values where condition is met for value if condition is not None: - #print("condition is: " + condition) - # if or in condition: - ''' - operator = ' or ' - if (operator in condition): # salary = 2000 or salary > 6000 - splt = condition.split(operator) # salary = 20000, salary > 6000 - #column_name = self._parse_condition(splt[0])[0] - #column = self.column_by_name(column_name) - - for s in splt: - - self._select_where(return_columns, s, distinct, order_by, desc, limit) - - column_name, operator, value = self._parse_condition(s) - column = self.column_by_name(column_name) - rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] - - ''' - #elif (' or ' not in condition): + column_name, operator, value = self._parse_condition(condition) column = self.column_by_name(column_name) rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + else: rows = [i for i in range(len(self.data))] @@ -347,6 +355,7 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False return s_table + def order_by(self, column_name, desc=True): ''' Order table based on column. @@ -437,6 +446,7 @@ def _inner_join(self, table_right: Table, condition): return join_table + def _left_join(self, table_right: Table, condition): ''' Perform a left join on the table with the supplied table (right). @@ -468,6 +478,7 @@ def _left_join(self, table_right: Table, condition): return join_table + def _right_join(self, table_right: Table, condition): ''' Perform a right join on the table with the supplied table (right). @@ -499,6 +510,7 @@ def _right_join(self, table_right: Table, condition): return join_table + def _full_join(self, table_right: Table, condition): ''' Perform a full join on the table with the supplied table (right). @@ -541,6 +553,7 @@ def _full_join(self, table_right: Table, condition): return join_table + def show(self, no_of_rows=None, is_locked=False): ''' Print the table in a nice readable format. @@ -613,348 +626,52 @@ def _load_from_file(self, filename): self.__dict__.update(tmp_dict.__dict__) -# try to handle or operator - def _select_where_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, data=None): - ''' - Select and return a table containing specified columns and rows where condition is met. - - Args: - return_columns: list. The columns to be returned. - condition: string. A condition using the following format: - 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. - - Operatores supported: (<,<=,==,>=,>) - distinct: boolean. If True, the resulting table will contain only unique rows (False by default). - order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). - desc: boolean. If True, order_by will return results in descending order (False by default). - limit: int. An integer that defines the number of rows that will be returned (all rows if None). - ''' + def _select_where_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): + # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - # if condition is None, return all rows - # if not, return the rows with values where condition is met for value - if condition is not None: - flag = 0 # flag 4 or operator - operator = ' or ' - # if or in condition - if (operator in condition): # salary = 2000 or salary > 6000 - print("Inside or function.\n") - flag = 1 - splt = condition.split(operator) # [salary = 20000, salary > 6000] - - list = [] - #build temp variable - - for s in splt: - - #self._select_where(return_columns, s, distinct, order_by, desc, limit) - column_name, operator, value = self._parse_condition(s) - column = self.column_by_name(column_name) - rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] - - - # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - # we need to set the new column names/types and no of columns, since we might - # only return some columns - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - #s_table_copy = Table(load=dict) # copy of table - - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data - #list = list(set(map(lambda x: tuple(x), list))) if distinct else list - #print("list with data") - #print(list) - - if order_by: - s_table.order_by(order_by, desc) - - # if isinstance(limit, str): - # try: - # k = int(limit) - # except ValueError: - # raise Exception("The value following 'top' in the query should be a number.") - - # # Remove from the table's data all the None-filled rows, as they are not shown by default - # # Then, show the first k rows - # s_table.data.remove(len(s_table.column_names) * [None]) - # s_table.data = s_table.data[:k] - - if isinstance(limit,str): - #list = list.append([row for row in s_table.data if any(row)][:int(limit)]) - s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - - #print("table data with or: ") - #print(s_table.data) - - #s_table_copy.data.append(s_table.data) + lst = [] + operator = ' or ' # e.g salary = 20000 or salary > 60000 + splt = condition.split(operator) # salary = 20000, salary > 6000 + if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator + for s in splt: + #print(s) + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] - - list.append(s_table.data) - print("Data table in list: ") + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + s_table.data = (list(set(map(lambda x: tuple(x), s_table.data)))) if distinct else s_table.data + lst.append(s_table.data) - print(list) - print("\n\n\n") - #s_table.data = list - #print("list data") - #print(list) - #print("-----------") - #print("\nTable data with operator or are: ") - #print(s_table.data) - - #print("-----------") - #print("\nTable copy data with operator or are: ") - #print(s_table_copy.data) - #print("-----------") - - - #s_table.data = list - #return s_table_copy - return s_table - - - else: # condition is null - rows = [i for i in range(len(self.data))] - - # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - - # we need to set the new column names/types and no of columns, since we might - # only return some columns - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data - if order_by: s_table.order_by(order_by, desc) - - # if isinstance(limit, str): - # try: - # k = int(limit) - # except ValueError: - # raise Exception("The value following 'top' in the query should be a number.") - - # # Remove from the table's data all the None-filled rows, as they are not shown by default - # # Then, show the first k rows - # s_table.data.remove(len(s_table.column_names) * [None]) - # s_table.data = s_table.data[:k] + if isinstance(limit,str): s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + #print(lst) + output = [elem for twod in lst for elem in twod] + #print("output") + #print(output) + s_table.data = output #print(s_table.data) - return s_table - - - - - - def _select_where_or2(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): - ''' - Select and return a table containing specified columns and rows where condition is met. - - Args: - return_columns: list. The columns to be returned. - condition: string. A condition using the following format: - 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. - - Operatores supported: (<,<=,==,>=,>) - distinct: boolean. If True, the resulting table will contain only unique rows (False by default). - order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). - desc: boolean. If True, order_by will return results in descending order (False by default). - limit: int. An integer that defines the number of rows that will be returned (all rows if None). - ''' - - # if * return all columns, else find the column indexes for the columns specified - if return_columns == '*': - return_cols = [i for i in range(len(self.column_names))] - else: - return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - - # if condition is None, return all rows - # if not, return the rows with values where condition is met for value - if condition is not None: - #print("condition is: " + condition) - # if or in condition: - flag = 0 - operator = ' or ' - if (operator in condition): # salary = 2000 or salary > 6000 - print("Inside or function.") - flag = 1 - splt = condition.split(operator) # salary = 20000, salary > 6000 - - list = [] - #build temp variable - - for s in splt: - - self._select_where(return_columns, s, distinct, order_by, desc, limit) - - column_name, operator, value = self._parse_condition(s) - column = self.column_by_name(column_name) - rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] - - - # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - # we need to set the new column names/types and no of columns, since we might - # only return some columns - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data - #list = list(set(map(lambda x: tuple(x), list))) if distinct else list - #print("list with data") - #print(list) - - if order_by: - s_table.order_by(order_by, desc) - - # if isinstance(limit, str): - # try: - # k = int(limit) - # except ValueError: - # raise Exception("The value following 'top' in the query should be a number.") - - # # Remove from the table's data all the None-filled rows, as they are not shown by default - # # Then, show the first k rows - # s_table.data.remove(len(s_table.column_names) * [None]) - # s_table.data = s_table.data[:k] - if isinstance(limit,str): - #list = list.append([row for row in s_table.data if any(row)][:int(limit)]) - s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - - #print("table data with or: ") - #print(s_table.data) - - - list.append(s_table.data) - #print("or data list are: ") - #print(list) - #s_table.data = list - print("list data") - print(list) - print("-----------") - print("\nTable data with operator or are: ") - print(s_table.data) - s_table.data = list - return s_table - - elif (' or ' not in condition): - column_name, operator, value = self._parse_condition(condition) - column = self.column_by_name(column_name) - rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] - - else: - rows = [i for i in range(len(self.data))] - - if (flag == 0): - print("i am here") - # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - - # we need to set the new column names/types and no of columns, since we might - # only return some columns - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data - - if order_by: - s_table.order_by(order_by, desc) - - # if isinstance(limit, str): - # try: - # k = int(limit) - # except ValueError: - # raise Exception("The value following 'top' in the query should be a number.") - - # # Remove from the table's data all the None-filled rows, as they are not shown by default - # # Then, show the first k rows - # s_table.data.remove(len(s_table.column_names) * [None]) - # s_table.data = s_table.data[:k] - if isinstance(limit,str): - s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - - #print(s_table.data) - return s_table - - def _select_where_or3(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, data=None): - ''' - Select and return a table containing specified columns and rows where condition is met. - - Args: - return_columns: list. The columns to be returned. - condition: string. A condition using the following format: - 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. - - Operatores supported: (<,<=,==,>=,>) - distinct: boolean. If True, the resulting table will contain only unique rows (False by default). - order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). - desc: boolean. If True, order_by will return results in descending order (False by default). - limit: int. An integer that defines the number of rows that will be returned (all rows if None). - ''' - - - # if * return all columns, else find the column indexes for the columns specified - if return_columns == '*': - return_cols = [i for i in range(len(self.column_names))] - else: - return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - - # if condition is None, return all rows - # if not, return the rows with values where condition is met for value - if condition is not None: - # build a loop and check rows line 925 - column_name, operator, value = self._parse_condition(condition) - column = self.column_by_name(column_name) - rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] #with the next oneee - - else: # condition is null - rows = [i for i in range(len(self.data))] - - # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - - # we need to set the new column names/types and no of columns, since we might - # only return some columns - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data - - if order_by: - s_table.order_by(order_by, desc) + return s_table - if isinstance(limit,str): - s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - #print(s_table.data) - - - return s_table - - + + \ No newline at end of file From 3328d1fb463dc2b2910e3293bdaa73715fc97fb0 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 29 Jan 2023 17:44:01 +0200 Subject: [PATCH 22/83] Add and functionality --- miniDB/database.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 27da8408..4f32393f 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -369,10 +369,15 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ print("Condition is: " + condition+"\n") operator = ' or ' - if (operator in condition): # e.g salary = 2000 or salary > 6000 - + operator1 = ' and ' + if (operator in condition or operator1 in condition): # e.g salary = 2000 or salary > 6000 + flag = 1 # found - splt = condition.split(operator) + if (operator in condition): + splt = condition.split(operator) + else: + splt = condition.split(operator1) + lst = [item[0] for item in splt] # condition_column list #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) @@ -384,7 +389,11 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ bt = self._load_idx(index_name) table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) else: - table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + if (operator in condition): + table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + else: + table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) + # self.unlock_table(table_name) if save_as is not None: table._name = save_as From 4affeb391f7c1baf6d7c54efdfabfccb0d8da2cc Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 29 Jan 2023 17:45:25 +0200 Subject: [PATCH 23/83] Add and functionality --- miniDB/table.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 9b49c4e5..628364ae 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -203,14 +203,27 @@ def _delete_where(self, condition): ''' indexes_to_del = [] operator = ' or ' - if (operator in condition): + operator1 = ' and ' + if (operator in condition): # or in condition splt = condition.split(operator) for s in splt: column_name, operator, value = self._parse_condition(s) column = self.column_by_name(column_name) for index, row_value in enumerate(column): if get_op(operator, row_value, value): - indexes_to_del.append(index) + indexes_to_del.append(index) + + elif(operator1 in condition): # and in condition + splt = condition.split(operator1) + for s in splt: + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + for index, row_value in enumerate(column): + + if get_op(operator, row_value, value): + print("index ") + print(index) + indexes_to_del.append(index) else: column_name, operator, value = self._parse_condition(condition) column = self.column_by_name(column_name) @@ -671,6 +684,76 @@ def _select_where_or(self, return_columns, condition=None, distinct=False, order return s_table + def _select_where_and(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): + ''' + Select and return a table containing specified columns and rows where condition is met. + + Args: + return_columns: list. The columns to be returned. + condition: string. A condition using the following format: + 'column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value and 'not column[<,<=,==,>=,>]value and ...' . + + Operatores supported: (<,<=,==,>=,>) + distinct: boolean. If True, the resulting table will contain only unique rows (False by default). + order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). + desc: boolean. If True, order_by will return results in descending order (False by default). + limit: int. An integer that defines the number of rows that will be returned (all rows if None). + ''' + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + operator = ' and ' + splt = condition.split(operator) + print(splt) + if (len(splt)!=0): # if there are any conditions on the left and on the right side of and operator + + #print(s) + column_name, operator, value = self._parse_condition(splt[0]) + column = self.column_by_name(column_name) + + rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + + for cond in splt[1:]: + column_name, operator, value = self._parse_condition(cond) + column = self.column_by_name(column_name) + rows1 = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] + + rows = [c for c in rows if c in rows1] + if len(rows) == 0: # no common element + break + + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + + #print(s_table.data) + return s_table + + + + + + + From 0c33de419ff66350a371936590203bb84ac79cb9 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 29 Jan 2023 20:06:24 +0200 Subject: [PATCH 24/83] Add and functionality in update --- miniDB/table.py | 69 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 628364ae..e23398a2 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -153,6 +153,7 @@ def _update_rows(self, set_value, set_column, condition): ''' operator = ' or ' + operator1 = ' and ' if (operator in condition): splt = condition.split(operator) for s in splt: @@ -167,6 +168,41 @@ def _update_rows(self, set_value, set_column, condition): for row_ind, column_value in enumerate(column): if get_op(operator, column_value, value): self.data[row_ind][set_column_idx] = set_value + + elif(operator1 in condition): # and in condition + rows = [] + rows1 = [] + splt = condition.split(operator1) + + column_name, operator, value = self._parse_condition(splt[0]) + # get the condition and the set column + column = self.column_by_name(column_name) + set_column_idx = self.column_names.index(set_column) + + # for each value in column, if condition, replace it with set_value + for row_ind, column_value in enumerate(column): + if get_op(operator, column_value, value): + rows.append(row_ind) + + for s in splt[1:]: + # parse the condition + column_name, operator, value = self._parse_condition(s) + # get the condition and the set column + column = self.column_by_name(column_name) + set_column_idx = self.column_names.index(set_column) + + # for each value in column, if condition, replace it with set_value + for row_ind, column_value in enumerate(column): + if get_op(operator, column_value, value): + rows1.append(row_ind) + rows = [c for c in rows if c in rows1] + print(rows) + if len(rows) == 0: # no common element + break + + for row_ind in rows: + self.data[row_ind][set_column_idx] = set_value + else: # parse the condition column_name, operator, value = self._parse_condition(condition) @@ -197,7 +233,15 @@ def _delete_where(self, condition): condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. + 'value[<,<=,==,>=,>]column' or + + 'column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value and not column[<,<=,==,>=,>]value and ...' or + + 'column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value or not column[<,<=,==,>=,>]value and ...' . Operatores supported: (<,<=,==,>=,>) ''' @@ -215,15 +259,26 @@ def _delete_where(self, condition): elif(operator1 in condition): # and in condition splt = condition.split(operator1) - for s in splt: + indexes_to_del1 = [] + column_name, operator, value = self._parse_condition(splt[0]) + column = self.column_by_name(column_name) + + for index, row_value in enumerate(column): + if get_op(operator, row_value, value): + indexes_to_del.append(index) + #print(indexes_to_del) + + for s in splt[1:]: column_name, operator, value = self._parse_condition(s) column = self.column_by_name(column_name) for index, row_value in enumerate(column): - if get_op(operator, row_value, value): - print("index ") - print(index) - indexes_to_del.append(index) + indexes_to_del1.append(index) + #print(indexes_to_del1) + indexes_to_del = [c for c in indexes_to_del if c in indexes_to_del1] + #print(indexes_to_del) + if len(indexes_to_del) == 0: # no common element + break else: column_name, operator, value = self._parse_condition(condition) column = self.column_by_name(column_name) @@ -693,7 +748,7 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or 'not column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value and 'not column[<,<=,==,>=,>]value and ...' . + 'not column[<,<=,==,>=,>]value and not column[<,<=,==,>=,>]value and ...' . Operatores supported: (<,<=,==,>=,>) distinct: boolean. If True, the resulting table will contain only unique rows (False by default). From 14104156ec87d52112cc33686f656ff0fc4cd027 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 29 Jan 2023 20:52:41 +0200 Subject: [PATCH 25/83] Finish and in select function --- miniDB/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/table.py b/miniDB/table.py index e23398a2..26b0574a 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -196,7 +196,7 @@ def _update_rows(self, set_value, set_column, condition): if get_op(operator, column_value, value): rows1.append(row_ind) rows = [c for c in rows if c in rows1] - print(rows) + #print(rows) if len(rows) == 0: # no common element break From d8e42072ac632602708e6526f98d40663243fe3a Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 14:44:56 +0200 Subject: [PATCH 26/83] Add complex AND and OR functionality --- miniDB/database.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 4f32393f..55c42474 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -370,7 +370,30 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ operator = ' or ' operator1 = ' and ' - if (operator in condition or operator1 in condition): # e.g salary = 2000 or salary > 6000 + + if(operator in condition and operator1 in condition): + flag = 1 + if self.is_locked(table_name): + return + if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + bt = self._load_idx(index_name) + table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + else: + table = self.tables[table_name]._select_where_and_or(columns, condition, distinct, order_by, desc, limit) + + # self.unlock_table(table_name) + if save_as is not None: + table._name = save_as + self.table_from_object(table) + else: + if return_object: + return table + else: + return table.show() + + + elif (operator in condition or operator1 in condition): # e.g salary = 2000 or salary > 6000 flag = 1 # found if (operator in condition): @@ -380,8 +403,6 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ lst = [item[0] for item in splt] # condition_column list - #table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - if self.is_locked(table_name): return if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: @@ -393,7 +414,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) else: table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) - + # self.unlock_table(table_name) if save_as is not None: table._name = save_as @@ -403,7 +424,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ return table else: return table.show() - + else: if(condition[:4] == 'not '): From e1bf7e3a53682d4651554317542724ca8a1e9257 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 14:45:23 +0200 Subject: [PATCH 27/83] Add complex AND and OR functionality --- miniDB/table.py | 115 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 12 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 26b0574a..0b0fba2a 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -1,4 +1,5 @@ from __future__ import annotations +import itertools from tabulate import tabulate import pickle import os @@ -248,7 +249,22 @@ def _delete_where(self, condition): indexes_to_del = [] operator = ' or ' operator1 = ' and ' - if (operator in condition): # or in condition + indexes_to_del_and_or = [] + indexes_to_del1 = [] + + if (operator, operator1 in condition): + splt = condition.split(operator) + + for cond in splt: + indexes_to_del_and_or.append(self._delete_where(cond)) + print("indexes:") + print(indexes_to_del_and_or) + + res = list(set(tuple(sorted(sub)) for sub in indexes_to_del_and_or)) + print("res") + print(res) + + elif (operator in condition): # or in condition splt = condition.split(operator) for s in splt: column_name, operator, value = self._parse_condition(s) @@ -259,7 +275,7 @@ def _delete_where(self, condition): elif(operator1 in condition): # and in condition splt = condition.split(operator1) - indexes_to_del1 = [] + #indexes_to_del1 = [] column_name, operator, value = self._parse_condition(splt[0]) column = self.column_by_name(column_name) @@ -302,7 +318,7 @@ def _delete_where(self, condition): return indexes_to_del - def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): + def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' Select and return a table containing specified columns and rows where condition is met. @@ -319,13 +335,15 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by desc: boolean. If True, order_by will return results in descending order (False by default). limit: int. An integer that defines the number of rows that will be returned (all rows if None). ''' - + # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - + #print(return_columns) + #print("return cols in select") + #print(return_cols) # if condition is None, return all rows # if not, return the rows with values where condition is met for value if condition is not None: @@ -339,12 +357,14 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - + + # we need to set the new column names/types and no of columns, since we might # only return some columns dict['column_names'] = [self.column_names[i] for i in return_cols] dict['column_types'] = [self.column_types[i] for i in return_cols] + #print(dict['column_names']) s_table = Table(load=dict) s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data @@ -366,7 +386,10 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by s_table.data = [row for row in s_table.data if any(row)][:int(limit)] #print(s_table.data) - return s_table + if (flag): + return s_table.data, dict + else: + return s_table def _select_where_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): @@ -696,7 +719,23 @@ def _load_from_file(self, filename): def _select_where_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): - + ''' + Select and return a table containing specified columns and rows where condition is met. + + Args: + return_columns: list. The columns to be returned. + condition: string. A condition using the following format: + 'column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or + 'not column[<,<=,==,>=,>]value or not column[<,<=,==,>=,>]value and ...' . + + Operatores supported: (<,<=,==,>=,>) + distinct: boolean. If True, the resulting table will contain only unique rows (False by default). + order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). + desc: boolean. If True, order_by will return results in descending order (False by default). + limit: int. An integer that defines the number of rows that will be returned (all rows if None). + ''' + # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] @@ -739,7 +778,7 @@ def _select_where_or(self, return_columns, condition=None, distinct=False, order return s_table - def _select_where_and(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): + def _select_where_and(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' Select and return a table containing specified columns and rows where condition is met. @@ -763,9 +802,10 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + #print(return_cols) operator = ' and ' splt = condition.split(operator) - print(splt) + #print(splt) if (len(splt)!=0): # if there are any conditions on the left and on the right side of and operator #print(s) @@ -800,8 +840,59 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde if isinstance(limit,str): s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - #print(s_table.data) - return s_table + #print(s_table.column_names) + if (flag): + return s_table.data, dict + else: + return s_table + + + def _select_where_and_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): + + # if * return all columns, else find the column indexes for the columns specified + ''' + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + ''' + data = [] + operator1 = ' and ' + operator2 = ' or ' + + splt = condition.split(operator2) + #print(splt) + + # dict -> in order to get the new column names, since we might only return some columns + if (operator1 in splt[0]): # and in condition -> call it's method + dict = self._select_where_and(return_columns, splt[0], distinct, order_by, desc, limit, True)[1] + else: + dict = self._select_where(return_columns, splt[0], distinct, order_by, desc, limit, True)[1] + + for cond in splt: + if (operator1 in cond): + data.append(self._select_where_and(return_columns, cond, distinct, order_by, desc, limit, True)[0]) + #dict = self._select_where_and(return_columns, cond, distinct, order_by, desc, limit, True)[1] + else: + data.append(self._select_where(return_columns, cond, distinct, order_by, desc, limit, True)[0]) + #dict = self._select_where(return_columns, cond, distinct, order_by, desc, limit, True)[1] + + self = Table(load=dict) + #print(data) + data1 = [elem for twod in data for elem in twod] # convert 3D list into a 2D list + + # remove duplicate records but first sort the list + data1.sort() + new_list = list(l for l, _ in itertools.groupby(data1)) + + self.data = new_list # final data + return self + + + + + + From 6695ea9d0ba986c658e80110cf8e43783f9935d4 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 14:59:57 +0200 Subject: [PATCH 28/83] add complex AND and OR functionality in delete --- miniDB/table.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/miniDB/table.py b/miniDB/table.py index 0b0fba2a..5c8bad23 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -253,6 +253,9 @@ def _delete_where(self, condition): indexes_to_del1 = [] if (operator, operator1 in condition): + + self._delete_where_and_or(condition) + ''' splt = condition.split(operator) for cond in splt: @@ -263,6 +266,7 @@ def _delete_where(self, condition): res = list(set(tuple(sorted(sub)) for sub in indexes_to_del_and_or)) print("res") print(res) + ''' elif (operator in condition): # or in condition splt = condition.split(operator) @@ -316,6 +320,14 @@ def _delete_where(self, condition): # self._update() # we have to return the deleted indexes, since they will be appended to the insert_stack return indexes_to_del + + def _delete_where_and_or(self, condition): + t_indexes = [] + operator = ' and ' + splt = condition(operator) + for s in splt: + t_indexes.append(self._delete_where(s)) + print(t_indexes) def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): From e2f4d7452e6631e4288f842e6aa05e8c5f5f9c3d Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 15:11:09 +0200 Subject: [PATCH 29/83] add AND and OR functionality in delete --- miniDB/table.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 5c8bad23..85e6d415 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -252,10 +252,8 @@ def _delete_where(self, condition): indexes_to_del_and_or = [] indexes_to_del1 = [] - if (operator, operator1 in condition): - - self._delete_where_and_or(condition) - ''' + + ''' splt = condition.split(operator) for cond in splt: @@ -266,9 +264,9 @@ def _delete_where(self, condition): res = list(set(tuple(sorted(sub)) for sub in indexes_to_del_and_or)) print("res") print(res) - ''' + ''' - elif (operator in condition): # or in condition + if (operator in condition): # or in condition splt = condition.split(operator) for s in splt: column_name, operator, value = self._parse_condition(s) @@ -324,7 +322,7 @@ def _delete_where(self, condition): def _delete_where_and_or(self, condition): t_indexes = [] operator = ' and ' - splt = condition(operator) + splt = condition.split(operator) for s in splt: t_indexes.append(self._delete_where(s)) print(t_indexes) From bd5e3d86487dc088560b7ff3fda2f59016260e73 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 15:26:49 +0200 Subject: [PATCH 30/83] . --- miniDB/table.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 85e6d415..d7c93610 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -265,8 +265,12 @@ def _delete_where(self, condition): print("res") print(res) ''' + if (operator and operator1 in condition): + print("complex AND and OR found!") + print(condition) + self._delete_where_and_or(condition) - if (operator in condition): # or in condition + elif (operator in condition): # or in condition splt = condition.split(operator) for s in splt: column_name, operator, value = self._parse_condition(s) @@ -321,11 +325,13 @@ def _delete_where(self, condition): def _delete_where_and_or(self, condition): t_indexes = [] - operator = ' and ' - splt = condition.split(operator) + #operator1 = ' and ' + operator2 = ' or ' + splt = condition.split(operator2) for s in splt: - t_indexes.append(self._delete_where(s)) - print(t_indexes) + #t_indexes.append(self._delete_where(s)) + print(s) + #print(t_indexes) def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): From 6a0cade432cd15275c9cc54c4a517ad30da9cf5f Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 15:36:41 +0200 Subject: [PATCH 31/83] . --- miniDB/table.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/miniDB/table.py b/miniDB/table.py index d7c93610..8801195f 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -323,6 +323,7 @@ def _delete_where(self, condition): # we have to return the deleted indexes, since they will be appended to the insert_stack return indexes_to_del + def _delete_where_and_or(self, condition): t_indexes = [] #operator1 = ' and ' @@ -331,7 +332,9 @@ def _delete_where_and_or(self, condition): for s in splt: #t_indexes.append(self._delete_where(s)) print(s) - #print(t_indexes) + #t_indexes.append(self._delete_where(splt[0])) + t_indexes.append(self._delete_where(splt[1])) + print(t_indexes) def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): From 2772fda97994e29ab9baa1ab3037a1fdc880cd1f Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 16:06:06 +0200 Subject: [PATCH 32/83] . --- miniDB/table.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 8801195f..238ba3d1 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -246,29 +246,20 @@ def _delete_where(self, condition): Operatores supported: (<,<=,==,>=,>) ''' - indexes_to_del = [] + operator = ' or ' operator1 = ' and ' - indexes_to_del_and_or = [] + indexes_to_del = [] indexes_to_del1 = [] - - ''' - splt = condition.split(operator) - - for cond in splt: - indexes_to_del_and_or.append(self._delete_where(cond)) - print("indexes:") - print(indexes_to_del_and_or) - - res = list(set(tuple(sorted(sub)) for sub in indexes_to_del_and_or)) - print("res") - print(res) - ''' - if (operator and operator1 in condition): + if (operator in condition and operator1 in condition): print("complex AND and OR found!") print(condition) - self._delete_where_and_or(condition) + operator2 = ' or ' + splt = condition.split(operator2) + for s in splt: + self._delete_where(s) + #self._delete_where_and_or(condition) elif (operator in condition): # or in condition splt = condition.split(operator) @@ -325,16 +316,16 @@ def _delete_where(self, condition): def _delete_where_and_or(self, condition): - t_indexes = [] + #t_indexes = [] #operator1 = ' and ' operator2 = ' or ' splt = condition.split(operator2) for s in splt: + self._delete_where(s) #t_indexes.append(self._delete_where(s)) - print(s) - #t_indexes.append(self._delete_where(splt[0])) - t_indexes.append(self._delete_where(splt[1])) - print(t_indexes) + #print(s) + #print(t_indexes) + #return t_indexes def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): From 5815a212bc33bf56e94cdbe349122dfcdb919617 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 2 Feb 2023 16:24:44 +0200 Subject: [PATCH 33/83] complete AND and OR functionality in DELETE, UPDATE, SELECT --- miniDB/table.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 238ba3d1..b12c30f3 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -155,7 +155,12 @@ def _update_rows(self, set_value, set_column, condition): operator = ' or ' operator1 = ' and ' - if (operator in condition): + if (operator in condition and operator1 in condition): + splt = condition.split(operator) + for s in splt: + self._update_rows(set_value, set_column, s) + + elif (operator in condition): # or in condition splt = condition.split(operator) for s in splt: # parse the condition @@ -170,7 +175,7 @@ def _update_rows(self, set_value, set_column, condition): if get_op(operator, column_value, value): self.data[row_ind][set_column_idx] = set_value - elif(operator1 in condition): # and in condition + elif (operator1 in condition): # and in condition rows = [] rows1 = [] splt = condition.split(operator1) @@ -255,8 +260,7 @@ def _delete_where(self, condition): if (operator in condition and operator1 in condition): print("complex AND and OR found!") print(condition) - operator2 = ' or ' - splt = condition.split(operator2) + splt = condition.split(operator) for s in splt: self._delete_where(s) #self._delete_where_and_or(condition) @@ -314,8 +318,8 @@ def _delete_where(self, condition): # we have to return the deleted indexes, since they will be appended to the insert_stack return indexes_to_del - - def _delete_where_and_or(self, condition): + ''' + def _delete_where_and_or(self, condition): # uselless has to be deleted! #t_indexes = [] #operator1 = ' and ' operator2 = ' or ' @@ -326,7 +330,7 @@ def _delete_where_and_or(self, condition): #print(s) #print(t_indexes) #return t_indexes - + ''' def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' From a3fd0546eca6a1d1b02861f103ad412655f57894 Mon Sep 17 00:00:00 2001 From: DeppieK Date: Thu, 2 Feb 2023 22:11:07 +0200 Subject: [PATCH 34/83] changes on between stmt --- miniDB/misc.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index bc37f02e..9aeedac7 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -58,8 +58,8 @@ def split_condition(condition): #print(">>",condition) if len(splt)>1: left, right = splt[0].strip(), splt[1].strip() - #print(">>>",left,right) - if 'and' in right: + + if ' and ' in right: begin,end = right.split('and') begin = begin.strip() end = end.strip() @@ -92,6 +92,5 @@ def reverse_op(op): '>=' : '<=', '<' : '>', '<=' : '>=', - '=' : '=', - 'between': operator_between + '=' : '=' }.get(op) From 8ccca4909bbdce0101fe71364e5de5e8db7d4460 Mon Sep 17 00:00:00 2001 From: DeppieK Date: Thu, 2 Feb 2023 22:17:23 +0200 Subject: [PATCH 35/83] changes on between stmt --- mdb.py | 6 +----- miniDB/database.py | 5 ++--- miniDB/misc.py | 6 ++---- miniDB/table.py | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/mdb.py b/mdb.py index 035a285d..ccb9a44a 100644 --- a/mdb.py +++ b/mdb.py @@ -1,4 +1,3 @@ - import os import re from pprint import pprint @@ -42,7 +41,6 @@ def in_paren(qsplit, ind): def create_query_plan(query, keywords, action): ''' Given a query, the set of keywords that we expect to pe present and the overall action, return the query plan for this query. - This can and will be used recursively ''' @@ -206,9 +204,7 @@ def execute_dic(dic): def interpret_meta(command): """ Interpret meta commands. These commands are used to handle DB stuff, something that can not be easily handled with mSQL given the current architecture. - The available meta commands are: - lsdb - list databases lstb - list tables cdb - change/create database @@ -296,4 +292,4 @@ def remove_db(db_name): if isinstance(result,Table): result.show() except Exception: - print(traceback.format_exc()) + print(traceback.format_exc()) \ No newline at end of file diff --git a/miniDB/database.py b/miniDB/database.py index 90e4ac68..31e8d97c 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -257,7 +257,6 @@ def cast(self, column_name, table_name, cast_type): def insert_into(self, table_name, row_str): ''' Inserts data to given table. - Args: table_name: string. Name of table (must be part of database). row: list. A list of values to be inserted (will be casted to a predifined type automatically). @@ -319,9 +318,9 @@ def delete_from(self, table_name, condition): Operatores supported: (<,<=,==,>=,>) ''' self.load_database() - lock_ownership = self.lock_table(table_name, mode='x') deleted = self.tables[table_name]._delete_where(condition) + if lock_ownership: self.unlock_table(table_name) self._update() @@ -363,7 +362,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ condition_column = '' - # self.lock_table(table_name, mode='x') + self.lock_table(table_name, mode='x') if self.is_locked(table_name): return if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: diff --git a/miniDB/misc.py b/miniDB/misc.py index 9aeedac7..a0e8546f 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -54,12 +54,10 @@ def split_condition(condition): for op_key in ops.keys(): splt=condition.split(op_key) - #print(splt) - #print(">>",condition) if len(splt)>1: left, right = splt[0].strip(), splt[1].strip() - - if ' and ' in right: + + if op_key == 'between': begin,end = right.split('and') begin = begin.strip() end = end.strip() diff --git a/miniDB/table.py b/miniDB/table.py index 2909c313..a8c03aca 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -183,12 +183,12 @@ def _delete_where(self, condition): Operatores supported: (<,<=,==,>=,>) ''' column_name, operator, value = self._parse_condition(condition) - indexes_to_del = [] column = self.column_by_name(column_name) for index, row_value in enumerate(column): if get_op(operator, row_value, value): + print(">>>>",index,">>>>",row_value,">>>",value) indexes_to_del.append(index) # we pop from highest to lowest index in order to avoid removing the wrong item From ff734d5d28ec012d6aa4e873ee623c81c0560a39 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 3 Feb 2023 13:08:17 +0200 Subject: [PATCH 36/83] chech some things for issue #2 --- mdb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mdb.py b/mdb.py index a981e5be..b468430c 100644 --- a/mdb.py +++ b/mdb.py @@ -98,11 +98,13 @@ def create_query_plan(query, keywords, action): dic['create table'] = dic['create table'].removesuffix(args).strip() arg_nopk = args.replace('primary key', '')[1:-1] arglist = [val.strip().split(' ') for val in arg_nopk.split(',')] + #print(arglist) # see the type of the arguments e.g str, int etc dic['column_names'] = ','.join([val[0] for val in arglist]) dic['column_types'] = ','.join([val[1] for val in arglist]) if 'primary key' in args: arglist = args[1:-1].split(' ') dic['primary key'] = arglist[arglist.index('primary')-2] + #if unique in args maybeeee else: dic['primary key'] = None From 4a653ba651b4f764510905804c623dddebf93bf6 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 3 Feb 2023 13:09:52 +0200 Subject: [PATCH 37/83] check some things for issue no 2 --- mdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdb.py b/mdb.py index b468430c..0e0a82d5 100644 --- a/mdb.py +++ b/mdb.py @@ -98,7 +98,7 @@ def create_query_plan(query, keywords, action): dic['create table'] = dic['create table'].removesuffix(args).strip() arg_nopk = args.replace('primary key', '')[1:-1] arglist = [val.strip().split(' ') for val in arg_nopk.split(',')] - #print(arglist) # see the type of the arguments e.g str, int etc + #print(arglist) # see the type of the arguments e.g str, int etc dic['column_names'] = ','.join([val[0] for val in arglist]) dic['column_types'] = ','.join([val[1] for val in arglist]) if 'primary key' in args: From c028329ea2fac2cda1979652da45c8a5421cafa6 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 3 Feb 2023 14:21:59 +0200 Subject: [PATCH 38/83] fix some things --- miniDB/database.py | 5 +++-- miniDB/misc.py | 7 +++---- miniDB/table.py | 14 ++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 1bce1f6f..c5025988 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -369,8 +369,9 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ operator = ' or ' operator1 = ' and ' + operator3 = ' between ' - if(operator in condition and operator1 in condition): + if(operator in condition and operator1 in condition and operator3 not in condition): flag = 1 if self.is_locked(table_name): return @@ -392,7 +393,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ return table.show() - elif (operator in condition or operator1 in condition): # e.g salary = 2000 or salary > 6000 + elif (operator in condition or operator1 in condition and operator3 not in condition): # e.g salary = 2000 or salary > 6000 flag = 1 # found if (operator in condition): diff --git a/miniDB/misc.py b/miniDB/misc.py index 64bd14fc..41361a5f 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -34,7 +34,7 @@ def get_op(op, a, b): '>=': operator.ge, '<=': operator.le, '=': operator.eq, - '<>': operator.ne, + '!>': operator.ne, 'between': operator_between } @@ -51,7 +51,6 @@ def split_condition(condition): '=': operator.eq, '>': operator.gt, '<': operator.lt, - '<': operator.lt, 'between': operator_between } @@ -67,7 +66,7 @@ def split_condition(condition): if len(splt)>1: left, right = splt[0].strip(), splt[1].strip() - if op_key == 'between': + if op_key == 'between': # between in condition begin,end = right.split('and') begin = begin.strip() end = end.strip() @@ -81,7 +80,6 @@ def split_condition(condition): raise ValueError(f'Invalid condition: {condition}\nDouble quotation marks are not allowed inside values.') return left, op_key, right - if right[0] == '"' == right[-1]: # If the value has leading and trailing quotes, remove them. right = right.strip('"') elif ' ' in right: # If it has whitespaces but no leading and trailing double quotes, throw. @@ -117,6 +115,7 @@ def not_op(op): '=' : '<>' }.get(op) + def split_not_condition(condition): # not salary > 50000 #condition = condition[4:] # salary > 50000 splt = condition.split(' ') diff --git a/miniDB/table.py b/miniDB/table.py index f43220d4..f47553ff 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -155,12 +155,13 @@ def _update_rows(self, set_value, set_column, condition): operator = ' or ' operator1 = ' and ' - if (operator in condition and operator1 in condition): + operator3 = ' between ' + if (operator in condition and operator1 in condition and operator3 not in condition): splt = condition.split(operator) for s in splt: self._update_rows(set_value, set_column, s) - elif (operator in condition): # or in condition + elif (operator in condition and operator3 not in condition): # or in condition splt = condition.split(operator) for s in splt: # parse the condition @@ -175,7 +176,7 @@ def _update_rows(self, set_value, set_column, condition): if get_op(operator, column_value, value): self.data[row_ind][set_column_idx] = set_value - elif (operator1 in condition): # and in condition + elif (operator1 in condition and operator3 not in condition): # and in condition rows = [] rows1 = [] splt = condition.split(operator1) @@ -254,10 +255,11 @@ def _delete_where(self, condition): operator = ' or ' operator1 = ' and ' + operator3 = ' between ' indexes_to_del = [] indexes_to_del1 = [] - if (operator in condition and operator1 in condition): + if (operator in condition and operator1 in condition and operator3 not in condition): print("complex AND and OR found!") print(condition) splt = condition.split(operator) @@ -265,7 +267,7 @@ def _delete_where(self, condition): self._delete_where(s) #self._delete_where_and_or(condition) - elif (operator in condition): # or in condition + elif (operator in condition and operator3 not in condition): # or in condition splt = condition.split(operator) for s in splt: column_name, operator, value = self._parse_condition(s) @@ -274,7 +276,7 @@ def _delete_where(self, condition): if get_op(operator, row_value, value): indexes_to_del.append(index) - elif(operator1 in condition): # and in condition + elif(operator1 in condition and operator3 not in condition): # and in condition splt = condition.split(operator1) #indexes_to_del1 = [] column_name, operator, value = self._parse_condition(splt[0]) From dce75769612f585147f8cf2c117157d21c8360bd Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 4 Feb 2023 00:47:04 +0200 Subject: [PATCH 39/83] add 'unique' in columns --- mdb.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/mdb.py b/mdb.py index b30bbef9..0a1de7f6 100644 --- a/mdb.py +++ b/mdb.py @@ -94,18 +94,72 @@ def create_query_plan(query, keywords, action): if action=='create table': args = dic['create table'][dic['create table'].index('('):dic['create table'].index(')')+1] + #print("\nargs") # uncomment + #print(type(args)) + #print(args[0]) -> olo mazi 1 string + #print(args) # uncomment dic['create table'] = dic['create table'].removesuffix(args).strip() + #arg_list = [val.strip().split(' ') for val in args[1:-1].split(',')] + #print(arg_list) + + # 4 primary key arg_nopk = args.replace('primary key', '')[1:-1] + #print(arg_nopk) # uncomment arglist = [val.strip().split(' ') for val in arg_nopk.split(',')] - #print(arglist) # see the type of the arguments e.g str, int etc + #print("argslist without pk") #uncomment + #print(arglist) # see the type of the arguments e.g str, int etc #uncomment + + # 4 unique columns + arg_nounique = args.replace('unique', '')[1:-1] + #print(arg_nounique) # uncomment + arglist1 = [val.strip().split(' ') for val in arg_nounique.split(',')] + #print("argslist1 without unique") # uncomment + #print(arglist1) # uncomment + + dic['column_names'] = ','.join([val[0] for val in arglist]) dic['column_types'] = ','.join([val[1] for val in arglist]) + #print("dic b4") # uncomment + #print(dic) # same + if 'primary key' in args: + #print("primary here") arglist = args[1:-1].split(' ') - dic['primary key'] = arglist[arglist.index('primary')-2] - #if unique in args maybeeee + ''' + print("arglist is") + print(arglist) + print("dic after") + ''' + dic['primary key'] = arglist[arglist.index('primary')-2] # -2 gia na vreis to onoma toy key, an phgaine -1 tha evriske ton typo toy key e.g string/ integer + #print(dic) # uncomment else: dic['primary key'] = None + + # handle unique columns + if 'unique' in args: + #for a in arg_list: + #if (' unique ' in a and ' primary key' not in a): + print("unique column found") + arglist1 = args[1:-1].split(' ') + ''' + print("arglist1 is") + print(arglist1) + print("dic after") + ''' + indx_lst = [idx for idx, value in enumerate(arglist1) if value == 'unique' or value == 'unique,'] + ''' + print("indexes 4 unique") + print(indx_lst) + ''' + for n in indx_lst: + #print(n) + dic['unique'] = ','.join(arglist1[n-2] for n in indx_lst) + #arglist1[n-2] # -2 gia na vreis to onoma toy key, an phgaine -1 tha evriske ton typo toy key e.g string/ integer + #print(dic) + else: + dic['unique'] = None + #print("\n") + print(dic) if action=='import': dic = {'import table' if key=='import' else key: val for key, val in dic.items()} From 0d529ee7eb14ad2647bb3f7602eda0d4be6a20bd Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 5 Feb 2023 11:40:09 +0200 Subject: [PATCH 40/83] set the unique attribute for issue no 2 --- mdb.py | 16 ++++++++++------ miniDB/database.py | 13 +++++++------ miniDB/table.py | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/mdb.py b/mdb.py index 0a1de7f6..52b01b8e 100644 --- a/mdb.py +++ b/mdb.py @@ -94,6 +94,7 @@ def create_query_plan(query, keywords, action): if action=='create table': args = dic['create table'][dic['create table'].index('('):dic['create table'].index(')')+1] + print("\n") #print("\nargs") # uncomment #print(type(args)) #print(args[0]) -> olo mazi 1 string @@ -106,9 +107,10 @@ def create_query_plan(query, keywords, action): arg_nopk = args.replace('primary key', '')[1:-1] #print(arg_nopk) # uncomment arglist = [val.strip().split(' ') for val in arg_nopk.split(',')] - #print("argslist without pk") #uncomment - #print(arglist) # see the type of the arguments e.g str, int etc #uncomment - + ''' + print("argslist without pk") #uncomment + print(arglist) # see the type of the arguments e.g str, int etc #uncomment + ''' # 4 unique columns arg_nounique = args.replace('unique', '')[1:-1] #print(arg_nounique) # uncomment @@ -117,8 +119,10 @@ def create_query_plan(query, keywords, action): #print(arglist1) # uncomment - dic['column_names'] = ','.join([val[0] for val in arglist]) - dic['column_types'] = ','.join([val[1] for val in arglist]) + dic['column_names'] = ','.join([val[0] for val in arglist1]) + #print(dic['column_names']) + dic['column_types'] = ','.join([val[1] for val in arglist1]) + #print(dic['column_types']) #print("dic b4") # uncomment #print(dic) # same @@ -139,7 +143,7 @@ def create_query_plan(query, keywords, action): if 'unique' in args: #for a in arg_list: #if (' unique ' in a and ' primary key' not in a): - print("unique column found") + #print("unique column(s) here") arglist1 = args[1:-1].split(' ') ''' print("arglist1 is") diff --git a/miniDB/database.py b/miniDB/database.py index c5025988..c169e230 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -55,7 +55,7 @@ def __init__(self, name, load=True, verbose = True): self.create_table('meta_length', 'table_name,no_of_rows', 'str,int') self.create_table('meta_locks', 'table_name,pid,mode', 'str,int,str') self.create_table('meta_insert_stack', 'table_name,indexes', 'str,list') - self.create_table('meta_indexes', 'table_name,index_name', 'str,str') + self.create_table('meta_indexes', 'table_name,table_column,index_name', 'str,str,str') self.save_database() def save_database(self): @@ -102,7 +102,7 @@ def _update(self): self._update_meta_insert_stack() - def create_table(self, name, column_names, column_types, primary_key=None, load=None): + def create_table(self, name, column_names, column_types, primary_key=None, unique=None, load=None): ''' This method create a new table. This table is saved and can be accessed via db_object.tables['table_name'] or db_object.table_name @@ -113,8 +113,9 @@ def create_table(self, name, column_names, column_types, primary_key=None, load= primary_key: string. The primary key (if it exists). load: boolean. Defines table object parameters as the name of the table and the column names. ''' + #print(primary_key) # print('here -> ', column_names.split(',')) - self.tables.update({name: Table(name=name, column_names=column_names.split(','), column_types=column_types.split(','), primary_key=primary_key, load=load)}) + self.tables.update({name: Table(name=name, column_names=column_names.split(','), column_types=column_types.split(','), primary_key=primary_key, unique=unique.split(',') if unique is not None else None, load=load)}) # self._name = Table(name=name, column_names=column_names, column_types=column_types, load=load) # check that new dynamic var doesnt exist already # self.no_of_tables += 1 @@ -748,14 +749,14 @@ def create_index(self, index_name, table_name, index_type='btree'): table_name: string. Table name (must be part of database). index_name: string. Name of the created index. ''' - if self.tables[table_name].pk_idx is None: # if no primary key, no index - raise Exception('Cannot create index. Table has no primary key.') + if (self.tables[table_name].pk_idx is None and self.tables[table_name].unique_cols_idx is None): # if no primary key, no index + raise Exception('Cannot create index. Table has no primary key or unique columns.') if index_name not in self.tables['meta_indexes'].column_by_name('index_name'): # currently only btree is supported. This can be changed by adding another if. if index_type=='btree': logging.info('Creating Btree index.') # insert a record with the name of the index and the table on which it's created to the meta_indexes table - self.tables['meta_indexes']._insert([table_name, index_name]) + self.tables['meta_indexes']._insert([table_name, self.tables[table_name].pk , index_name]) # crate the actual index self._construct_index(table_name, index_name) self.save_database() diff --git a/miniDB/table.py b/miniDB/table.py index f47553ff..2b5d4c60 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -27,9 +27,11 @@ class Table: - a dictionary that includes the appropriate info (all the attributes in __init__) ''' - def __init__(self, name=None, column_names=None, column_types=None, primary_key=None, load=None): + def __init__(self, name=None, column_names=None, column_types=None, primary_key=None, unique=None, load=None): + if load is not None: + #print("here") # if load is a dict, replace the object dict with it (replaces the object with the specified one) if isinstance(load, dict): self.__dict__.update(load) @@ -41,15 +43,33 @@ def __init__(self, name=None, column_names=None, column_types=None, primary_key= # if name, columns_names and column types are not none elif (name is not None) and (column_names is not None) and (column_types is not None): + #print("here1") self._name = name if len(column_names)!=len(column_types): raise ValueError('Need same number of column names and types.') self.column_names = column_names + self.unique = unique self.columns = [] + self.unique_cols_idx = [] + #self.unique_cols = [] + + #self.unique_cols= [] + #print(self.unique) + #print(self.column_names) + ''' + for c in self.unique: + if c not in self.__dir__(): + # this is used in order to be able to call a column using its name as an attribute. + # example: instead of table.columns['column_name'], we do table.column_name + setattr(self, c, []) + self.unique.append([]) + else: + raise Exception(f'"{c}" attribute already exists in "{self.__class__.__name__} "class.') + ''' for col in self.column_names: if col not in self.__dir__(): # this is used in order to be able to call a column using its name as an attribute. @@ -67,8 +87,18 @@ def __init__(self, name=None, column_names=None, column_types=None, primary_key= self.pk_idx = self.column_names.index(primary_key) else: self.pk_idx = None - + + if unique is not None: + for c in unique: + self.unique_cols_idx.append(self.column_names.index(c)) + else: + self.unique_cols_idx = [] + self.pk = primary_key + print("pk is: ",self.pk) + self.unique = unique + print("unique cols are: ",self.unique) + print("unique cols indexes are: ",self.unique_cols_idx) # self._update() # if any of the name, columns_names and column types are none. return an empty table object From 032470c3472dce55bed2338ef62e437672883d11 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 7 Feb 2023 18:22:40 +0200 Subject: [PATCH 41/83] handle unique for btree indexing... --- mdb.py | 3 +- miniDB/btree.py | 20 +++++- miniDB/database.py | 93 +++++++++++++++++--------- miniDB/misc.py | 4 +- miniDB/table.py | 23 +++++-- sql_files/smallRelationsInsertFile.sql | 6 +- 6 files changed, 106 insertions(+), 43 deletions(-) diff --git a/mdb.py b/mdb.py index 52b01b8e..9fad0c6d 100644 --- a/mdb.py +++ b/mdb.py @@ -234,7 +234,8 @@ def interpret(query): 'unlock table': ['unlock table', 'force'], 'delete from': ['delete from', 'where'], 'update table': ['update table', 'set', 'where'], - 'create index': ['create index', 'on', 'using'], + #'create index': ['create index', 'on', 'using'], + 'create index': ['create index', 'on', 'column', 'using'], 'drop index': ['drop index'], 'create view' : ['create view', 'as'] } diff --git a/miniDB/btree.py b/miniDB/btree.py index f0676209..0053a918 100644 --- a/miniDB/btree.py +++ b/miniDB/btree.py @@ -28,10 +28,20 @@ def find(self, value, return_ops=False): ops = 0 # number of operations (<>= etc). Used for benchmarking if self.is_leaf: # return - + + ''' + if (isinstance(value, int)): + value = float(value) + ''' + #print(value) # for each value in the node, if the user supplied value is smaller, return the btrees value index # else (no value in the node is larger) return the last ptr + #print(self.values) + #for index, existing_val in enumerate(self.values): + #print("existing val: ", existing_val) + #print("index: ", index) for index, existing_val in enumerate(self.values): + #print("existing val: ", existing_val) ops+=1 if value is None or existing_val is None: continue @@ -288,7 +298,14 @@ def find(self, operator, value): operator: string. The provided evaluation operator. value: float. The value being searched for. ''' + + ''' + if (isinstance(value, int)): + value = float(value) + ''' results = [] + + # find the index of the node that the element should exist in leaf_idx, ops = self._search(value, True) target_node = self.nodes[leaf_idx] @@ -343,6 +360,7 @@ def find(self, operator, value): target_node = self.nodes[target_node.left_sibling] results.extend(target_node.ptrs) + # print the number of operations (usefull for benchamrking) # print(f'With BTree -> {ops} comparison operations') return results diff --git a/miniDB/database.py b/miniDB/database.py index c169e230..d73e10c1 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -372,11 +372,14 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ operator1 = ' and ' operator3 = ' between ' + # case: complex AND and OR conditions if(operator in condition and operator1 in condition and operator3 not in condition): flag = 1 if self.is_locked(table_name): return + # has to be changed if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + print("handle complex AND and OR btree") index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] bt = self._load_idx(index_name) table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) @@ -406,6 +409,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ if self.is_locked(table_name): return + #has to be changed if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] bt = self._load_idx(index_name) @@ -429,33 +433,37 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ else: if(condition[:4] == 'not '): - - #print("condition[0] is: " + condition[0]) - #print("condition[1] is: " + condition[1]) - #print("condition[2] is: " + condition[2]) - #print("condition[3] is: " + condition[3]) + print("Not has been found in condition!") condition_column = split_not_condition(condition)[0] + ''' print("Column is: " + condition_column) print("Operator is: " + split_not_condition(condition)[1]) print("Value is: " + split_not_condition(condition)[2] + '\n') - + ''' else: - print("i am here") - print(condition) + #print("Not not in condition") + #print(condition) condition_column = split_condition(condition)[0] + ''' print("Column is: " + condition_column) print("Operator is: " + split_condition(condition)[1]) print("Value is: " + split_condition(condition)[2] + '\n') - - else: + ''' + else: # just a simple select query condition_column = '' if (flag == 0): # or has not been found # self.lock_table(table_name, mode='x') if self.is_locked(table_name): return - if self._has_index(table_name) and condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: + #print(self.tables[table_name].unique) + #print(self._has_index(table_name)) + #print(condition_column in self.tables[table_name].unique) + if (self._has_index(table_name) and ((self.tables[table_name].pk_idx is not None and + condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]) or + condition_column in self.tables[table_name].unique)): + print("btree here") index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] bt = self._load_idx(index_name) table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) @@ -740,57 +748,75 @@ def _update_meta_insert_stack_for_tb(self, table_name, new_stack): # indexes - def create_index(self, index_name, table_name, index_type='btree'): + def create_index(self, index_name, table_name, column_name, index_type='btree'): ''' Creates an index on a specified table with a given name. - Important: An index can only be created on a primary key (the user does not specify the column). - + The index is created over a primary key or over a unique column + (the user has to specify the column). + Args: - table_name: string. Table name (must be part of database). index_name: string. Name of the created index. - ''' - if (self.tables[table_name].pk_idx is None and self.tables[table_name].unique_cols_idx is None): # if no primary key, no index - raise Exception('Cannot create index. Table has no primary key or unique columns.') - if index_name not in self.tables['meta_indexes'].column_by_name('index_name'): + table_name: string with the following format: + TableName 'column' columnName index_type + IMPORTANT: The TableName (must be part of database) + column_name: string. Name of the column where the index is created over (must be part of database). + + ''' + + if (column_name != None): # look 4 unique columns + print("case: look 4 pk or 4 unique column") + #print(self.tables[table_name].pk) + if (column_name != self.tables[table_name].pk): + print("case: search 4 unique column") + + if (column_name not in self.tables[table_name].unique): + raise Exception('Cannot create index. The column you specified is not unique.') + + if index_name not in self.tables['meta_indexes'].column_by_name('index_name'): # currently only btree is supported. This can be changed by adding another if. - if index_type=='btree': - logging.info('Creating Btree index.') - # insert a record with the name of the index and the table on which it's created to the meta_indexes table - self.tables['meta_indexes']._insert([table_name, self.tables[table_name].pk , index_name]) - # crate the actual index - self._construct_index(table_name, index_name) - self.save_database() - else: - raise Exception('Cannot create index. Another index with the same name already exists.') - - def _construct_index(self, table_name, index_name): + if index_type == 'btree': # case1 : index btree + logging.info('Creating Btree index.') + # insert a record with the name of the index and the table on which it's created to the meta_indexes table + self.tables['meta_indexes']._insert([table_name, column_name , index_name]) + # crate the actual index + self._construct_index(table_name, column_name, index_name) + self.save_database() + else: + raise Exception('Cannot create index. Another index with the same name already exists.') + + + def _construct_index(self, table_name, column_name, index_name): ''' Construct a btree on a table and save. Args: table_name: string. Table name (must be part of database). + column_name: string. Name of the table's column where the index is created over (must be part of database). index_name: string. Name of the created index. ''' bt = Btree(3) # 3 is arbitrary # for each record in the primary key of the table, insert its value and index to the btree - for idx, key in enumerate(self.tables[table_name].column_by_name(self.tables[table_name].pk)): + # for each record in the specified unique column of the table, insert its value and index to the btree + for idx, key in enumerate(self.tables[table_name].column_by_name(column_name)): if key is None: continue bt.insert(key, idx) # save the btree - self._save_index(index_name, bt) + self._save_index( index_name, bt) def _has_index(self, table_name): ''' Check whether the specified table's primary key column is indexed. + Check whether the specified table's unique column is indexed. Args: table_name: string. Table name (must be part of database). ''' return table_name in self.tables['meta_indexes'].column_by_name('table_name') + def _save_index(self, index_name, index): ''' Save the index object. @@ -807,6 +833,7 @@ def _save_index(self, index_name, index): with open(f'{self.savedir}/indexes/meta_{index_name}_index.pkl', 'wb') as f: pickle.dump(index, f) + def _load_idx(self, index_name): ''' Load and return the specified index. @@ -819,6 +846,7 @@ def _load_idx(self, index_name): f.close() return index + def drop_index(self, index_name): ''' Drop index from current database. @@ -836,6 +864,7 @@ def drop_index(self, index_name): self.save_database() + def handle_or_op(self, columns, table_name, s, distinct=None, order_by=None, \ limit=True, desc=None, save_as=None, return_object=True): diff --git a/miniDB/misc.py b/miniDB/misc.py index 41361a5f..4cd58918 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -12,6 +12,7 @@ def operator_between(value, condition): if (begin.isnumeric() and end.isnumeric()): begin = int(begin) end = int(end) + value = int(value) if (value >= begin) and (value <= end): return True @@ -34,7 +35,7 @@ def get_op(op, a, b): '>=': operator.ge, '<=': operator.le, '=': operator.eq, - '!>': operator.ne, + '<>': operator.ne, 'between': operator_between } @@ -47,7 +48,6 @@ def split_condition(condition): ops = {'>=': operator.ge, '<=': operator.le, - '!=': operator.ne, '=': operator.eq, '>': operator.gt, '<': operator.lt, diff --git a/miniDB/table.py b/miniDB/table.py index 2b5d4c60..9e65636c 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -96,6 +96,7 @@ def __init__(self, name=None, column_names=None, column_types=None, primary_key= self.pk = primary_key print("pk is: ",self.pk) + print("pk index: ",self.pk_idx) self.unique = unique print("unique cols are: ",self.unique) print("unique cols indexes are: ",self.unique_cols_idx) @@ -387,9 +388,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by return_cols = [i for i in range(len(self.column_names))] else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - #print(return_columns) - #print("return cols in select") - #print(return_cols) + # if condition is None, return all rows # if not, return the rows with values where condition is met for value if condition is not None: @@ -449,9 +448,22 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False column_name, operator, value = self._parse_condition(condition) + #table_name = condition.split(' where')[0] + + #print(self.column_names[0]) + #print(self.unique_cols_idx) + #print(self.column_names[i] for i in self.unique_cols_idx) + flag = False + for i in self.unique_cols_idx: + if column_name == self.column_names[i]: + flag = True + break + + if (flag is False): + print('Column is not unique. Aborting') # if the column in condition is not a primary key, abort the select - if column_name != self.column_names[self.pk_idx]: + elif (self.pk_idx and column_name != self.column_names[self.pk_idx]): print('Column is not PK. Aborting') # here we run the same select twice, sequentially and using the btree. @@ -468,12 +480,14 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False # btree find rows = bt.find(operator, value) + print(operator) try: k = int(limit) except TypeError: k = None # same as simple select from now on + rows = rows[:k] # TODO: this needs to be dumbed down dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} @@ -749,6 +763,7 @@ def _parse_condition(self, condition, join=False): coltype = self.column_types[self.column_names.index(left)] if op == 'between': + print("between has been found") return left, op, right return left, op, coltype(right) diff --git a/sql_files/smallRelationsInsertFile.sql b/sql_files/smallRelationsInsertFile.sql index d05d81b9..a8721e32 100644 --- a/sql_files/smallRelationsInsertFile.sql +++ b/sql_files/smallRelationsInsertFile.sql @@ -1,7 +1,7 @@ -create table classroom (building str, room_number str, capacity int); -create table department (dept_name str primary key, building str, budget int); +create table classroom (building str unique, room_number str, capacity int unique); +create table department (dept_name str primary key, building str, budget int unique); create table course (course_id str primary key, title str, dept_name str, credits int); -create table instructor (ID str primary key, name str, dept_name str, salary int); +create table instructor (ID str primary key, name str, dept_name str, salary int unique); create table section (course_id str, sec_id str, semester str, year int, building str, room_number str, time_slot_id str); create table teaches (ID str, course_id str, sec_id str, semester str, year int); create table student (ID str primary key, name str, dept_name str, tot_cred int); From 614b3e1c491b2d254db6489c281b0ea50dde1ea2 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Tue, 7 Feb 2023 18:27:55 +0200 Subject: [PATCH 42/83] fix some thing for unique in btree indexing --- miniDB/database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index d73e10c1..68f4a403 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -783,8 +783,9 @@ def create_index(self, index_name, table_name, column_name, index_type='btree'): self.save_database() else: raise Exception('Cannot create index. Another index with the same name already exists.') - - + else: + raise Exception('Cannot create index. You have to specify the column first.') + def _construct_index(self, table_name, column_name, index_name): ''' Construct a btree on a table and save. From a5c9129652038596d586045751cc540bd3df86fe Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 9 Feb 2023 15:59:42 +0200 Subject: [PATCH 43/83] Print some messages to see the tree's data --- miniDB/btree.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miniDB/btree.py b/miniDB/btree.py index 0053a918..aaaab14b 100644 --- a/miniDB/btree.py +++ b/miniDB/btree.py @@ -308,6 +308,8 @@ def find(self, operator, value): # find the index of the node that the element should exist in leaf_idx, ops = self._search(value, True) + print("lead idx: ",leaf_idx) + print("ops: ", ops) target_node = self.nodes[leaf_idx] if operator == '=': From e22235c32a9758e7274860e5d5559a575c571c50 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 9 Feb 2023 16:02:05 +0200 Subject: [PATCH 44/83] Add keyword unique to some columns to handle btree index over unique columns. --- sql_files/smallRelationsInsertFile.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql_files/smallRelationsInsertFile.sql b/sql_files/smallRelationsInsertFile.sql index a8721e32..2a897d4a 100644 --- a/sql_files/smallRelationsInsertFile.sql +++ b/sql_files/smallRelationsInsertFile.sql @@ -1,7 +1,7 @@ create table classroom (building str unique, room_number str, capacity int unique); create table department (dept_name str primary key, building str, budget int unique); create table course (course_id str primary key, title str, dept_name str, credits int); -create table instructor (ID str primary key, name str, dept_name str, salary int unique); +create table instructor (ID str primary key, name str unique, dept_name str, salary int unique); create table section (course_id str, sec_id str, semester str, year int, building str, room_number str, time_slot_id str); create table teaches (ID str, course_id str, sec_id str, semester str, year int); create table student (ID str primary key, name str, dept_name str, tot_cred int); From 1f424fcd213504cc7c1ee26fa070bd790e213c63 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 9 Feb 2023 16:04:04 +0200 Subject: [PATCH 45/83] Fix some issues and handle btree index in conditions --- miniDB/database.py | 112 +++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 68f4a403..ee1474aa 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -370,20 +370,46 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ operator = ' or ' operator1 = ' and ' + operator2 = ' not ' operator3 = ' between ' + # case: complex AND and OR conditions if(operator in condition and operator1 in condition and operator3 not in condition): flag = 1 if self.is_locked(table_name): return + # has to be changed - if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: - print("handle complex AND and OR btree") - index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] - bt = self._load_idx(index_name) - table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + #if self._has_index(table_name) and (item in self.tables['meta_indexes'].column_by_name('table_column') for item in columns_name_lst): + if self._has_index(table_name) and operator2 not in condition and 'not ' not in condition: + + columns_name_lst = [] + splt1 = condition.split(operator) # split or + for cond in splt1: + if (operator1 in cond): + l = cond.split(operator1) + for i in l: + columns_name_lst.append(i.split(' ')[0]) + else: + columns_name_lst.append(cond.split(' ')[0]) + print("Columns name list is: ",columns_name_lst) + + sum = 0 + for i in columns_name_lst: + if i in self.tables['meta_indexes'].column_by_name('table_column'): + sum+=1 + if sum == len(columns_name_lst): + print("All columns in meta indexes table.\nHandle complex AND and OR in btree.") + + #has to be changed + index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + bt = self._load_idx(index_name) + + #has to change! + table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) else: + print("No select with btree :(") table = self.tables[table_name]._select_where_and_or(columns, condition, distinct, order_by, desc, limit) # self.unlock_table(table_name) @@ -396,8 +422,8 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ else: return table.show() - - elif (operator in condition or operator1 in condition and operator3 not in condition): # e.g salary = 2000 or salary > 6000 + # OR or AND in condition + elif (operator in condition or operator1 in condition and operator3 not in condition): flag = 1 # found if (operator in condition): @@ -405,19 +431,36 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ else: splt = condition.split(operator1) - lst = [item[0] for item in splt] # condition_column list - if self.is_locked(table_name): return + #has to be changed - if self._has_index(table_name) and [item for item in lst]==self.tables[table_name].column_names[self.tables[table_name].pk_idx]: - index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] - bt = self._load_idx(index_name) - table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + if self._has_index(table_name) and 'not ' not in condition and operator2 not in condition: + print("yes") + lst = ([item.split(' ')[0] for item in splt ]) # condition_column list + print("Condition columns: ",lst) + + sum = 0 + for i in lst: + if i in self.tables['meta_indexes'].column_by_name('table_column'): + sum+=1 + if sum == len(lst): # all condition_columns in meta_indexes + print("Handle data with btree") + + index_name = self.select('*', 'meta_indexes', f'table_name={table_name} and table_column={lst[0]}', return_object=True).column_by_name('index_name') + index_name = ('').join(index_name) + bt = self._load_idx(index_name) + + if (operator in condition): # or in condition + print("index_name is: ",index_name) + table = self.tables[table_name]._select_where_or_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + else: # and in condition + table = self.tables[table_name]._select_where_and_with_btree(columns, bt, condition, distinct, order_by, desc, limit) else: - if (operator in condition): + print("No select where with btree.") + if (operator in condition): # or in condition table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - else: + else: # and in condition table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) # self.unlock_table(table_name) @@ -428,48 +471,49 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ if return_object: return table else: - return table.show() - + return table.show() else: - - if(condition[:4] == 'not '): - - print("Not has been found in condition!") + + if(condition[:4] == 'not '): # NOT in condition condition_column = split_not_condition(condition)[0] ''' print("Column is: " + condition_column) print("Operator is: " + split_not_condition(condition)[1]) print("Value is: " + split_not_condition(condition)[2] + '\n') ''' - else: - #print("Not not in condition") - #print(condition) + else: #NOT not in condition condition_column = split_condition(condition)[0] ''' print("Column is: " + condition_column) print("Operator is: " + split_condition(condition)[1]) print("Value is: " + split_condition(condition)[2] + '\n') ''' - else: # just a simple select query + else: # just a simple select * query condition_column = '' - if (flag == 0): # or has not been found + if (flag == 0): # a simple select query # self.lock_table(table_name, mode='x') if self.is_locked(table_name): return - #print(self.tables[table_name].unique) - #print(self._has_index(table_name)) - #print(condition_column in self.tables[table_name].unique) - if (self._has_index(table_name) and ((self.tables[table_name].pk_idx is not None and - condition_column==self.tables[table_name].column_names[self.tables[table_name].pk_idx]) or - condition_column in self.tables[table_name].unique)): - print("btree here") - index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + # self.tables[table_name].pk_idx is not None or + if (self._has_index(table_name) and ((condition_column == self.tables[table_name].column_names[self.tables[table_name].pk_idx]) or + condition_column in self.tables['meta_indexes'].column_by_name('table_column') and + 'not ' not in condition)): + + #print(condition_column) + #a=('').join(condition_column) + #index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] + index_name = self.select('*', 'meta_indexes', f'table_name = {table_name} and table_column = {condition_column}', return_object=True).column_by_name('index_name') + #index_name = self.tables['meta_indexes']._select_where_and('*',f'table_name={table_name} and table_column={a}', distinct, order_by, desc, limit).column_by_name('index_name') + index_name = ('').join(index_name) + print("index_name is: ",index_name) bt = self._load_idx(index_name) table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) else: + print("No select where with btree") table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) # self.unlock_table(table_name) + if save_as is not None: table._name = save_as self.table_from_object(table) From 53ba666ec4a2feef1d58b62844ee524161482dc4 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 9 Feb 2023 16:05:22 +0200 Subject: [PATCH 46/83] Handle select where with btree --- miniDB/table.py | 221 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 176 insertions(+), 45 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 9e65636c..c1f5e7fb 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -440,6 +440,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by def _select_where_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): + print("Select where with btree hereee") # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] @@ -448,6 +449,9 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False column_name, operator, value = self._parse_condition(condition) + print("column name is: ",column_name) + print("operator is: ",operator) + print("value is: ",value) #table_name = condition.split(' where')[0] #print(self.column_names[0]) @@ -459,17 +463,19 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False if column_name == self.column_names[i]: flag = True break - - if (flag is False): - print('Column is not unique. Aborting') - # if the column in condition is not a primary key, abort the select + + # if the column in condition is not a primary key or unique, abort the select + if (flag is False and self.pk_idx and column_name != self.column_names[self.pk_idx]): + print('Column is not unique or PK. Aborting') + ''' elif (self.pk_idx and column_name != self.column_names[self.pk_idx]): print('Column is not PK. Aborting') - + ''' + # here we run the same select twice, sequentially and using the btree. # we then check the results match and compare performance (number of operation) column = self.column_by_name(column_name) - + #print(column) # sequential rows1 = [] opsseq = 0 @@ -477,11 +483,19 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False opsseq+=1 if get_op(operator, x, value): rows1.append(ind) + #print("rows1 are: ", rows1) # btree find + print(bt.show()) rows = bt.find(operator, value) - print(operator) + print("rows1 are: ", rows1) + print("rows from btree are: ", rows) + ''' + print("rows are: ", rows) + print("value is: ",value) + print("operator is: ",operator) + ''' try: k = int(limit) except TypeError: @@ -506,6 +520,123 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False s_table.data = [row for row in s_table.data if row is not None][:int(limit)] return s_table + + + #------------------------------------------------------------------------------------- + + def _select_where_or_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): + + print("Select where OR with btree here!") + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + #return_cols = [self.column_names.index(colname) for colname in return_columns.split(',')] + #else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + operator = ' or ' # e.g salary = 20000 or salary > 60000 + splt = condition.split(operator) # salary = 20000, salary > 6000 + print("split is: ",splt) + if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator + rows1 = [] + rows = [] + for s in splt: + #print(s) + column_name, operator, value = self._parse_condition(s) + print(column_name) + # column_name, operator, value = self._parse_condition(condition) + # here we run the same select twice, sequentially and using the btree. + # we then check the results match and compare performance (number of operation) + column = self.column_by_name(column_name) + # sequential + + opsseq = 0 + for ind, x in enumerate(column): + opsseq+=1 + if get_op(operator, x, value): + rows1.append(ind) + + print(operator) + # btree find + # btree find + print(bt.show()) + rows.append(bt.find(operator, value)) + flatten_list = [j for sub in rows for j in sub] + + print("rows1 are: ", rows1) # table rows + print("rows are: ", flatten_list) # btree indexes + + try: + k = int(limit) + except TypeError: + k = None + + # same as select OR from now on + rows = flatten_list[:k] + # TODO: this needs to be dumbed down + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if row is not None][:int(limit)] + + return s_table + + + def _select_where_and_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): + + print("select where AND with btree here!") + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + #return_cols = [self.column_names.index(colname) for colname in return_columns.split(',')] + #else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + + operator = ' and ' + splt = condition.split(operator) + if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator + + column_name, operator, value = self._parse_condition(splt[0]) + column = self.column_by_name(column_name) + + rows = bt.find(operator, value) + + print(bt.show()) + print(rows) + + #print("rows are: ", rows) + ''' + rows1 = [] + rows = [] + for s in splt[1:]: + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + + #opsseq = 0 + + + + # btree find + rows.append(bt.find(operator, value)) + flatten_list = [j for sub in rows for j in sub] + ''' + #print("rows1 are: ", rows1) # table rows + #print("rows are: ", flatten_list) # btree indexes def order_by(self, column_name, desc=True): @@ -805,43 +936,45 @@ def _select_where_or(self, return_columns, condition=None, distinct=False, order return_cols = [i for i in range(len(self.column_names))] else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - - lst = [] - operator = ' or ' # e.g salary = 20000 or salary > 60000 - splt = condition.split(operator) # salary = 20000, salary > 6000 + + operator = ' or ' + splt = condition.split(operator) if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator + rows = [] for s in splt: - #print(s) column_name, operator, value = self._parse_condition(s) column = self.column_by_name(column_name) - rows = [ind for ind, x in enumerate(column) if get_op(operator, x, value)] - - # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - # we need to set the new column names/types and no of columns, since we might - # only return some columns - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - s_table.data = (list(set(map(lambda x: tuple(x), s_table.data)))) if distinct else s_table.data - lst.append(s_table.data) - + + #rows.append([ind for ind, x in enumerate(column) if get_op(operator, x, value)]) + for ind, x in enumerate(column): + if get_op(operator, x, value) and ind not in rows: + rows.append(ind) + + #print("rows are: ",rows) + try: + k = int(limit) + except TypeError: + k = None + + rows = rows[:k] + + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + if order_by: s_table.order_by(order_by, desc) - - if isinstance(limit,str): - s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - #print(lst) - output = [elem for twod in lst for elem in twod] - #print("output") - #print(output) - s_table.data = output - #print(s_table.data) + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if row is not None][:int(limit)] return s_table - + + def _select_where_and(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' Select and return a table containing specified columns and rows where condition is met. @@ -866,13 +999,12 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - #print(return_cols) + operator = ' and ' splt = condition.split(operator) - #print(splt) + if (len(splt)!=0): # if there are any conditions on the left and on the right side of and operator - #print(s) column_name, operator, value = self._parse_condition(splt[0]) column = self.column_by_name(column_name) @@ -904,22 +1036,23 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde if isinstance(limit,str): s_table.data = [row for row in s_table.data if any(row)][:int(limit)] - #print(s_table.column_names) if (flag): return s_table.data, dict else: return s_table - + def _select_where_and_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): # if * return all columns, else find the column indexes for the columns specified + ''' if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] ''' + data = [] operator1 = ' and ' operator2 = ' or ' @@ -934,13 +1067,11 @@ def _select_where_and_or(self, return_columns, condition=None, distinct=False, o dict = self._select_where(return_columns, splt[0], distinct, order_by, desc, limit, True)[1] for cond in splt: - if (operator1 in cond): + if (operator1 in cond): # and in condition data.append(self._select_where_and(return_columns, cond, distinct, order_by, desc, limit, True)[0]) - #dict = self._select_where_and(return_columns, cond, distinct, order_by, desc, limit, True)[1] - else: + else: data.append(self._select_where(return_columns, cond, distinct, order_by, desc, limit, True)[0]) - #dict = self._select_where(return_columns, cond, distinct, order_by, desc, limit, True)[1] - + self = Table(load=dict) #print(data) data1 = [elem for twod in data for elem in twod] # convert 3D list into a 2D list From 3d3ac04fbdd4f56ebd01ec7f6a6c7b1a90056380 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 10 Feb 2023 02:10:53 +0200 Subject: [PATCH 47/83] Remove some unnecessary print messages for table's data --- mdb.py | 50 ++++++++------------------------------------------ 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/mdb.py b/mdb.py index 9fad0c6d..554bdc1a 100644 --- a/mdb.py +++ b/mdb.py @@ -94,38 +94,20 @@ def create_query_plan(query, keywords, action): if action=='create table': args = dic['create table'][dic['create table'].index('('):dic['create table'].index(')')+1] - print("\n") - #print("\nargs") # uncomment - #print(type(args)) - #print(args[0]) -> olo mazi 1 string - #print(args) # uncomment + #print("\n") dic['create table'] = dic['create table'].removesuffix(args).strip() - #arg_list = [val.strip().split(' ') for val in args[1:-1].split(',')] - #print(arg_list) - + # 4 primary key arg_nopk = args.replace('primary key', '')[1:-1] - #print(arg_nopk) # uncomment arglist = [val.strip().split(' ') for val in arg_nopk.split(',')] - ''' - print("argslist without pk") #uncomment - print(arglist) # see the type of the arguments e.g str, int etc #uncomment - ''' + # 4 unique columns arg_nounique = args.replace('unique', '')[1:-1] - #print(arg_nounique) # uncomment arglist1 = [val.strip().split(' ') for val in arg_nounique.split(',')] - #print("argslist1 without unique") # uncomment - #print(arglist1) # uncomment - - + dic['column_names'] = ','.join([val[0] for val in arglist1]) - #print(dic['column_names']) dic['column_types'] = ','.join([val[1] for val in arglist1]) - #print(dic['column_types']) - #print("dic b4") # uncomment - #print(dic) # same - + if 'primary key' in args: #print("primary here") arglist = args[1:-1].split(' ') @@ -135,35 +117,19 @@ def create_query_plan(query, keywords, action): print("dic after") ''' dic['primary key'] = arglist[arglist.index('primary')-2] # -2 gia na vreis to onoma toy key, an phgaine -1 tha evriske ton typo toy key e.g string/ integer - #print(dic) # uncomment else: dic['primary key'] = None # handle unique columns if 'unique' in args: - #for a in arg_list: - #if (' unique ' in a and ' primary key' not in a): - #print("unique column(s) here") arglist1 = args[1:-1].split(' ') - ''' - print("arglist1 is") - print(arglist1) - print("dic after") - ''' indx_lst = [idx for idx, value in enumerate(arglist1) if value == 'unique' or value == 'unique,'] - ''' - print("indexes 4 unique") - print(indx_lst) - ''' - for n in indx_lst: - #print(n) - dic['unique'] = ','.join(arglist1[n-2] for n in indx_lst) - #arglist1[n-2] # -2 gia na vreis to onoma toy key, an phgaine -1 tha evriske ton typo toy key e.g string/ integer - #print(dic) + + dic['unique'] = ','.join(arglist1[n-2] for n in indx_lst) else: dic['unique'] = None #print("\n") - print(dic) + #print(dic) if action=='import': dic = {'import table' if key=='import' else key: val for key, val in dic.items()} From beccf8f79bfc5ef92b6511f08c68dffb52097069 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 10 Feb 2023 02:12:32 +0200 Subject: [PATCH 48/83] Fix some things for btree index in select condition --- miniDB/database.py | 161 ++++++++++----------------------------------- 1 file changed, 33 insertions(+), 128 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index ee1474aa..34b1eecc 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -358,7 +358,6 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ distinct: boolean. If True, the resulting table will contain only unique rows. ''' - # print(table_name) self.load_database() if isinstance(table_name,Table): return table_name._select_where(columns, condition, distinct, order_by, desc, limit) @@ -370,124 +369,36 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ operator = ' or ' operator1 = ' and ' - operator2 = ' not ' operator3 = ' between ' - # case: complex AND and OR conditions if(operator in condition and operator1 in condition and operator3 not in condition): + flag = 1 if self.is_locked(table_name): return - # has to be changed - #if self._has_index(table_name) and (item in self.tables['meta_indexes'].column_by_name('table_column') for item in columns_name_lst): - if self._has_index(table_name) and operator2 not in condition and 'not ' not in condition: - - columns_name_lst = [] - splt1 = condition.split(operator) # split or - for cond in splt1: - if (operator1 in cond): - l = cond.split(operator1) - for i in l: - columns_name_lst.append(i.split(' ')[0]) - else: - columns_name_lst.append(cond.split(' ')[0]) - print("Columns name list is: ",columns_name_lst) - - sum = 0 - for i in columns_name_lst: - if i in self.tables['meta_indexes'].column_by_name('table_column'): - sum+=1 - if sum == len(columns_name_lst): - print("All columns in meta indexes table.\nHandle complex AND and OR in btree.") - - #has to be changed - index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] - bt = self._load_idx(index_name) - - #has to change! - table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - else: - print("No select with btree :(") - table = self.tables[table_name]._select_where_and_or(columns, condition, distinct, order_by, desc, limit) - - # self.unlock_table(table_name) - if save_as is not None: - table._name = save_as - self.table_from_object(table) - else: - if return_object: - return table - else: - return table.show() - + table = self.tables[table_name]._select_where_and_or(columns, condition, distinct, order_by, desc, limit) + # OR or AND in condition elif (operator in condition or operator1 in condition and operator3 not in condition): flag = 1 # found - if (operator in condition): - splt = condition.split(operator) - else: - splt = condition.split(operator1) if self.is_locked(table_name): return - - #has to be changed - if self._has_index(table_name) and 'not ' not in condition and operator2 not in condition: - print("yes") - lst = ([item.split(' ')[0] for item in splt ]) # condition_column list - print("Condition columns: ",lst) - - sum = 0 - for i in lst: - if i in self.tables['meta_indexes'].column_by_name('table_column'): - sum+=1 - if sum == len(lst): # all condition_columns in meta_indexes - print("Handle data with btree") - - index_name = self.select('*', 'meta_indexes', f'table_name={table_name} and table_column={lst[0]}', return_object=True).column_by_name('index_name') - index_name = ('').join(index_name) - bt = self._load_idx(index_name) - - if (operator in condition): # or in condition - print("index_name is: ",index_name) - table = self.tables[table_name]._select_where_or_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - else: # and in condition - table = self.tables[table_name]._select_where_and_with_btree(columns, bt, condition, distinct, order_by, desc, limit) - else: - print("No select where with btree.") - if (operator in condition): # or in condition - table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - else: # and in condition - table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) - - # self.unlock_table(table_name) - if save_as is not None: - table._name = save_as - self.table_from_object(table) - else: - if return_object: - return table - else: - return table.show() + + if (operator in condition): # or in condition + table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + else: # and in condition + table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) + else: - - if(condition[:4] == 'not '): # NOT in condition - condition_column = split_not_condition(condition)[0] - ''' - print("Column is: " + condition_column) - print("Operator is: " + split_not_condition(condition)[1]) - print("Value is: " + split_not_condition(condition)[2] + '\n') - ''' - else: #NOT not in condition + if(condition[:4] == 'not '): # NOT in condition + condition_column = split_not_condition(condition)[0] + else: #NOT not in condition condition_column = split_condition(condition)[0] - ''' - print("Column is: " + condition_column) - print("Operator is: " + split_condition(condition)[1]) - print("Value is: " + split_condition(condition)[2] + '\n') - ''' + else: # just a simple select * query condition_column = '' @@ -495,33 +406,29 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ # self.lock_table(table_name, mode='x') if self.is_locked(table_name): return - # self.tables[table_name].pk_idx is not None or - if (self._has_index(table_name) and ((condition_column == self.tables[table_name].column_names[self.tables[table_name].pk_idx]) or - condition_column in self.tables['meta_indexes'].column_by_name('table_column') and + + if (self._has_index(table_name) and (condition_column in self.tables['meta_indexes'].column_by_name('table_column') and 'not ' not in condition)): - #print(condition_column) - #a=('').join(condition_column) - #index_name = self.select('*', 'meta_indexes', f'table_name={table_name}', return_object=True).column_by_name('index_name')[0] - index_name = self.select('*', 'meta_indexes', f'table_name = {table_name} and table_column = {condition_column}', return_object=True).column_by_name('index_name') - #index_name = self.tables['meta_indexes']._select_where_and('*',f'table_name={table_name} and table_column={a}', distinct, order_by, desc, limit).column_by_name('index_name') + index_name = self.select('*', 'meta_indexes', f'table_name = {table_name} and table_column = {condition_column}', return_object=True).column_by_name('index_name')[0] index_name = ('').join(index_name) - print("index_name is: ",index_name) + print("Index_name is: ",index_name) bt = self._load_idx(index_name) + table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) else: - print("No select where with btree") + #print("No select where with btree") table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) - # self.unlock_table(table_name) - - if save_as is not None: - table._name = save_as - self.table_from_object(table) + + # self.unlock_table(table_name) + if save_as is not None: + table._name = save_as + self.table_from_object(table) + else: + if return_object: + return table else: - if return_object: - return table - else: - return table.show() + return table.show() def show_table(self, table_name, no_of_rows=None): @@ -808,18 +715,16 @@ def create_index(self, index_name, table_name, column_name, index_type='btree'): ''' if (column_name != None): # look 4 unique columns - print("case: look 4 pk or 4 unique column") + #print("case: look 4 pk or 4 unique column") #print(self.tables[table_name].pk) - if (column_name != self.tables[table_name].pk): - print("case: search 4 unique column") - - if (column_name not in self.tables[table_name].unique): - raise Exception('Cannot create index. The column you specified is not unique.') - + if (column_name != self.tables[table_name].pk and column_name not in self.tables[table_name].unique): + raise Exception('Cannot create index. The column you specified is not unique or table has no primary key.') + if index_name not in self.tables['meta_indexes'].column_by_name('index_name'): # currently only btree is supported. This can be changed by adding another if. if index_type == 'btree': # case1 : index btree logging.info('Creating Btree index.') + print('Creating Btree index.') # insert a record with the name of the index and the table on which it's created to the meta_indexes table self.tables['meta_indexes']._insert([table_name, column_name , index_name]) # crate the actual index From 15cec4a95619cb7cc1ddbf3fc79ac2d7f3596da8 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 10 Feb 2023 02:13:19 +0200 Subject: [PATCH 49/83] Almost finish with select where with btree --- miniDB/table.py | 162 +++++++++--------------------------------------- 1 file changed, 31 insertions(+), 131 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index c1f5e7fb..2fbad935 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -95,11 +95,11 @@ def __init__(self, name=None, column_names=None, column_types=None, primary_key= self.unique_cols_idx = [] self.pk = primary_key - print("pk is: ",self.pk) - print("pk index: ",self.pk_idx) + #print("pk is: ",self.pk) + #print("pk index: ",self.pk_idx) self.unique = unique - print("unique cols are: ",self.unique) - print("unique cols indexes are: ",self.unique_cols_idx) + #print("unique cols are: ",self.unique) + #print("unique cols indexes are: ",self.unique_cols_idx) # self._update() # if any of the name, columns_names and column types are none. return an empty table object @@ -447,17 +447,18 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False else: return_cols = [self.column_names.index(colname) for colname in return_columns] - + + column_name, operator, value = self._parse_condition(condition) + #print("self first",self.data) + self.order_by(column_name, desc=True) + print("self first1",self.data) + #self.order_by(column_name, desc=True) + #print("\nself after",self.data) print("column name is: ",column_name) print("operator is: ",operator) print("value is: ",value) - #table_name = condition.split(' where')[0] - - #print(self.column_names[0]) - #print(self.unique_cols_idx) - #print(self.column_names[i] for i in self.unique_cols_idx) - + flag = False for i in self.unique_cols_idx: if column_name == self.column_names[i]: @@ -521,124 +522,6 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False return s_table - - #------------------------------------------------------------------------------------- - - def _select_where_or_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): - - print("Select where OR with btree here!") - - # if * return all columns, else find the column indexes for the columns specified - if return_columns == '*': - return_cols = [i for i in range(len(self.column_names))] - else: - #return_cols = [self.column_names.index(colname) for colname in return_columns.split(',')] - #else: - return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - - operator = ' or ' # e.g salary = 20000 or salary > 60000 - splt = condition.split(operator) # salary = 20000, salary > 6000 - print("split is: ",splt) - if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator - rows1 = [] - rows = [] - for s in splt: - #print(s) - column_name, operator, value = self._parse_condition(s) - print(column_name) - # column_name, operator, value = self._parse_condition(condition) - # here we run the same select twice, sequentially and using the btree. - # we then check the results match and compare performance (number of operation) - column = self.column_by_name(column_name) - # sequential - - opsseq = 0 - for ind, x in enumerate(column): - opsseq+=1 - if get_op(operator, x, value): - rows1.append(ind) - - print(operator) - # btree find - # btree find - print(bt.show()) - rows.append(bt.find(operator, value)) - flatten_list = [j for sub in rows for j in sub] - - print("rows1 are: ", rows1) # table rows - print("rows are: ", flatten_list) # btree indexes - - try: - k = int(limit) - except TypeError: - k = None - - # same as select OR from now on - rows = flatten_list[:k] - # TODO: this needs to be dumbed down - dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} - - dict['column_names'] = [self.column_names[i] for i in return_cols] - dict['column_types'] = [self.column_types[i] for i in return_cols] - - s_table = Table(load=dict) - - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data - - if order_by: - s_table.order_by(order_by, desc) - - if isinstance(limit,str): - s_table.data = [row for row in s_table.data if row is not None][:int(limit)] - - return s_table - - - def _select_where_and_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): - - print("select where AND with btree here!") - - # if * return all columns, else find the column indexes for the columns specified - if return_columns == '*': - return_cols = [i for i in range(len(self.column_names))] - else: - #return_cols = [self.column_names.index(colname) for colname in return_columns.split(',')] - #else: - return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - - - operator = ' and ' - splt = condition.split(operator) - if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator - - column_name, operator, value = self._parse_condition(splt[0]) - column = self.column_by_name(column_name) - - rows = bt.find(operator, value) - - print(bt.show()) - print(rows) - - #print("rows are: ", rows) - ''' - rows1 = [] - rows = [] - for s in splt[1:]: - column_name, operator, value = self._parse_condition(s) - column = self.column_by_name(column_name) - - #opsseq = 0 - - - - # btree find - rows.append(bt.find(operator, value)) - flatten_list = [j for sub in rows for j in sub] - ''' - #print("rows1 are: ", rows1) # table rows - #print("rows are: ", flatten_list) # btree indexes - - def order_by(self, column_name, desc=True): ''' Order table based on column. @@ -854,9 +737,26 @@ def show(self, no_of_rows=None, is_locked=False): # headers -> "column name (column type)" headers = [f'{col} ({tp.__name__})' for col, tp in zip(self.column_names, self.column_types)] - if self.pk_idx is not None: + #print(headers) + for c in range(len(self.column_names)): + if self.column_names[c] == self.pk: + headers[c] = headers[c]+' #PK#' + break + #if self.pk_idx is not None and self.pk in self.column_names: # table has a primary key, add PK next to the appropriate column - headers[self.pk_idx] = headers[self.pk_idx]+' #PK#' + #headers[self.pk_idx] = headers[self.pk_idx]+' #PK#' + + for c in range(len(self.column_names)): + if self.unique is not None and self.column_names[c] in self.unique: + headers[c] = headers[c]+' #UQ#' + ''' + if self.unique_cols_idx is not None: + # table has been declared as unique, add UQ next to the appropriate column + for c in self.unique_cols_idx: + print(self.unique_cols_idx) + print(self.unique) + headers[c] = headers[c]+' #UQ#' + ''' # detect the rows that are no tfull of nones (these rows have been deleted) # if we dont skip these rows, the returning table has empty rows at the deleted positions non_none_rows = [row for row in self.data if any(row)] From ef09672f61da26201e4d6d11b5328528fe2c307c Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Fri, 10 Feb 2023 20:22:56 +0200 Subject: [PATCH 50/83] fix some things --- miniDB/misc.py | 1 - miniDB/table.py | 2 -- sql_files/smallRelationsInsertFile.sql | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/miniDB/misc.py b/miniDB/misc.py index 4cd58918..0efd9cae 100644 --- a/miniDB/misc.py +++ b/miniDB/misc.py @@ -117,7 +117,6 @@ def not_op(op): def split_not_condition(condition): # not salary > 50000 - #condition = condition[4:] # salary > 50000 splt = condition.split(' ') #print(splt) op_key = not_op(splt[2]) diff --git a/miniDB/table.py b/miniDB/table.py index 2fbad935..bffc3f2a 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -928,7 +928,6 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde dict['column_types'] = [self.column_types[i] for i in return_cols] s_table = Table(load=dict) - s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data if order_by: @@ -945,7 +944,6 @@ def _select_where_and(self, return_columns, condition=None, distinct=False, orde def _select_where_and_or(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None): # if * return all columns, else find the column indexes for the columns specified - ''' if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] diff --git a/sql_files/smallRelationsInsertFile.sql b/sql_files/smallRelationsInsertFile.sql index 2a897d4a..b982881b 100644 --- a/sql_files/smallRelationsInsertFile.sql +++ b/sql_files/smallRelationsInsertFile.sql @@ -1,7 +1,7 @@ -create table classroom (building str unique, room_number str, capacity int unique); +create table classroom (building str unique, room_number str unique, capacity int); create table department (dept_name str primary key, building str, budget int unique); create table course (course_id str primary key, title str, dept_name str, credits int); -create table instructor (ID str primary key, name str unique, dept_name str, salary int unique); +create table instructor (ID str primary key, name str unique, dept_name str, salary int); create table section (course_id str, sec_id str, semester str, year int, building str, room_number str, time_slot_id str); create table teaches (ID str, course_id str, sec_id str, semester str, year int); create table student (ID str primary key, name str, dept_name str, tot_cred int); From d5d8cb1f3ff68a43712bb588cd9e2b759a76d3a1 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 11 Feb 2023 13:16:50 +0200 Subject: [PATCH 51/83] implement hash index --- miniDB/database.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index 34b1eecc..6623c00c 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -16,6 +16,7 @@ from misc import split_condition from misc import split_not_condition from table import Table +from hash import Hash # readline.clear_history() @@ -699,7 +700,7 @@ def _update_meta_insert_stack_for_tb(self, table_name, new_stack): # indexes - def create_index(self, index_name, table_name, column_name, index_type='btree'): + def create_index(self, index_name, table_name, column_name, index_type): ''' Creates an index on a specified table with a given name. The index is created over a primary key or over a unique column @@ -730,10 +731,21 @@ def create_index(self, index_name, table_name, column_name, index_type='btree'): # crate the actual index self._construct_index(table_name, column_name, index_name) self.save_database() + + elif index_type == 'hashing': # case2 : index hash + logging.info('Creating hash index.') + print('Creating hash index.') + # insert a record with the name of the index and the table on which it's created to the meta_indexes table + self.tables['meta_indexes']._insert([table_name, column_name, index_name]) + # crate the actual index + self._construct_hash_index(table_name, column_name, index_name) + self.save_database() + else: raise Exception('Cannot create index. Another index with the same name already exists.') else: raise Exception('Cannot create index. You have to specify the column first.') + def _construct_index(self, table_name, column_name, index_name): ''' @@ -756,6 +768,21 @@ def _construct_index(self, table_name, column_name, index_name): self._save_index( index_name, bt) + def _construct_hash_index(self, table_name, column_name, index_name): #table_column + ''' + + ''' + m=Hash() + for idx, key in enumerate(self.tables[table_name].column_by_name(column_name)): + if key is None: + continue + m.insert(key, idx) + print("\n") + #m.get_hash_index(key) + self._save_index(index_name,m) + print('M is:',m) + + def _has_index(self, table_name): ''' Check whether the specified table's primary key column is indexed. From 2f9a3687522c55efec14f86a20221fd0069a4d15 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 11 Feb 2023 13:18:03 +0200 Subject: [PATCH 52/83] fix some things --- miniDB/table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/miniDB/table.py b/miniDB/table.py index bffc3f2a..c8ef1d02 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -431,6 +431,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by if isinstance(limit,str): s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + #pd.eval(s_table) #print(s_table.data) if (flag): return s_table.data, dict From da47f0e7cba647f0a8b7f648edb8d4117c925506 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 11 Feb 2023 13:18:42 +0200 Subject: [PATCH 53/83] handle hash index --- miniDB/hash.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 miniDB/hash.py diff --git a/miniDB/hash.py b/miniDB/hash.py new file mode 100644 index 00000000..844592c7 --- /dev/null +++ b/miniDB/hash.py @@ -0,0 +1,52 @@ +import math +class Hash: + def __init__(self): + self.size = 2 + self.data=[[] for i in range(self.size)] + #print('Self data is:',self.data) + + def get_hash_index(self,key): + if type(key)==int: + h=key + elif type(key)==float: + h=math.ceil(key) + else: + h=0 + for c in key: + h+=ord(c) + print("key is: ",key) + print("h is: ",h) + print("len of self data is: ",len(self.data)) + hash=h%len(self.data) #2,4,8 + print("hash value is: ",h,"%",len(self.data),"=",hash) + hash_index=int(bin(hash)[2:]) + #LSB() + print('hash_index: ',hash_index) + #print("\n") + return hash_index + + def LSB(num, K): + return bool(num & (1 << (K - 1) )) + + def insert(self,key,value): + + hash_key=self.get_hash_index(key) + bucket=self.data[hash_key] + + found_key = False + for index, record in enumerate(bucket): + record_key, record_val = record + if record_key == key and record_val == value: + found_key = True + break + print("bucket's length is: ",len(bucket)) + if found_key: + print("key found") + bucket[index] = (key, value) + else: + print("key not found") + if len(bucket) == self.size: + print("full backet!") + bucket.append((key, value)) + + print(self.data) \ No newline at end of file From b317851a6ebc57c49aeed50fa07e4267513176ae Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 13 Feb 2023 17:42:22 +0200 Subject: [PATCH 54/83] Remove some unnecessary comments --- mdb.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mdb.py b/mdb.py index 554bdc1a..3ba99ae8 100644 --- a/mdb.py +++ b/mdb.py @@ -111,12 +111,8 @@ def create_query_plan(query, keywords, action): if 'primary key' in args: #print("primary here") arglist = args[1:-1].split(' ') - ''' - print("arglist is") - print(arglist) - print("dic after") - ''' - dic['primary key'] = arglist[arglist.index('primary')-2] # -2 gia na vreis to onoma toy key, an phgaine -1 tha evriske ton typo toy key e.g string/ integer + + dic['primary key'] = arglist[arglist.index('primary')-2] # -2 tp find key's name, -1 to find key's data type e.g string/ integer else: dic['primary key'] = None @@ -130,6 +126,7 @@ def create_query_plan(query, keywords, action): dic['unique'] = None #print("\n") #print(dic) + #print("\n") if action=='import': dic = {'import table' if key=='import' else key: val for key, val in dic.items()} From f04946386af87ac41776bb40ef4fbdc1043ce6e5 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 13 Feb 2023 17:47:40 +0200 Subject: [PATCH 55/83] Handle equivalent query plans using relational algebraic equivalence transformation rules --- miniDB/database.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 6623c00c..ad68857a 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -1,5 +1,6 @@ from __future__ import annotations import pickle +import random from time import sleep, localtime, strftime import os,sys import logging @@ -16,7 +17,7 @@ from misc import split_condition from misc import split_not_condition from table import Table -from hash import Hash +from hash2 import Hash # readline.clear_history() @@ -392,7 +393,16 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ if (operator in condition): # or in condition table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) else: # and in condition - table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) + #try to use transformation rules: + k = random.randint(0, 1) # decide on k once + print("random k is: ",k) + #splt = condition.split(operator1) + k=0 + if k == 0: # use transformation rules + #print("2 conditions combined with AND have been found!") + table = self.tables[table_name].transformation_rules(columns, condition, distinct, order_by, desc, limit) + else: # select the default path + table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) else: if(condition[:4] == 'not '): # NOT in condition From 1034d95262e83a69409f6a921da5af898a817887 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 13 Feb 2023 17:48:54 +0200 Subject: [PATCH 56/83] Create a function to handle the query transformation --- miniDB/table.py | 137 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 128 insertions(+), 9 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index c8ef1d02..23af9311 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -1,5 +1,6 @@ from __future__ import annotations import itertools +import random from tabulate import tabulate import pickle import os @@ -364,6 +365,123 @@ def _delete_where_and_or(self, condition): # uselless has to be deleted! #print(t_indexes) #return t_indexes ''' + def transformation_rules(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): + ''' + rules: + 1. σθ1 ^ σθ2 = σθ1(σθ2) + 2. σθ1(σθ2) = σθ2(σθ1) + ''' + print("Use transformation rule: σθ1^σθ2 = σθ1(σθ2)") + + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + + ''' + if ' and ' in condition: + data = [] + splt = condition.split(' and ') + dict = self._select_where(return_columns, splt[-1], distinct, order_by, desc, limit, True)[1] + print(dict) + #data = data.append(self._select_where(return_columns, splt[1], distinct, order_by, desc, limit, True)[0]) + #print("data is: \n",data) + self = Table(load=dict) + print("Self.data of inside select is: ",self.data) + #data = self.data + for i in reversed(splt): + if i == splt[-1]: + continue + else: + data.append(self._select_where(return_columns, i, distinct, order_by, desc, limit, True)[0]) + print(data) + data1 = [elem for twod in data for elem in twod] # convert 3D list into a 2D list + print(data1) + # remove duplicate records but first sort the list + data1.sort() + new_list = list(l for l, _ in itertools.groupby(data1)) + self.data = new_list # final data + + ''' + splt = condition.split(' and ') + if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator + rows = [] + rows1 = [] + + if (len(splt) == 2): + + k = random.randint(0, 1) # decide on k once + print("random k is: ",k) + k = 0 + if k == 0: #reverse + print("Use transformation rule: σθ1(σθ2)=σθ2(σθ1)") + temp = '' + temp = splt[-1] + splt[-1] = splt[0] + splt[0] = temp + + + column_name, operator, value = self._parse_condition(splt[-1]) + column = self.column_by_name(column_name) + + #rows.append([ind for ind, x in enumerate(column) if get_op(operator, x, value)]) + for ind, x in enumerate(column): + #print(ind,x) + #print(x) + if get_op(operator, x, value): + rows.append(ind) + + print("Ιnitial rows are: ",rows) + for s in reversed(splt): + if s == splt[-1]: + continue + else: + column_name, operator, value = self._parse_condition(s) + column = self.column_by_name(column_name) + + #print("column name: ",column_name) + #print("column: ",column) + + for ind, x in enumerate(column): + if ind not in rows: + continue + else: + #print(x,operator,value) + if get_op(operator, x, value): + rows1.append(ind) + print("Rows1 are: ",rows1) + rows = [c for c in rows if c in rows1] + if len(rows) == 0: # no common element + break + print("Τotal rows are: ",rows) + + # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + # we need to set the new column names/types and no of columns, since we might + # only return some columns + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if any(row)][:int(limit)] + + if (flag): + return s_table.data, dict + else: + return s_table + + #return self + #print("Self.data of outside select is: ",dataout) + #print("new self data is: ",self.data) + + def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' @@ -441,7 +559,7 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by def _select_where_with_btree(self, return_columns, bt, condition, distinct=False, order_by=None, desc=True, limit=None): - print("Select where with btree hereee") + print("Select where with btree.") # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] @@ -452,13 +570,14 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False column_name, operator, value = self._parse_condition(condition) #print("self first",self.data) - self.order_by(column_name, desc=True) - print("self first1",self.data) + self.order_by(column_name, desc=False) + #print("self first1",self.data) #self.order_by(column_name, desc=True) #print("\nself after",self.data) - print("column name is: ",column_name) - print("operator is: ",operator) - print("value is: ",value) + + #print("column name is: ",column_name) + #print("operator is: ",operator) + #print("value is: ",value) flag = False for i in self.unique_cols_idx: @@ -488,11 +607,11 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False #print("rows1 are: ", rows1) # btree find - print(bt.show()) + print("btree is: ",bt.show()) rows = bt.find(operator, value) - print("rows1 are: ", rows1) - print("rows from btree are: ", rows) + #print("rows1 are: ", rows1) + #print("rows from btree are: ", rows) ''' print("rows are: ", rows) print("value is: ",value) From fd63c4118c4838b9f7b92096e65e0a1921eb594b Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 16 Feb 2023 20:19:06 +0200 Subject: [PATCH 57/83] test some things for OR Optimizations --- mdb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mdb.py b/mdb.py index 3ba99ae8..abaa93cd 100644 --- a/mdb.py +++ b/mdb.py @@ -193,6 +193,9 @@ def interpret(query): 'export': ['export', 'to'], 'insert into': ['insert into', 'values'], 'select': ['select', 'from', 'where', 'distinct', 'order by', 'limit'], + # test for optimize or query + #'select': ['select', 'from', 'where', 'in', 'distinct', 'order by', 'limit'], + # e.g. column IN (expr1,expr2,expr3,...) 'lock table': ['lock table', 'mode'], 'unlock table': ['unlock table', 'force'], 'delete from': ['delete from', 'where'], From 82b81549a746ac21aeb5b7d43db4f1a0d4e05cd0 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 16 Feb 2023 20:21:53 +0200 Subject: [PATCH 58/83] Test some things for OR Optimization's query --- mdb.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mdb.py b/mdb.py index abaa93cd..3ba99ae8 100644 --- a/mdb.py +++ b/mdb.py @@ -193,9 +193,6 @@ def interpret(query): 'export': ['export', 'to'], 'insert into': ['insert into', 'values'], 'select': ['select', 'from', 'where', 'distinct', 'order by', 'limit'], - # test for optimize or query - #'select': ['select', 'from', 'where', 'in', 'distinct', 'order by', 'limit'], - # e.g. column IN (expr1,expr2,expr3,...) 'lock table': ['lock table', 'mode'], 'unlock table': ['unlock table', 'force'], 'delete from': ['delete from', 'where'], From 05a863d7fb913acef5c3f07f77ac43abf47e4cf1 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 16 Feb 2023 20:25:01 +0200 Subject: [PATCH 59/83] Add optimization for AND and OR operators --- miniDB/database.py | 87 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index ad68857a..ed7fa6c0 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -338,8 +338,8 @@ def delete_from(self, table_name, condition): - def select(self, columns, table_name, condition, distinct=None, order_by=None, \ - limit=True, desc=None, save_as=None, return_object=True): + def select (self, columns, table_name, condition, distinct=None, order_by=None, \ + limit=True, desc=None, save_as=None, return_object=True): ''' Selects and outputs a table's data where condtion is met. @@ -365,6 +365,7 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ return table_name._select_where(columns, condition, distinct, order_by, desc, limit) flag = 0 + statistics_OR = False if condition is not None: print("Condition is: " + condition+"\n") @@ -386,22 +387,65 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ elif (operator in condition or operator1 in condition and operator3 not in condition): flag = 1 # found - if self.is_locked(table_name): return - if (operator in condition): # or in condition - table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) - else: # and in condition - #try to use transformation rules: + if (operator in condition): # OR in condition + + #print("OR here") + ''' + test for OR optimizations + - first check if all conditions refer to the same column name + ''' + splt = condition.split(operator) + condition_column_name = splt[0].split(' ')[0] + #print("Condition column name is: ",condition_column_name) + + flag1 = False + for i in range(len(splt)): + if 'not' not in splt[i]: + if splt[i].split(' ')[0] == condition_column_name: + flag1 = True + else: + flag1 = False + break + else: # NOT in condition + if splt[i].split(' ')[1] == condition_column_name: + flag1 = True + else: + flag1 = False + break + if flag1: + print("----OR optimizations-----") + print("All conditions refer to the same column!") + + if (self._has_index(table_name) and (condition_column_name in self.tables['meta_indexes'].column_by_name('table_column') and + 'not' not in splt)): + + statistics_OR = True + expression_list = [] + + print("BTree index has been found!") + + for s in splt: + s1 = s.split(condition_column_name) + expression_list.append(s1[1]) + + tuple1 = tuple(expression_list) + #print(tuple1) + condition1 = f'{condition_column_name}' " IN " f'{tuple1}' + print("New term is: ",condition1) + else: + print("Conditions do not refer to the same column!") + table = self.tables[table_name]._select_where_or(columns, condition, distinct, order_by, desc, limit) + else: # AND in condition + # Try to use Equivalence Transformation Rules: k = random.randint(0, 1) # decide on k once - print("random k is: ",k) - #splt = condition.split(operator1) - k=0 - if k == 0: # use transformation rules - #print("2 conditions combined with AND have been found!") - table = self.tables[table_name].transformation_rules(columns, condition, distinct, order_by, desc, limit) - else: # select the default path + #print("random k is: ",k) + + if k == 0: # use Equivalence Transformation Rules + table = self.tables[table_name].equivalence_transformation_rules(columns, condition, distinct, order_by, desc, limit) + else: # choose the default path -> function select_where_and table = self.tables[table_name]._select_where_and(columns, condition, distinct, order_by, desc, limit) else: @@ -432,14 +476,15 @@ def select(self, columns, table_name, condition, distinct=None, order_by=None, \ table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) # self.unlock_table(table_name) - if save_as is not None: - table._name = save_as - self.table_from_object(table) - else: - if return_object: - return table + if (statistics_OR == False): + if save_as is not None: + table._name = save_as + self.table_from_object(table) else: - return table.show() + if return_object: + return table + else: + return table.show() def show_table(self, table_name, no_of_rows=None): From 8b9004fd60ea8eafa0ec709abbdb884c2a950431 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 16 Feb 2023 20:26:37 +0200 Subject: [PATCH 60/83] Handle Equivalence Transformation Rules --- miniDB/table.py | 67 +++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 50 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 23af9311..1910e6e1 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -365,13 +365,13 @@ def _delete_where_and_or(self, condition): # uselless has to be deleted! #print(t_indexes) #return t_indexes ''' - def transformation_rules(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): + def equivalence_transformation_rules(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' - rules: + Relational Algebraic Equivalence Transformation Rules: 1. σθ1 ^ σθ2 = σθ1(σθ2) 2. σθ1(σθ2) = σθ2(σθ1) ''' - print("Use transformation rule: σθ1^σθ2 = σθ1(σθ2)") + print("Equivalence Transformation Rule: σθ1^σθ2 = σθ1(σθ2)") # if * return all columns, else find the column indexes for the columns specified if return_columns == '*': @@ -379,31 +379,7 @@ def transformation_rules(self, return_columns, condition=None, distinct=False, o else: return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] - ''' - if ' and ' in condition: - data = [] - splt = condition.split(' and ') - dict = self._select_where(return_columns, splt[-1], distinct, order_by, desc, limit, True)[1] - print(dict) - #data = data.append(self._select_where(return_columns, splt[1], distinct, order_by, desc, limit, True)[0]) - #print("data is: \n",data) - self = Table(load=dict) - print("Self.data of inside select is: ",self.data) - #data = self.data - for i in reversed(splt): - if i == splt[-1]: - continue - else: - data.append(self._select_where(return_columns, i, distinct, order_by, desc, limit, True)[0]) - print(data) - data1 = [elem for twod in data for elem in twod] # convert 3D list into a 2D list - print(data1) - # remove duplicate records but first sort the list - data1.sort() - new_list = list(l for l, _ in itertools.groupby(data1)) - self.data = new_list # final data - - ''' + splt = condition.split(' and ') if (len(splt)!=0): # if there are any conditions on the left and on the right side of or operator rows = [] @@ -412,10 +388,10 @@ def transformation_rules(self, return_columns, condition=None, distinct=False, o if (len(splt) == 2): k = random.randint(0, 1) # decide on k once - print("random k is: ",k) - k = 0 + #print("random k is: ",k) + if k == 0: #reverse - print("Use transformation rule: σθ1(σθ2)=σθ2(σθ1)") + print("Equivalence Transformation Rule: σθ1(σθ2)=σθ2(σθ1)") temp = '' temp = splt[-1] splt[-1] = splt[0] @@ -425,36 +401,30 @@ def transformation_rules(self, return_columns, condition=None, distinct=False, o column_name, operator, value = self._parse_condition(splt[-1]) column = self.column_by_name(column_name) - #rows.append([ind for ind, x in enumerate(column) if get_op(operator, x, value)]) for ind, x in enumerate(column): - #print(ind,x) - #print(x) if get_op(operator, x, value): rows.append(ind) - print("Ιnitial rows are: ",rows) + #print("Ιnitial rows are: ",rows) for s in reversed(splt): if s == splt[-1]: continue else: column_name, operator, value = self._parse_condition(s) column = self.column_by_name(column_name) - - #print("column name: ",column_name) - #print("column: ",column) for ind, x in enumerate(column): - if ind not in rows: + if ind not in rows: # not in inner condition indexes continue else: - #print(x,operator,value) if get_op(operator, x, value): rows1.append(ind) - print("Rows1 are: ",rows1) + + #print("Rows1 are: ",rows1) rows = [c for c in rows if c in rows1] if len(rows) == 0: # no common element break - print("Τotal rows are: ",rows) + #print("Τotal rows are: ",rows) # copy the old dict, but only the rows and columns of data with index in rows/columns (the indexes that we want returned) dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} @@ -477,11 +447,6 @@ def transformation_rules(self, return_columns, condition=None, distinct=False, o else: return s_table - #return self - #print("Self.data of outside select is: ",dataout) - #print("new self data is: ",self.data) - - def _select_where(self, return_columns, condition=None, distinct=False, order_by=None, desc=True, limit=None, flag = False): ''' @@ -566,8 +531,7 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False else: return_cols = [self.column_names.index(colname) for colname in return_columns] - - + column_name, operator, value = self._parse_condition(condition) #print("self first",self.data) self.order_by(column_name, desc=False) @@ -598,13 +562,14 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False column = self.column_by_name(column_name) #print(column) # sequential + rows1 = [] opsseq = 0 for ind, x in enumerate(column): opsseq+=1 if get_op(operator, x, value): rows1.append(ind) - #print("rows1 are: ", rows1) + #print("rows1 are: ", rows1) # btree find print("btree is: ",bt.show()) @@ -612,6 +577,7 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False #print("rows1 are: ", rows1) #print("rows from btree are: ", rows) + ''' print("rows are: ", rows) print("value is: ",value) @@ -621,6 +587,7 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False k = int(limit) except TypeError: k = None + # same as simple select from now on rows = rows[:k] From 729fe5352343bd24123c7104c1a1ff33746718b9 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Thu, 16 Feb 2023 20:32:15 +0200 Subject: [PATCH 61/83] Add attribute unique in some columns --- sql_files/smallRelationsInsertFile.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql_files/smallRelationsInsertFile.sql b/sql_files/smallRelationsInsertFile.sql index b982881b..50624111 100644 --- a/sql_files/smallRelationsInsertFile.sql +++ b/sql_files/smallRelationsInsertFile.sql @@ -1,6 +1,6 @@ create table classroom (building str unique, room_number str unique, capacity int); create table department (dept_name str primary key, building str, budget int unique); -create table course (course_id str primary key, title str, dept_name str, credits int); +create table course (course_id str primary key, title str, dept_name str unique, credits int); create table instructor (ID str primary key, name str unique, dept_name str, salary int); create table section (course_id str, sec_id str, semester str, year int, building str, room_number str, time_slot_id str); create table teaches (ID str, course_id str, sec_id str, semester str, year int); From b544c69a6925f820a0cad9ff1594dd11a3a71e5a Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 13:52:39 +0200 Subject: [PATCH 62/83] Add some comments --- miniDB/database.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index ed7fa6c0..e97a5aae 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -296,8 +296,15 @@ def update_table(self, table_name, set_args, condition): condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. - + 'value[<,<=,==,>=,>]column' or + + 'column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or ...' or + + 'column between value1 and value2' . + Operatores supported: (<,<=,==,>=,>) ''' set_column, set_value = set_args.replace(' ','').split('=') @@ -319,7 +326,14 @@ def delete_from(self, table_name, condition): condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. + 'value[<,<=,==,>=,>]column' or + + 'column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or ...' or + + 'column between value1 and value2' . Operatores supported: (<,<=,==,>=,>) ''' @@ -349,8 +363,18 @@ def select (self, columns, table_name, condition, distinct=None, order_by=None, condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. + 'value[<,<=,==,>=,>]column' or + 'column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or ...' or + + 'column between value1 and value2 or not column[<,<=,==,>=,>]value or ... ' or + 'column between value1 and value2 or column[<,<=,==,>=,>]value or ... ' or + 'column[<,<=,==,>=,>]value or column between value1 and value2 or ... ' or + 'not column[<,<=,==,>=,>]value or column between value1 and value2 or ... ' . + Operators supported: (<,<=,==,>=,>) order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). desc: boolean. If True, order_by will return results in descending order (True by default). From 6cc2e5a0f1ec4824ebb328903f5d13c26b5ef180 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 13:52:59 +0200 Subject: [PATCH 63/83] Add some comments --- miniDB/table.py | 81 +++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 1910e6e1..4de9e7b8 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -55,12 +55,7 @@ def __init__(self, name=None, column_names=None, column_types=None, primary_key= self.columns = [] self.unique_cols_idx = [] - #self.unique_cols = [] - - - #self.unique_cols= [] - #print(self.unique) - #print(self.column_names) + ''' for c in self.unique: if c not in self.__dir__(): @@ -96,11 +91,8 @@ def __init__(self, name=None, column_names=None, column_types=None, primary_key= self.unique_cols_idx = [] self.pk = primary_key - #print("pk is: ",self.pk) - #print("pk index: ",self.pk_idx) self.unique = unique - #print("unique cols are: ",self.unique) - #print("unique cols indexes are: ",self.unique_cols_idx) + # self._update() # if any of the name, columns_names and column types are none. return an empty table object @@ -180,8 +172,14 @@ def _update_rows(self, set_value, set_column, condition): condition: string. A condition using the following format: 'column[<,<=,=,>=,>]value' or 'not column[<,<=,=,>=,>]value' or - 'value[<,<=,=,>=,>]column'. - + 'value[<,<=,=,>=,>]column' or + + 'column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or ...' + 'column between value1 and value2' . + Operatores supported: (<,<=,=,>=,>) ''' @@ -274,14 +272,13 @@ def _delete_where(self, condition): 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column' or - 'column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value and column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value and not column[<,<=,==,>=,>]value and ...' or + 'column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or ...' or + + 'column between value1 and value2' . - 'column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value or not column[<,<=,==,>=,>]value and ...' . - Operatores supported: (<,<=,==,>=,>) ''' @@ -291,7 +288,7 @@ def _delete_where(self, condition): indexes_to_del = [] indexes_to_del1 = [] - if (operator in condition and operator1 in condition and operator3 not in condition): + if (operator in condition and operator1 in condition and operator3 not in condition): # OR and AND in condition print("complex AND and OR found!") print(condition) splt = condition.split(operator) @@ -299,7 +296,7 @@ def _delete_where(self, condition): self._delete_where(s) #self._delete_where_and_or(condition) - elif (operator in condition and operator3 not in condition): # or in condition + elif (operator in condition and operator3 not in condition): # OR in condition splt = condition.split(operator) for s in splt: column_name, operator, value = self._parse_condition(s) @@ -308,7 +305,7 @@ def _delete_where(self, condition): if get_op(operator, row_value, value): indexes_to_del.append(index) - elif(operator1 in condition and operator3 not in condition): # and in condition + elif(operator1 in condition and operator3 not in condition): # AND in condition splt = condition.split(operator1) #indexes_to_del1 = [] column_name, operator, value = self._parse_condition(splt[0]) @@ -330,7 +327,7 @@ def _delete_where(self, condition): #print(indexes_to_del) if len(indexes_to_del) == 0: # no common element break - else: + else: # a simple delete query column_name, operator, value = self._parse_condition(condition) column = self.column_by_name(column_name) for index, row_value in enumerate(column): @@ -353,7 +350,7 @@ def _delete_where(self, condition): return indexes_to_del ''' - def _delete_where_and_or(self, condition): # uselless has to be deleted! + def _delete_where_and_or(self, condition): #t_indexes = [] #operator1 = ' and ' operator2 = ' or ' @@ -457,8 +454,18 @@ def _select_where(self, return_columns, condition=None, distinct=False, order_by condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. - + 'value[<,<=,==,>=,>]column' or + + 'column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or column[<,<=,==,>=,>]value and/or... ' or + 'not column[<,<=,==,>=,>]value and/or not column[<,<=,==,>=,>]value and/or ...' or + + 'column between value1 and value2 or not column[<,<=,==,>=,>]value or ... ' or + 'column between value1 and value2 or column[<,<=,==,>=,>]value or ... ' or + 'column[<,<=,==,>=,>]value or column between value1 and value2 or ... ' or + 'not column[<,<=,==,>=,>]value or column between value1 and value2 or ... '. + Operators supported: (<,<=,==,>=,>) distinct: boolean. If True, the resulting table will contain only unique rows (False by default). order_by: string. A column name that signals that the resulting table should be ordered based on it (no order if None). @@ -552,10 +559,6 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False # if the column in condition is not a primary key or unique, abort the select if (flag is False and self.pk_idx and column_name != self.column_names[self.pk_idx]): print('Column is not unique or PK. Aborting') - ''' - elif (self.pk_idx and column_name != self.column_names[self.pk_idx]): - print('Column is not PK. Aborting') - ''' # here we run the same select twice, sequentially and using the btree. # we then check the results match and compare performance (number of operation) @@ -578,11 +581,6 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False #print("rows1 are: ", rows1) #print("rows from btree are: ", rows) - ''' - print("rows are: ", rows) - print("value is: ",value) - print("operator is: ",operator) - ''' try: k = int(limit) except TypeError: @@ -859,7 +857,8 @@ def _parse_condition(self, condition, join=False): condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or 'not column[<,<=,==,>=,>]value' or - 'value[<,<=,==,>=,>]column'. + 'value[<,<=,==,>=,>]column' or + 'column between value1 and value2' . Operatores supported: (<,<=,==,>=,>) join: boolean. Whether to join or not (False by default). @@ -907,9 +906,13 @@ def _select_where_or(self, return_columns, condition=None, distinct=False, order Args: return_columns: list. The columns to be returned. condition: string. A condition using the following format: - 'column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value and... ' or - 'not column[<,<=,==,>=,>]value or not column[<,<=,==,>=,>]value and ...' . + 'column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value or... ' or + 'not column[<,<=,==,>=,>]value or column[<,<=,==,>=,>]value or... ' or + 'not column[<,<=,==,>=,>]value or not column[<,<=,==,>=,>]value or ... ' or + 'column between value1 and value2 or not column[<,<=,==,>=,>]value or ... ' or + 'column between value1 and value2 or column[<,<=,==,>=,>]value or ... ' or + 'column[<,<=,==,>=,>]value or column between value1 and value2 or ... ' or + 'not column[<,<=,==,>=,>]value or column between value1 and value2 or ... '. Operatores supported: (<,<=,==,>=,>) distinct: boolean. If True, the resulting table will contain only unique rows (False by default). From fd78a6347fdf33e85e90358edae0798de8de3f12 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 13:55:12 +0200 Subject: [PATCH 64/83] Add some comments --- miniDB/btree.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/miniDB/btree.py b/miniDB/btree.py index aaaab14b..de110826 100644 --- a/miniDB/btree.py +++ b/miniDB/btree.py @@ -15,7 +15,7 @@ def __init__(self, b, values=None, ptrs=None,left_sibling=None, right_sibling=No self.parent = parent # the index of a buckets parent self.is_leaf = is_leaf # a boolean value signaling whether the node is a leaf or not - + def find(self, value, return_ops=False): ''' Returns the index of the next node to search for a value if the node is not a leaf (a ptrs of the available ones). @@ -231,8 +231,6 @@ def split(self, node_id): self.split(node.parent) - - def show(self): ''' Show important info for each node (sort by level - root first, then left to right). @@ -308,8 +306,8 @@ def find(self, operator, value): # find the index of the node that the element should exist in leaf_idx, ops = self._search(value, True) - print("lead idx: ",leaf_idx) - print("ops: ", ops) + #print("leaf idx: ",leaf_idx) + #print("ops: ", ops) target_node = self.nodes[leaf_idx] if operator == '=': From 253a1f1bc0095afffa73bae17d07737cc71531f5 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 17:16:08 +0200 Subject: [PATCH 65/83] Remove some unnecessary print messages --- miniDB/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/table.py b/miniDB/table.py index 4de9e7b8..fb3e142c 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -880,7 +880,7 @@ def _parse_condition(self, condition, join=False): coltype = self.column_types[self.column_names.index(left)] if op == 'between': - print("between has been found") + #print("between has been found") return left, op, right return left, op, coltype(right) From 70887635333508bd5f71e855aee71f0e7941a7c1 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 17:17:05 +0200 Subject: [PATCH 66/83] Handle hash index --- miniDB/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index e97a5aae..dc73f243 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -17,7 +17,7 @@ from misc import split_condition from misc import split_not_condition from table import Table -from hash2 import Hash +from hash import Hash # readline.clear_history() From d29c457a30c65a15d192c75f3a5ae9bdeda1cc68 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 17:17:50 +0200 Subject: [PATCH 67/83] Handle hash --- miniDB/hash.py | 164 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 132 insertions(+), 32 deletions(-) diff --git a/miniDB/hash.py b/miniDB/hash.py index 844592c7..3bffafc6 100644 --- a/miniDB/hash.py +++ b/miniDB/hash.py @@ -1,11 +1,20 @@ import math class Hash: def __init__(self): - self.size = 2 - self.data=[[] for i in range(self.size)] - #print('Self data is:',self.data) - + self.size=2 + self.capacity=3 + self.global_depth=1 + self.dict1={} + bucket1=Bucket(bucket=[],ld=1,key='0') + bucket2=Bucket(bucket=[],ld=1,key='1') + self.data={'0':bucket1,'1':bucket2} + #self.local_depth={'0':1,'1':1} + #self.prefix=[self.data[0],self.data[1]] + #self.number_of_lsb=1 + #print(self.data) + def get_hash_index(self,key): + print('Key is:',key) if type(key)==int: h=key elif type(key)==float: @@ -14,39 +23,130 @@ def get_hash_index(self,key): h=0 for c in key: h+=ord(c) - print("key is: ",key) - print("h is: ",h) - print("len of self data is: ",len(self.data)) - hash=h%len(self.data) #2,4,8 - print("hash value is: ",h,"%",len(self.data),"=",hash) - hash_index=int(bin(hash)[2:]) - #LSB() - print('hash_index: ',hash_index) - #print("\n") - return hash_index - - def LSB(num, K): - return bool(num & (1 << (K - 1) )) + size=self.global_depth *2 + hash=h%(size) #2,4,8 + hashed=int(bin(hash)[2:]) + hash_index=str(hashed) + + if(self.global_depth>1): + if(len(hash_index)!=self.global_depth): + hash_index=hash_index.zfill(self.global_depth) + + #print("HASHED INDEX:",hash_index) + return hash_index + def insert(self,key,value): - hash_key=self.get_hash_index(key) - bucket=self.data[hash_key] - + hash_key = str(self.get_hash_index(key)) found_key = False - for index, record in enumerate(bucket): + for record in enumerate(self.data): record_key, record_val = record - if record_key == key and record_val == value: + #print("record key is: ",record_key) + #print("record_value is: ",record_val) + + if record_key == key and record_val==value: found_key = True break - print("bucket's length is: ",len(bucket)) - if found_key: - print("key found") - bucket[index] = (key, value) - else: - print("key not found") - if len(bucket) == self.size: - print("full backet!") - bucket.append((key, value)) + + #if found_key: + #print("if here") + #self.bucket[index] = (key, value) + + if not found_key: + if len(self.data[hash_key].bucket)==self.capacity: + self.split_bucket(key,hash_key,value) + print("Bucket is full!") + print("Buckets must be splitted!") + print("Record key:",key) + + + #call a split function here! + else: + print("key not found -- append") + self.data[hash_key].bucket.append((key,value)) + #self.[hash_key].lst.append(value) + for key2 in list(self.data): + if len(key2)!=self.global_depth: + del self.data[key2] + for item in self.data: + print("Key : {} , Value : {}".format(item,self.data[item].bucket)) + + + def split_bucket(self,key,hash_key,value): + list1=[] + for key1,value1 in(self.data[hash_key].bucket): + print("keY VALUE:",(key1,value1)) + list1.append((key1,value1)) + list1.append((key,value)) + + if self.global_depth==self.data[hash_key].ld: + print("List is",list1) + self.global_depth+=1 + print("global depth is:",self.global_depth) + self.directory_expansion(hash_key) + self.rehashing(list1) + elif self.data[hash_key].ld Date: Sat, 18 Feb 2023 21:09:56 +0200 Subject: [PATCH 68/83] Implement select_where_with_hash function --- miniDB/table.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index fb3e142c..4015cb33 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -541,7 +541,7 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False column_name, operator, value = self._parse_condition(condition) #print("self first",self.data) - self.order_by(column_name, desc=False) + #self.order_by(column_name, desc=True) #print("self first1",self.data) #self.order_by(column_name, desc=True) #print("\nself after",self.data) @@ -575,7 +575,7 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False #print("rows1 are: ", rows1) # btree find - print("btree is: ",bt.show()) + #print("btree is: ",bt.show()) rows = bt.find(operator, value) #print("rows1 are: ", rows1) @@ -607,6 +607,80 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False return s_table + + def _select_where_with_hash(self, return_columns, h, condition, distinct=False, order_by=None, desc=True, limit=None): + + print("Select where with hash.\n") + # if * return all columns, else find the column indexes for the columns specified + if return_columns == '*': + return_cols = [i for i in range(len(self.column_names))] + else: + return_cols = [self.column_names.index(colname) for colname in return_columns] + + + column_name, operator, value = self._parse_condition(condition) + + flag = False + for i in self.unique_cols_idx: + if column_name == self.column_names[i]: + flag = True + break + + # if the column in condition is not a primary key or unique, abort the select + if (flag is False and self.pk_idx and column_name != self.column_names[self.pk_idx]): + print('Column is not unique or PK. Aborting') + + # here we run the same select twice, sequentially and using the btree. + # we then check the results match and compare performance (number of operation) + column = self.column_by_name(column_name) + #print(column) + # sequential + + rows = [] + opsseq = 0 + for ind, x in enumerate(column): + opsseq+=1 + if get_op(operator, x, value): + rows.append(ind) + #print("rows1 are: ", rows1) + + # btree find + print(h.data) + print("Extendible Hashing: ",h.show()) + #rows = bt.find(operator, value) + + #print("rows1 are: ", rows1) + #print("rows from btree are: ", rows) + + + + try: + k = int(limit) + except TypeError: + k = None + + # same as simple select from now on + rows = rows[:k] + # TODO: this needs to be dumbed down + dict = {(key):([[self.data[i][j] for j in return_cols] for i in rows] if key=="data" else value) for key,value in self.__dict__.items()} + + dict['column_names'] = [self.column_names[i] for i in return_cols] + dict['column_types'] = [self.column_types[i] for i in return_cols] + + s_table = Table(load=dict) + + s_table.data = list(set(map(lambda x: tuple(x), s_table.data))) if distinct else s_table.data + + if order_by: + s_table.order_by(order_by, desc) + + if isinstance(limit,str): + s_table.data = [row for row in s_table.data if row is not None][:int(limit)] + + return s_table + + + def order_by(self, column_name, desc=True): ''' Order table based on column. From 71f0bf095943b286015f4a6386335d546d339014 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 21:10:43 +0200 Subject: [PATCH 69/83] Add some comments --- miniDB/hash.py | 137 ++++++++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 54 deletions(-) diff --git a/miniDB/hash.py b/miniDB/hash.py index 3bffafc6..8efe7d0a 100644 --- a/miniDB/hash.py +++ b/miniDB/hash.py @@ -1,10 +1,9 @@ import math class Hash: def __init__(self): - self.size=2 + #self.size=2 self.capacity=3 self.global_depth=1 - self.dict1={} bucket1=Bucket(bucket=[],ld=1,key='0') bucket2=Bucket(bucket=[],ld=1,key='1') self.data={'0':bucket1,'1':bucket2} @@ -14,20 +13,19 @@ def __init__(self): #print(self.data) def get_hash_index(self,key): - print('Key is:',key) + print('Key is: ',key) if type(key)==int: - h=key + h=key elif type(key)==float: h=math.ceil(key) - else: - h=0 - for c in key: - h+=ord(c) - size=self.global_depth *2 + else: # type string + h=0 + for c in key: + h+=ord(c) # ascii number + size=2**(self.global_depth) hash=h%(size) #2,4,8 hashed=int(bin(hash)[2:]) hash_index=str(hashed) - if(self.global_depth>1): if(len(hash_index)!=self.global_depth): hash_index=hash_index.zfill(self.global_depth) @@ -37,9 +35,9 @@ def get_hash_index(self,key): return hash_index def insert(self,key,value): - hash_key = str(self.get_hash_index(key)) found_key = False + for record in enumerate(self.data): record_key, record_val = record #print("record key is: ",record_key) @@ -47,84 +45,110 @@ def insert(self,key,value): if record_key == key and record_val==value: found_key = True - break - + break #if found_key: #print("if here") #self.bucket[index] = (key, value) if not found_key: + print("HASH KEY:",hash_key) + #print("Bucket's Length is: ",len(self.data[hash_key].bucket)) + print("Bucket is: ",self.data[hash_key].bucket) + if len(self.data[hash_key].bucket)==self.capacity: - self.split_bucket(key,hash_key,value) print("Bucket is full!") print("Buckets must be splitted!") - print("Record key:",key) - - - #call a split function here! - else: - print("key not found -- append") + print("Record key that caused split is:",key) + + self.split_bucket(key,hash_key,value) # call split function + + else: # no split -> add tuple to bucket + print("Value is not in the dictionary -- ADD NEW VALUE!") self.data[hash_key].bucket.append((key,value)) - #self.[hash_key].lst.append(value) - for key2 in list(self.data): - if len(key2)!=self.global_depth: - del self.data[key2] + + ''' for item in self.data: print("Key : {} , Value : {}".format(item,self.data[item].bucket)) - + ''' def split_bucket(self,key,hash_key,value): list1=[] - for key1,value1 in(self.data[hash_key].bucket): - print("keY VALUE:",(key1,value1)) + for key1,value1 in(self.data[hash_key].bucket): + #print("keY VALUE:",(key1,value1)) list1.append((key1,value1)) - list1.append((key,value)) + + if((key,value)) not in list1: + list1.append((key,value)) - if self.global_depth==self.data[hash_key].ld: - print("List is",list1) + if self.global_depth==self.data[hash_key].ld: # global depth = local depth + #print("List is",list1) self.global_depth+=1 - print("global depth is:",self.global_depth) + #print("global depth is:",self.global_depth) self.directory_expansion(hash_key) self.rehashing(list1) + elif self.data[hash_key].ld Date: Sat, 18 Feb 2023 21:11:30 +0200 Subject: [PATCH 70/83] Handle hash index in select --- miniDB/database.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index dc73f243..f4e95f4d 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -57,7 +57,7 @@ def __init__(self, name, load=True, verbose = True): self.create_table('meta_length', 'table_name,no_of_rows', 'str,int') self.create_table('meta_locks', 'table_name,pid,mode', 'str,int,str') self.create_table('meta_insert_stack', 'table_name,indexes', 'str,list') - self.create_table('meta_indexes', 'table_name,table_column,index_name', 'str,str,str') + self.create_table('meta_indexes', 'table_name,table_column,index_name,index_type', 'str,str,str,str') self.save_database() def save_database(self): @@ -491,10 +491,20 @@ def select (self, columns, table_name, condition, distinct=None, order_by=None, index_name = self.select('*', 'meta_indexes', f'table_name = {table_name} and table_column = {condition_column}', return_object=True).column_by_name('index_name')[0] index_name = ('').join(index_name) + + index_type = self.select('*', 'meta_indexes', f'table_name = {table_name} and table_column = {condition_column} and index_name = {index_name}', return_object=True).column_by_name('index_type') + index_type = ('').join(index_type) + print("Index_name is: ",index_name) - bt = self._load_idx(index_name) + print("Index_type is: ",index_type) - table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + if index_type == 'btree': + bt = self._load_idx(index_name) + table = self.tables[table_name]._select_where_with_btree(columns, bt, condition, distinct, order_by, desc, limit) + else: # Extendible hashing + h = self._load_idx(index_name) + table = self.tables[table_name]._select_where_with_hash(columns, h, condition, distinct, order_by, desc, limit) + else: #print("No select where with btree") table = self.tables[table_name]._select_where(columns, condition, distinct, order_by, desc, limit) @@ -806,7 +816,7 @@ def create_index(self, index_name, table_name, column_name, index_type): logging.info('Creating Btree index.') print('Creating Btree index.') # insert a record with the name of the index and the table on which it's created to the meta_indexes table - self.tables['meta_indexes']._insert([table_name, column_name , index_name]) + self.tables['meta_indexes']._insert([table_name, column_name , index_name, index_type]) # crate the actual index self._construct_index(table_name, column_name, index_name) self.save_database() @@ -815,7 +825,7 @@ def create_index(self, index_name, table_name, column_name, index_type): logging.info('Creating hash index.') print('Creating hash index.') # insert a record with the name of the index and the table on which it's created to the meta_indexes table - self.tables['meta_indexes']._insert([table_name, column_name, index_name]) + self.tables['meta_indexes']._insert([table_name, column_name, index_name, index_type]) # crate the actual index self._construct_hash_index(table_name, column_name, index_name) self.save_database() @@ -847,9 +857,14 @@ def _construct_index(self, table_name, column_name, index_name): self._save_index( index_name, bt) - def _construct_hash_index(self, table_name, column_name, index_name): #table_column + def _construct_hash_index(self, table_name, column_name, index_name): ''' - + Construct extendible hashing on a table and save. + + Args: + table_name: string. Table name (must be part of database). + column_name: string. Name of the table's column where the index is created over (must be part of database). + index_name: string. Name of the created index. ''' m=Hash() for idx, key in enumerate(self.tables[table_name].column_by_name(column_name)): @@ -857,9 +872,8 @@ def _construct_hash_index(self, table_name, column_name, index_name): #table_col continue m.insert(key, idx) print("\n") - #m.get_hash_index(key) self._save_index(index_name,m) - print('M is:',m) + #print('M is:',m.show()) # object address def _has_index(self, table_name): From 0950d30bfab624e6a04b718d6589f68ba686da09 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 22:46:55 +0200 Subject: [PATCH 71/83] Fix some things --- miniDB/database.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index f4e95f4d..ecc41110 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -815,7 +815,7 @@ def create_index(self, index_name, table_name, column_name, index_type): if index_type == 'btree': # case1 : index btree logging.info('Creating Btree index.') print('Creating Btree index.') - # insert a record with the name of the index and the table on which it's created to the meta_indexes table + # insert a record with the name of the index, the index type, the table and the table's column on which it's created to the meta_indexes table self.tables['meta_indexes']._insert([table_name, column_name , index_name, index_type]) # crate the actual index self._construct_index(table_name, column_name, index_name) @@ -824,7 +824,7 @@ def create_index(self, index_name, table_name, column_name, index_type): elif index_type == 'hashing': # case2 : index hash logging.info('Creating hash index.') print('Creating hash index.') - # insert a record with the name of the index and the table on which it's created to the meta_indexes table + # insert a record with the name of the index, the index type, the table and the table's column on which it's created to the meta_indexes table self.tables['meta_indexes']._insert([table_name, column_name, index_name, index_type]) # crate the actual index self._construct_hash_index(table_name, column_name, index_name) @@ -866,14 +866,14 @@ def _construct_hash_index(self, table_name, column_name, index_name): column_name: string. Name of the table's column where the index is created over (must be part of database). index_name: string. Name of the created index. ''' - m=Hash() + h=Hash() for idx, key in enumerate(self.tables[table_name].column_by_name(column_name)): if key is None: continue - m.insert(key, idx) + h.insert(key, idx) print("\n") - self._save_index(index_name,m) - #print('M is:',m.show()) # object address + self._save_index(index_name,h) + #print('H is:',h.show()) # object's address def _has_index(self, table_name): From 418a2853cf4a89b5686169a041bd13de8c54c653 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 22:47:57 +0200 Subject: [PATCH 72/83] Create find function in hash index --- miniDB/hash.py | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/miniDB/hash.py b/miniDB/hash.py index 8efe7d0a..f269adee 100644 --- a/miniDB/hash.py +++ b/miniDB/hash.py @@ -11,6 +11,33 @@ def __init__(self): #self.prefix=[self.data[0],self.data[1]] #self.number_of_lsb=1 #print(self.data) + + + def find(self,operator,value): + + rows = [] + for key in self.data: # in each key + for k,ind in self.data[key].bucket: # in each bucket + if operator == '=': + if k == value: + rows.append(ind) + elif operator == '>': + if k > value: + rows.append(ind) + elif operator == '>=': + if k >= value: + rows.append(ind) + elif operator == '<': + if k < value: + rows.append(ind) + elif operator == '<=': + if k <= value: + rows.append(ind) + + #remove duplicates first + rows = list(dict.fromkeys(rows)) + return rows + def get_hash_index(self,key): print('Key is: ',key) @@ -135,21 +162,14 @@ def rehashing(self,list1): def show(self): ''' - Print the bucket's value and relevant information. + Print the whole dictionary (keys and values). ''' - print("Show function here!") - print(self.data) + for item in self.data: print("Key : {} , Value : {}".format(item,self.data[item].bucket)) #print("LD ISS:",self.data[item].ld) - ''' - for key in self.data: - self.data[key].bucket.show() - ''' - - - + ''' def directory_expansion(self): @@ -169,13 +189,6 @@ def __init__(self,bucket,ld,key): self.ld=ld self.key=key - def show(self): - ''' - Print the buckets's tuples. - ''' - for item in self.bucket: - print(item) - #print('Values', self.bucket) \ No newline at end of file From 396d081871eec82344d7474b37a2977b49caaace Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 22:48:59 +0200 Subject: [PATCH 73/83] Fix some things --- miniDB/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index ecc41110..c890fbe3 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -817,7 +817,7 @@ def create_index(self, index_name, table_name, column_name, index_type): print('Creating Btree index.') # insert a record with the name of the index, the index type, the table and the table's column on which it's created to the meta_indexes table self.tables['meta_indexes']._insert([table_name, column_name , index_name, index_type]) - # crate the actual index + # create the actual index self._construct_index(table_name, column_name, index_name) self.save_database() From 3b6001016a8cc83887a6a7a08140e590b6078d3d Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 22:49:50 +0200 Subject: [PATCH 74/83] Complete select_where_with_hash --- miniDB/table.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 4015cb33..9307a0e9 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -572,7 +572,6 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False opsseq+=1 if get_op(operator, x, value): rows1.append(ind) - #print("rows1 are: ", rows1) # btree find #print("btree is: ",bt.show()) @@ -630,30 +629,27 @@ def _select_where_with_hash(self, return_columns, h, condition, distinct=False, if (flag is False and self.pk_idx and column_name != self.column_names[self.pk_idx]): print('Column is not unique or PK. Aborting') - # here we run the same select twice, sequentially and using the btree. + # here we run the same select twice, sequentially and using the hash. # we then check the results match and compare performance (number of operation) column = self.column_by_name(column_name) #print(column) # sequential - rows = [] + rows1 = [] opsseq = 0 for ind, x in enumerate(column): opsseq+=1 if get_op(operator, x, value): - rows.append(ind) - #print("rows1 are: ", rows1) - - # btree find - print(h.data) - print("Extendible Hashing: ",h.show()) - #rows = bt.find(operator, value) - - #print("rows1 are: ", rows1) - #print("rows from btree are: ", rows) - - - + rows1.append(ind) + #print("rows1: ",rows1) + + # hash find + print("\nExtendible hashing:") + h.show() + print("\n") + rows = h.find(operator,value) + #print("rows: ",rows) + try: k = int(limit) except TypeError: From e881ec5ba1fd369359652b5a5a92fbd51263cd7d Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sat, 18 Feb 2023 22:50:41 +0200 Subject: [PATCH 75/83] Make some columns unique --- sql_files/smallRelationsInsertFile.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql_files/smallRelationsInsertFile.sql b/sql_files/smallRelationsInsertFile.sql index 50624111..df0d4009 100644 --- a/sql_files/smallRelationsInsertFile.sql +++ b/sql_files/smallRelationsInsertFile.sql @@ -1,6 +1,6 @@ create table classroom (building str unique, room_number str unique, capacity int); create table department (dept_name str primary key, building str, budget int unique); -create table course (course_id str primary key, title str, dept_name str unique, credits int); +create table course (course_id str primary key, title str unique, dept_name str unique, credits int); create table instructor (ID str primary key, name str unique, dept_name str, salary int); create table section (course_id str, sec_id str, semester str, year int, building str, room_number str, time_slot_id str); create table teaches (ID str, course_id str, sec_id str, semester str, year int); From 5ec293c27ba7870141aca93b7dc9890468bdd63a Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 19 Feb 2023 14:20:44 +0200 Subject: [PATCH 76/83] Make some columns unique --- miniDB/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index c890fbe3..86d8f8c2 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -572,7 +572,6 @@ def join(self, mode, left_table, right_table, condition, save_as=None, return_ob right_table: string. Name of the right table (must be in DB) or Table obj. condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -866,6 +865,7 @@ def _construct_hash_index(self, table_name, column_name, index_name): column_name: string. Name of the table's column where the index is created over (must be part of database). index_name: string. Name of the created index. ''' + h=Hash() for idx, key in enumerate(self.tables[table_name].column_by_name(column_name)): if key is None: From fbdd3159b953a42b482d1b9eb2d171b22d3bffc5 Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 19 Feb 2023 14:22:05 +0200 Subject: [PATCH 77/83] Remove some unnecessary comments --- miniDB/database.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/miniDB/database.py b/miniDB/database.py index 86d8f8c2..e3e7a51a 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -873,8 +873,7 @@ def _construct_hash_index(self, table_name, column_name, index_name): h.insert(key, idx) print("\n") self._save_index(index_name,h) - #print('H is:',h.show()) # object's address - + def _has_index(self, table_name): ''' From 370c4a160c3387990c4136828f3ecb758c997fca Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 19 Feb 2023 14:23:39 +0200 Subject: [PATCH 78/83] Make some columns unique --- sql_files/smallRelationsInsertFile.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sql_files/smallRelationsInsertFile.sql b/sql_files/smallRelationsInsertFile.sql index df0d4009..34e45393 100644 --- a/sql_files/smallRelationsInsertFile.sql +++ b/sql_files/smallRelationsInsertFile.sql @@ -1,10 +1,10 @@ create table classroom (building str unique, room_number str unique, capacity int); -create table department (dept_name str primary key, building str, budget int unique); +create table department (dept_name str primary key, building str, budget int); create table course (course_id str primary key, title str unique, dept_name str unique, credits int); create table instructor (ID str primary key, name str unique, dept_name str, salary int); -create table section (course_id str, sec_id str, semester str, year int, building str, room_number str, time_slot_id str); +create table section (course_id str, sec_id str, semester str unique, year int, building str, room_number str, time_slot_id str); create table teaches (ID str, course_id str, sec_id str, semester str, year int); -create table student (ID str primary key, name str, dept_name str, tot_cred int); +create table student (ID str primary key, name str, dept_name str unique, tot_cred int); create table takes (ID str, course_id str, sec_id str, semester str, year int, grade str); create table advisor (s_ID str primary key, i_ID str); create table time_slot (time_slot_id str, day str, start_hr int, start_min int, end_hr str, end_min str); From 1844e9cd3aba9bae9c703079ce0326bfbd28a8dc Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Sun, 19 Feb 2023 14:24:08 +0200 Subject: [PATCH 79/83] Remove some unnecessary comments --- miniDB/table.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index 9307a0e9..d3fe2a08 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -699,7 +699,6 @@ def _general_join_processing(self, table_right:Table, condition, join_type): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -744,7 +743,6 @@ def _inner_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -775,7 +773,6 @@ def _left_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -807,7 +804,6 @@ def _right_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) @@ -839,7 +835,6 @@ def _full_join(self, table_right: Table, condition): Args: condition: string. A condition using the following format: 'column[<,<=,==,>=,>]value' or - 'not column[<,<=,==,>=,>]value' or 'value[<,<=,==,>=,>]column'. Operators supported: (<,<=,==,>=,>) From 9c2eccb1136ac8cee6674697e04813dbdfa90c8d Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 20 Feb 2023 18:59:38 +0200 Subject: [PATCH 80/83] Fixed some things --- miniDB/database.py | 1 - 1 file changed, 1 deletion(-) diff --git a/miniDB/database.py b/miniDB/database.py index e3e7a51a..e9c5e6df 100644 --- a/miniDB/database.py +++ b/miniDB/database.py @@ -871,7 +871,6 @@ def _construct_hash_index(self, table_name, column_name, index_name): if key is None: continue h.insert(key, idx) - print("\n") self._save_index(index_name,h) From 952fceaba7172cd0f96c882ec7e147b430a5ac3c Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 20 Feb 2023 19:01:16 +0200 Subject: [PATCH 81/83] Finish extendible hashing --- miniDB/hash.py | 222 ++++++++++++++++++++++--------------------------- 1 file changed, 100 insertions(+), 122 deletions(-) diff --git a/miniDB/hash.py b/miniDB/hash.py index f269adee..5ac01f14 100644 --- a/miniDB/hash.py +++ b/miniDB/hash.py @@ -1,46 +1,19 @@ import math class Hash: def __init__(self): - #self.size=2 - self.capacity=3 - self.global_depth=1 - bucket1=Bucket(bucket=[],ld=1,key='0') - bucket2=Bucket(bucket=[],ld=1,key='1') + self.capacity=3 #bucket capacity + self.global_depth=1 + bucket1=Bucket(bucket=[],ld=1) + bucket2=Bucket(bucket=[],ld=1) self.data={'0':bucket1,'1':bucket2} - #self.local_depth={'0':1,'1':1} - #self.prefix=[self.data[0],self.data[1]] - #self.number_of_lsb=1 - #print(self.data) - - - def find(self,operator,value): - - rows = [] - for key in self.data: # in each key - for k,ind in self.data[key].bucket: # in each bucket - if operator == '=': - if k == value: - rows.append(ind) - elif operator == '>': - if k > value: - rows.append(ind) - elif operator == '>=': - if k >= value: - rows.append(ind) - elif operator == '<': - if k < value: - rows.append(ind) - elif operator == '<=': - if k <= value: - rows.append(ind) - - #remove duplicates first - rows = list(dict.fromkeys(rows)) - return rows - + def get_hash_index(self,key): - print('Key is: ',key) + ''' + Hash function.Returns the hashed value. + + key:The key used for placing the tuple in the correct bucket according to a hash function. + ''' if type(key)==int: h=key elif type(key)==float: @@ -49,145 +22,150 @@ def get_hash_index(self,key): h=0 for c in key: h+=ord(c) # ascii number - size=2**(self.global_depth) - hash=h%(size) #2,4,8 + size=2**(self.global_depth) + hash=h%(size) #returns number of lsb hashed=int(bin(hash)[2:]) hash_index=str(hashed) - if(self.global_depth>1): + if(self.global_depth>1): if(len(hash_index)!=self.global_depth): - hash_index=hash_index.zfill(self.global_depth) - - - #print("HASHED INDEX:",hash_index) + hash_index=hash_index.zfill(self.global_depth) #fills with zeros return hash_index def insert(self,key,value): + ''' + Insert the key and its value(pointer) to the appropriate bucket. + Args: + key:The key used for placing the tuple in the correct bucket. + value: int. The ptr of the inserted value (e.g. its index). + ''' hash_key = str(self.get_hash_index(key)) found_key = False for record in enumerate(self.data): record_key, record_val = record - #print("record key is: ",record_key) - #print("record_value is: ",record_val) - if record_key == key and record_val==value: found_key = True break - #if found_key: - #print("if here") - #self.bucket[index] = (key, value) - if not found_key: - print("HASH KEY:",hash_key) - #print("Bucket's Length is: ",len(self.data[hash_key].bucket)) - print("Bucket is: ",self.data[hash_key].bucket) - - if len(self.data[hash_key].bucket)==self.capacity: - print("Bucket is full!") - print("Buckets must be splitted!") - print("Record key that caused split is:",key) - - self.split_bucket(key,hash_key,value) # call split function - + if len(self.data[hash_key].bucket)==self.capacity: #full bucket + self.split_bucket(key,hash_key,value) # call split function else: # no split -> add tuple to bucket - print("Value is not in the dictionary -- ADD NEW VALUE!") self.data[hash_key].bucket.append((key,value)) - - ''' - for item in self.data: - print("Key : {} , Value : {}".format(item,self.data[item].bucket)) - ''' - + + def split_bucket(self,key,hash_key,value): + ''' + Checks the global depth in relation to the local depth.If the global depth is equal to the local depth + then directory expansion,rehashing of the bucket and increment by one of the local and global depth occur.If the + local depth is less than the global depth then only rehashing of the bucket and increment by of the local depth occur. + + Args: + key:The key used for placing the tuple in the correct bucket. + hash_key:str.The hashed key where the bucket overflow occurs. + value: int. The ptr of the inserted value (e.g. its index). + + ''' list1=[] for key1,value1 in(self.data[hash_key].bucket): - #print("keY VALUE:",(key1,value1)) list1.append((key1,value1)) if((key,value)) not in list1: - list1.append((key,value)) - + list1.append((key,value)) if self.global_depth==self.data[hash_key].ld: # global depth = local depth - #print("List is",list1) self.global_depth+=1 - #print("global depth is:",self.global_depth) self.directory_expansion(hash_key) self.rehashing(list1) - - elif self.data[hash_key].ld': + if k > value: + rows.append(ind) + elif operator == '>=': + if k >= value: + rows.append(ind) + elif operator == '<': + if k < value: + rows.append(ind) + elif operator == '<=': + if k <= value: + rows.append(ind) + #remove duplicates first + rows = list(dict.fromkeys(rows)) + return rows def show(self): ''' Print the whole dictionary (keys and values). ''' - for item in self.data: print("Key : {} , Value : {}".format(item,self.data[item].bucket)) - #print("LD ISS:",self.data[item].ld) - - -''' - def directory_expansion(self): - - self.size=2*self.size - print("HASHHH:",hash_key) - h1=hash_key - k1='0'+h1 - h2='1'+h1 - - self.global_depth+=1 -''' - + class Bucket: - - def __init__(self,bucket,ld,key): + ''' + The bucket abstraction. + ''' + def __init__(self,bucket,ld): self.bucket= [] if bucket is None else bucket self.ld=ld - self.key=key From a597ca35442d4186aad797fe0eea056bd3032fdf Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 20 Feb 2023 19:01:50 +0200 Subject: [PATCH 82/83] Fixed some things --- miniDB/table.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/miniDB/table.py b/miniDB/table.py index d3fe2a08..c497f29b 100644 --- a/miniDB/table.py +++ b/miniDB/table.py @@ -536,7 +536,8 @@ def _select_where_with_btree(self, return_columns, bt, condition, distinct=False if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] else: - return_cols = [self.column_names.index(colname) for colname in return_columns] + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + column_name, operator, value = self._parse_condition(condition) @@ -614,9 +615,9 @@ def _select_where_with_hash(self, return_columns, h, condition, distinct=False, if return_columns == '*': return_cols = [i for i in range(len(self.column_names))] else: - return_cols = [self.column_names.index(colname) for colname in return_columns] + return_cols = [self.column_names.index(col.strip()) for col in return_columns.split(',')] + - column_name, operator, value = self._parse_condition(condition) flag = False From 7fbbd77eef3dd1792caac538b7f36a82d57fea9e Mon Sep 17 00:00:00 2001 From: Helen Polychroni Date: Mon, 20 Feb 2023 19:05:57 +0200 Subject: [PATCH 83/83] Finish extendible hashing --- miniDB/hash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniDB/hash.py b/miniDB/hash.py index 5ac01f14..e10c3a16 100644 --- a/miniDB/hash.py +++ b/miniDB/hash.py @@ -57,7 +57,7 @@ def split_bucket(self,key,hash_key,value): ''' Checks the global depth in relation to the local depth.If the global depth is equal to the local depth then directory expansion,rehashing of the bucket and increment by one of the local and global depth occur.If the - local depth is less than the global depth then only rehashing of the bucket and increment by of the local depth occur. + local depth is less than the global depth then only rehashing of the bucket and increment by one of the local depth occur. Args: key:The key used for placing the tuple in the correct bucket.