172 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			172 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
'''
 | 
						|
Database
 | 
						|
 | 
						|
Central object for defining storage protocol-specific interfaces. The database wraps up
 | 
						|
central items for interacting with database resources, namely the Accessor and Manager
 | 
						|
objects.
 | 
						|
 | 
						|
The Database type hierarchy attempts to be exceedingly general; SQL-derivatives should
 | 
						|
subclass from the RelationalDatabase subtype, for example, which itself becomes a new
 | 
						|
generic via a type dependence on Relation.
 | 
						|
 | 
						|
While relying no many constituent pieces, Databases intend to provide all needed objects
 | 
						|
under one roof. This includes the Engine (opens up connections to the database), Accessors
 | 
						|
(running select-like queries on DB data), Managers (updating DB state with sync
 | 
						|
insert-like actions), and Indexers (systematically caching Accessor queries). Generalized
 | 
						|
behavior is supported by explicitly leveraging the individual components. For example,
 | 
						|
 | 
						|
.. code-block:: python
 | 
						|
 | 
						|
    with db.engine.connect() as connection:
 | 
						|
        db.access.select(
 | 
						|
            connection,
 | 
						|
            <query>
 | 
						|
        )
 | 
						|
        db.manager.insert(
 | 
						|
            connection,
 | 
						|
            component,
 | 
						|
            data
 | 
						|
        )
 | 
						|
 | 
						|
The Database also supports a few directly callable methods for simplified interaction.
 | 
						|
These methods manage a connection context internally, passing them through the way they
 | 
						|
might otherwise be handled explicitly, as seen above.
 | 
						|
 | 
						|
.. code-block:: python
 | 
						|
 | 
						|
    db.select(<query>)
 | 
						|
    
 | 
						|
    db.insert(<query>, data)
 | 
						|
 | 
						|
 | 
						|
.. admonition:: on explicit connection contexts
 | 
						|
 | 
						|
    Older models supported Accessors/Managers that housed their own Engine instances, and
 | 
						|
    when performing actions like ``insert``, the Engine would be passed all the way through
 | 
						|
    until a Connection could be spawned, and in that context the single action would be
 | 
						|
    made. This model forfeits a lot of connection control, preventing multiple actions
 | 
						|
    under a single connection.
 | 
						|
 | 
						|
    The newer model now avoids directly allowing Managers/Accessors access to their own
 | 
						|
    engines, and instead they expose methods that explicitly require Connection objects.
 | 
						|
    This means a user can invoke these methods in their own Connection contexts (seen
 | 
						|
    above) and group up operations as they please, reducing overhead. The Database then
 | 
						|
    wraps up a few single-operation contexts where outer connection control is not needed.
 | 
						|
'''
 | 
						|
import logging
 | 
						|
 | 
						|
from co3.engine   import Engine
 | 
						|
from co3.schema   import Schema
 | 
						|
from co3.manager  import Manager
 | 
						|
from co3.indexer  import Indexer
 | 
						|
from co3.accessor import Accessor
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
class Database[C: Component]:
 | 
						|
    '''
 | 
						|
    Generic Database definition
 | 
						|
 | 
						|
    Generic to both a Component (C), and an Engine resource type (R). The Engine's
 | 
						|
    generic openness must be propagated here, as it's intended to be fully abstracted away
 | 
						|
    under the Database roof. Note that we cannot explicitly use an Engine type in its
 | 
						|
    place, as it obscures its internal resource type dependence when we need it for
 | 
						|
    hinting here in ``__init__``.
 | 
						|
 | 
						|
    .. admonition:: Development TODO list
 | 
						|
 | 
						|
        Decide on official ruling for assigning Schema objects, and verifying any
 | 
						|
        attempted Component-based actions (e.g., inserts, selects) to belong to or be a
 | 
						|
        composition of Components within an attached Schema. Reasons for: helps complete
 | 
						|
        the sense of a "Database" here programmatically, incorporating a more structurally
 | 
						|
        accurate representation of allowed operations, and prevent possible attribute and
 | 
						|
        type collisions. Reasons against: generally not a huge concern to align Schemas as
 | 
						|
        transactions will rollback, broadly increases a bit of bulk, and users often
 | 
						|
        expected know which components belong to a particular DB. Leaning more to **for**,
 | 
						|
        and would only apply to the directly supported method passthroughs (and thus would
 | 
						|
        have no impact on independent methods like ``Accessor.raw_select``). Additionally,
 | 
						|
        even if component clashes don't pose serious risk, it can be helpful to
 | 
						|
        systematically address the cases where a misalignment is occurring (by having
 | 
						|
        helpful ``verify`` methods that can be ran before any actions).
 | 
						|
    '''
 | 
						|
    _accessor_cls: type[Accessor[C]] = Accessor[C]
 | 
						|
    _manager_cls:  type[Manager[C]]  = Manager[C]
 | 
						|
    _engine_cls:   type[Engine]      = Engine
 | 
						|
 | 
						|
    def __init__(self, *engine_args, **engine_kwargs):
 | 
						|
        '''
 | 
						|
        Parameters:
 | 
						|
            engine_args: positional arguments to pass on to the Engine object during
 | 
						|
                         instantiation 
 | 
						|
            engine_kwargs: keyword arguments to pass on to the Engine object during
 | 
						|
                           instantiation 
 | 
						|
 | 
						|
        Variables:
 | 
						|
            _local_cache: a database-local property store for ad-hoc CacheBlock-esque
 | 
						|
                          methods, that are nevertheless _not_ query/group-by responses to
 | 
						|
                          pass on to the Indexer. Dependent properties should write to the
 | 
						|
                          this cache and check for existence of stored results; the cache
 | 
						|
                          state must be managed globally.
 | 
						|
        ''' 
 | 
						|
        self.engine = self._engine_cls(*engine_args, **engine_kwargs)
 | 
						|
 | 
						|
        self.accessor = self._accessor_cls()
 | 
						|
        self.manager  = self._manager_cls()
 | 
						|
        self.indexer  = Indexer(self.accessor)
 | 
						|
 | 
						|
        self._local_cache = {}
 | 
						|
        self._reset_cache = False
 | 
						|
 | 
						|
    def raw_query(self, connection, query):
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def select(self, component: C, *args, **kwargs):
 | 
						|
        '''
 | 
						|
        .. admonition:: Dev note
 | 
						|
 | 
						|
            args and kwargs have to be general/unspecified here due to the possible
 | 
						|
            passthrough method adopting arbitrary parameters in subtypes. I could simply
 | 
						|
            overload this method in the relevant inheriting DBs (i.e., by matching the
 | 
						|
            expected Accessor's .select signature).
 | 
						|
        '''
 | 
						|
        with self.engine.connect() as connection:
 | 
						|
            return self.accessor.select(
 | 
						|
                connection,
 | 
						|
                component,
 | 
						|
                *args,
 | 
						|
                **kwargs
 | 
						|
            )
 | 
						|
 | 
						|
    def insert(self, component: C, *args, **kwargs):
 | 
						|
        with self.engine.connect() as connection:
 | 
						|
            return self.manager.insert(
 | 
						|
                connection,
 | 
						|
                component,
 | 
						|
                *args,
 | 
						|
                **kwargs
 | 
						|
            )
 | 
						|
 | 
						|
    def recreate(self, schema: Schema[C]):
 | 
						|
        self.manager.recreate(schema, self.engine)
 | 
						|
 | 
						|
    @property
 | 
						|
    def index(self):
 | 
						|
        if self.reset_cache:
 | 
						|
            self._index.cache_clear()
 | 
						|
            self.reset_cache = False
 | 
						|
        return self._index
 | 
						|
 | 
						|
    @property
 | 
						|
    def manage(self):
 | 
						|
        '''
 | 
						|
        Accessing ``.manage`` queues a cache clear on the external index, as well wipes the
 | 
						|
        local index.
 | 
						|
        '''
 | 
						|
        self.reset_cache = True
 | 
						|
        self._local_cache = {}
 | 
						|
        return self._manage
 | 
						|
 | 
						|
    def populate_indexes(self): pass
 | 
						|
 |