Exemple #1
0
 def _join(self, kw):
     join = ''
     if not 'join' in kw:
         return join
     for join_table, condition in kw['join'].items():
         if not join_table in self.database.tables:
             error = f"{join_table} does not exist in database"
             raise InvalidInputError(
                 error,
                 f"valid tables are {list(t for t in self.database.tables)}"
             )
         count = 0
         for col1, col2 in condition.items():
             for col in [col1, col2]:
                 if not '.' in col:
                     usage = "join usage: join={'table1': {'table1.col': 'this.col'} } or  join={'table1': {'this.col': 'table1.col'} }"
                     raise InvalidInputError(
                         f"column {col} missing expected '.'", usage)
                 table, column = col.split('.')
                 if not table in self.database.tables:
                     error = f"table {table} does not exist in database"
                     raise InvalidInputError(
                         error,
                         f"valid tables {list(t for t in self.database.tables)}"
                     )
                 if not column in self.database.tables[table].columns:
                     error = f"column {column} is not a valid column in table {table}"
                     raise InvalidColumnType(
                         error,
                         f"valid column types {self.database.tables[table].columns}"
                     )
             join_and = ' AND ' if count > 0 else f'JOIN {join_table} ON'
             join = f'{join}{join_and} {col1} = {col2} '
             count += 1
     return join
Exemple #2
0
    def __init__(self,
                 name: str,
                 database,
                 columns: list,
                 prim_key: str,
                 foreign_keys: dict = None,
                 cache_enabled: Optional[bool] = False,
                 max_cache_len: Optional[int] = 125):
        self.name = name
        self.database = database
        self.log = self.database.log
        self.types = {int, str, float, bool, bytes}

        self.cache_enabled = cache_enabled
        self.max_cache_len = max_cache_len
        self.cache = None
        if self.cache_enabled:
            self.enable_cache()

        self.columns = {}
        for c in columns:
            if not c.type in self.types:
                raise InvalidColumnType(
                    f"input type: {type(c.type)} of {c}",
                    f"invalid type provided for column, supported types {self.types}"
                )
            if c.name in self.columns:
                raise InvalidInputError(
                    f"duplicate column name {c.name} provided",
                    f"column names may only be specified once for table objects"
                )
            self.columns[c.name] = c
        if prim_key is not None:
            self.prim_key = prim_key if prim_key in self.columns else None
        self.foreign_keys = foreign_keys
Exemple #3
0
 def _process_input(self, kw):
     tables = self.get_tables_from_input(kw)
     kw = self.database._validate_where_input(tables, kw)
     if 'where' in kw:
         if not type(kw['where']) in {list, dict}:
             raise InvalidInputError(
                 kw['where'],
                 f"{kw['where']} is type {type(kw['where'])} - use where={{'col': <value>}} or where=[['col', <operator>, <value>]]"
             )
         kw['where'] = self.database._validate_where_input(
             tables, kw['where'])
     return kw
Exemple #4
0
 def _validate_table_column(self, col_name, no_raise=False):
     dot_table = False
     if isinstance(col_name, str) and '.' in col_name:
         dot_table = True
         table, column = col_name.split('.')
     else:
         table, column = self.name, col_name
     if (not column in self.database.tables[table].columns
             and not (no_raise and not dot_table)):
         raise InvalidInputError(
             f"{column} is not a valid column in table {table}",
             "invalid column specified for 'where'")
     return table, column
Exemple #5
0
    async def insert(self, **kw):
        """
        Usage:
            db.tables['stocks_new_tb2'].insert(
                date='2006-01-05',
                trans={
                    'type': 'BUY', 
                    'conditions': {'limit': '36.00', 'time': 'EndOfTradingDay'}, #JSON
                'tradeTimes':['16:30:00.00','16:30:01.00']}, # JSON
                symbol='RHAT', 
                qty=100.0,
                price=35.14)
        """
        cols = '('
        vals = '('
        #checking input kw's for correct value types
        add_to_cache = False

        # copy insertion before modification / validation
        insert_values = {}
        insert_values.update(kw)

        kw = self._process_input(kw)

        if len(kw) == len(self.columns):
            add_to_cache = True
        for col_name, col in self.columns.items():
            if not col_name in kw:
                if not col.mods == None:
                    if 'NOT NULL' in col.mods and not 'INCREMENT' in col.mods:
                        raise InvalidInputError(
                            f'{col_name} is a required field for INSERT in table {self.name}',
                            "correct and try again")
                continue

            if len(cols) > 2:
                cols = f'{cols}, '
                vals = f'{vals}, '
            cols = f'{cols}{col_name}'
            #json handling
            if kw[col_name] == 'NULL' or kw[
                    col_name] == None or col.type == str and '{"' and '}' in kw[
                        col_name]:
                new_val = kw[col_name]
            else:
                #new_val = kw[col_name] if col.type is not str else f'"{kw[col_name]}"'
                new_val = kw[
                    col_name] if not col.type is str else f"'{kw[col_name]}'"
            vals = f'{vals}{new_val}'

        cols = cols + ')'
        vals = vals + ')'

        query = f'INSERT INTO {self.name} {cols} VALUES {vals}'
        #self.log.debug(query)
        try:
            result = await self.database.run(query)
            if add_to_cache and self.cache_enabled:
                self.log.debug("## cache add - from insertion ##")
                self.cache[kw[self.prim_key]] = insert_values

        except Exception as e:
            self.log.exception(f"exception inserting into {self.name}")
            raise e
Exemple #6
0
    async def select(self, selection, *args, **kw):
        """
        Usage: returns list of dictionaries for each selection in each row. 
            tb = db.tables['stocks_new_tb2']

            sel = tb.select('order_num',
                            'symbol', 
                            where={'trans': 'BUY', 'qty': 100})
            sel = tb.select('*')
            # Iterate through table
            sel = [row for row in tb]
            # Using Primary key only
            sel = tb[0] # select * from <table> where <table_prim_key> = <val>
        """
        col_select = [selection] + list(args) if not isinstance(
            selection, list) else selection
        col_select = [i for i in col_select]

        if 'join' in kw and isinstance(kw['join'], str):
            if kw['join'] in [
                    self.foreign_keys[k]['table'] for k in self.foreign_keys
            ]:
                for local_key, foreign_key in self.foreign_keys.items():
                    if foreign_key['table'] == kw['join']:
                        kw['join'] = {
                            foreign_key['table']: {
                                f"{self.name}.{local_key}":
                                f"{foreign_key['table']}.{foreign_key['ref']}"
                            }
                        }
            else:
                error = f"join table {kw['join']} specified without specifying matching columns or tables do not share keys"
                raise InvalidInputError(
                    error, f"valid foreign_keys {self.foreign_keys}")
        cache_new_rows = True
        if '*' in selection:
            selection = '*'
            if 'join' in kw:
                if isinstance(kw['join'], dict):
                    col_refs = {}
                    links = {}
                    keys = []
                    for table in [self.name] + list(kw['join'].keys()):
                        if not table in self.database.tables:
                            raise InvalidInputError(
                                table,
                                f"{table} is not a valid table in {self.database}"
                            )
                        for col in self.database.tables[table].columns:
                            column = self.database.tables[table].columns[col]
                            if table in kw['join']:
                                cont = False
                                for col1, col2 in kw['join'][table].items():
                                    if f'{table}.{col}' == f'{col2}':
                                        cont = True
                                        if col1 in links:
                                            keys.append(links[col1])
                                            links[col2] = links[col1]
                                            break
                                        links[col2] = col1
                                        keys.append(col1)
                                        break
                                if cont:
                                    continue
                            if not self.database.type == 'postgres':
                                col_refs[f'{table}.{column.name}'] = column
                                keys.append(f'{table}.{column.name}')
                            else:
                                col_refs[f'{column.name}'] = column
            else:
                col_refs = self.columns
                keys = list(self.columns.keys())
        else:
            # only cache complete rows using '*'
            cache_new_rows = False

            col_refs = {}
            keys = []
            for col in col_select:  # selection:
                if not col in self.columns:
                    if not '.' in col:
                        raise InvalidColumnType(
                            f"column {col} is not a valid column",
                            f"valid column types {self.columns}")
                    table, column = col.split('.')
                    if table in self.database.tables and column in self.database.tables[
                            table].columns:
                        if not self.database.type == 'postgres':
                            col_refs[col] = self.database.tables[
                                table].columns[column]
                        else:
                            col_refs[column] = self.database.tables[
                                table].columns[column]
                        keys.append(col)
                        continue

                col_refs[col] = self.columns[col]
                keys.append(col)
            selection = ','.join(col_select)

        # validates where conditions provided and where query
        where_sel = self.__where(kw)

        join = ''
        if 'join' in kw:
            # validate join usage
            join = self._join(kw)

            cache_new_rows = False
        else:
            # join statements cannot return cached rows with
            # cache check
            if 'where' in kw and isinstance(kw['where'],
                                            dict) and self.cache_enabled:
                cached_row = None
                for column, value in kw['where'].items():
                    # primary key used in where statement
                    if column == self.prim_key:
                        # check if value exists in cache
                        if value in self.cache:
                            cached_row = self.cache[value]
                # check cached_row against other where conditions
                # As primary key was used, we know only 1 row should ever
                # exist so remaining conditions can be safely validated
                if not cached_row == None:
                    for column, value in kw['where'].items():
                        if column == self.prim_key:
                            continue
                        # check cached value against 'where' value
                        if not cached_row[column] == value:
                            # no rows match condition
                            return []
                    # return cache row
                    if '*' in selection:
                        self.log.debug(
                            f"## cache - SELECT * - {cached_row} ##")
                        return [cached_row]
                    else:
                        cached_row = {
                            sel: cached_row[sel]
                            for sel in col_select
                        }
                        self.log.debug(
                            f"## cache - SELECT {col_select} - {cached_row} ##"
                        )
                        return [cached_row]

        orderby = ''
        if 'orderby' in kw:
            if not kw['orderby'] in self.columns:
                raise InvalidInputError(
                    f"orderby input {kw['orderby']} is not a valid column name",
                    f"valid columns {self.columns}")
            orderby = ' ORDER BY ' + kw['orderby']
        query = 'SELECT {select_item} FROM {name} {join}{where}{order}'.format(
            select_item=selection,
            name=self.name,
            join=join,
            where=where_sel,
            order=orderby)
        try:
            rows = await self.database.get(query)
        except Exception as e:
            self.log.exception(
                f"Exception while selecting data in {self.name} ")
            raise e
            #cache_new_rows = False
            #rows = []

        #dictonarify each row result and return
        to_return = []
        if not rows == None:
            for row in rows:
                if self.database.row_return_type is tuple:
                    r_dict = {}
                    for i, v in enumerate(row):
                        try:
                            if not v == None and col_refs[
                                    keys[i]].type == str and '{"' and '}' in v:
                                r_dict[keys[i]] = json.loads(v)
                            else:
                                r_dict[keys[i]] = v if not col_refs[
                                    keys[i]].type == bool else bool(v)
                        except Exception as e:
                            self.log.exception(
                                f"error processing results on row {row} index {i} value {v} with {keys}"
                            )
                            assert False
                    to_return.append(r_dict)
                if self.database.row_return_type is dict:
                    r_dict = {}
                    for column, value in dict(row).items():
                        if not value == None and col_refs[
                                column].type == str and '{"' and '}' in value:
                            r_dict[column] = json.loads(value)
                        else:
                            r_dict[column] = value if not col_refs[
                                column].type == bool else bool(value)

                    to_return.append(r_dict)
        if cache_new_rows and self.cache_enabled:
            for row in to_return:
                value_to_cache = row[self.prim_key]
                self.cache[value_to_cache] = row
        return to_return
Exemple #7
0
    def __where(self, kw):
        where_sel = ''
        kw = self._process_input(kw)
        if not 'where' in kw:
            return where_sel
        and_value = 'WHERE '

        supported_operators = {
            '=', '==', '<>', '!=', '>', '>=', '<', '<=', 'like', 'in',
            'not in', 'not like'
        }
        if isinstance(kw['where'], list):
            for condition in kw['where']:
                if not type(condition) in [dict, list]:
                    raise InvalidInputError(
                        f"{condition} is not a valid type within where=[]",
                        "invalid subcondition type within where=[], expected type(list, dict)"
                    )
                if isinstance(condition, list):
                    if not len(condition) == 3:
                        cond_len = len(condition)
                        raise InvalidInputError(
                            f"{condition} has {cond_len} items, expected 3",
                            f"{condition} has {cond_len} items, expected 3")
                    condition1 = f"{condition[0]}"
                    operator = condition[1]
                    condition2 = condition[2]

                    table, column = None, None
                    # expecting comparison operators
                    if not operator in supported_operators:
                        raise InvalidInputError(
                            f"Invalid operator {operator} within {condition}",
                            f"supported operators [{supported_operators}]")
                    for i, value in enumerate([condition1, condition2]):
                        if i == 0:
                            table, column = self._validate_table_column(value)
                            pass
                        if i == 1 and 'in' in operator:
                            # in operators should be proceeded by a list of values
                            if not isinstance(condition2, list):
                                raise InvalidInputError(
                                    f"Invalid use of operator '{operator}' within {condition}",
                                    f"'in' should be proceeded by ['list', 'of', 'values'] not {type(condition2)} - {condition2}"
                                )
                            if self.database.tables[table].columns[
                                    column].type == str:
                                condition2 = [
                                    f"'{cond}'" for cond in condition2
                                ]
                            else:
                                condition2 = [str(cond) for cond in condition2]
                            condition2 = ', '.join(condition2)
                            condition2 = f"({condition2})"
                            break
                        if i == 1:
                            self._validate_table_column(value, no_raise=True)
                            """still raises if dot_table used & not in table """
                            pass

                    if 'like' in operator:
                        condition2 = f"{condition2}"
                        if not '*' in condition2:
                            condition2 = f"'%{condition2}%'"
                        else:
                            condition2 = '%'.join(condition2.split('*'))
                            condition2 = f"'{condition2}'"

                    where_sel = f"{where_sel}{and_value}{condition1} {operator} {condition2}"
                if isinstance(condition, dict):
                    for col_name, v in condition.items():
                        table, column = self._validate_table_column(col_name)
                        table = self.database.tables[table]
                        eq = '=' if not v == 'NULL' else ' IS '
                        #json check
                        if v == 'NULL' or table.columns[
                                column].type == str and '{"' and '}' in v:
                            where_sel = f"{where_sel}{and_value}{col_name}{eq}{v}"
                        else:
                            val = v if table.columns[
                                column].type is not str else f"'{v}'"
                            where_sel = f"{where_sel}{and_value}{col_name}{eq}{val}"

                and_value = ' AND '

        if isinstance(kw['where'], dict):
            for col_name, v in kw['where'].items():
                table, column = self._validate_table_column(col_name)
                table = self.database.tables[table]
                eq = '=' if not v == 'NULL' else ' IS '
                #json check
                if v == 'NULL' or table.columns[
                        column].type == str and '{"' and '}' in v:
                    where_sel = f"{where_sel}{and_value}{col_name}{eq}{v}"
                else:
                    val = v if table.columns[
                        column].type is not str else f"'{v}'"
                    where_sel = f"{where_sel}{and_value}{col_name}{eq}{val}"
                and_value = ' AND '
        return where_sel