Database
========

Creating a Database
-------------------

The *Database* will store its *Tables* in a single file which name and directory must be specified.
To create this file with a `.db` extension, a call to the ``new`` method is required:

.. code-block:: python

    from SSD.core import Database

    # Create a new Database object and a new storage file
    db = Database(database_dir='my_directory',
                  database_name='my_database')
    db.new(remove_existing=True)

    # Same thing in a single line
    db = Database(database_dir='my_directory',
                  database_name='my_database').new(remove_existing=True)


Loading a Database
------------------

Loading an existing *Database* is pretty similar as creating a new one, except the call to the ``load`` method:

.. code-block:: python

    from SSD.core import Database

    # Create a new Database object and load an exiting storage file
    db = Database(database_dir='my_directory',
                  database_name='my_database')
    db.load(show_architecture=True)

    # Same thing in a single line
    db = Database(database_dir='my_directory',
                  database_name='my_database').load(show_architecture=True)


Creating a new Table
--------------------

Creating a new *Table* to the *Database* only requires its name and the type (either *StoringTable* or *ExchangeTable*):

.. code-block:: python

    # Create a StoringTable
    db.create_table(table_name='my_StoringTable',
                    storing_table=True)

    # Create an ExchangeTable
    db.create_table(table_name='my_ExchangeTable',
                    storing_table=False)


Creating a new Field
--------------------

Creating a new *Field* requires a few information in a tuple defined as
:guilabel:`(field_name, field_type, default_value)`.
The default value specifies the value to set in a row if an entry does not contain data for this field.
The name and the type of the *Field* are mandatory but the default value is optional; if not set, it will be ``<null>``.

*Fields* can either be created one by one or at once:

.. code-block:: python

    # Create several Fields in the StoringTable
    db.create_fields(table_name='my_StoringTable',
                     fields=[('my_Value', float, 0.), ('my_Condition', bool, False), ('my_Color', str)])

    # Create a unique Field in the ExchangeTable
    db.create_fields(table_name='my_ExchangeTable',
                     fields=('my_Data', float, 0.))

It is also possible to create fields at *Table* creation:

.. code-block:: python

    # Create a StoringTable with several fields
    db.create_table(table_name='my_StoringTable',
                    storing_table=True,
                    fields=[('my_Value', float, 0.), ('my_Condition', bool, False), ('my_Color', str)])

    # Create an ExchangeTable with a unique field
    db.create_table(table_name='my_ExchangeTable',
                    storing_table=False,
                    fields=('my_Data', float, 0.))


The following *Field* types are available:

.. table::
    :widths: 10 15 20

    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+
    | **Type**     | **Definition**                       | **Field Documentation**                                                               |
    +==============+======================================+=======================================================================================+
    | ``int``      | :guilabel:`int`                      | `IntegerField <http://docs.peewee-orm.com/en/latest/peewee/api.html#IntegerField>`_   |
    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+
    | ``float``    | :guilabel:`float`                    | `FloatField <http://docs.peewee-orm.com/en/latest/peewee/api.html#FloatField>`_       |
    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+
    | ``str``      | :guilabel:`str`                      | `TextField <http://docs.peewee-orm.com/en/latest/peewee/api.html#TextField>`_         |
    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+
    | ``bool``     | :guilabel:`bool`                     | `BooleanField <http://docs.peewee-orm.com/en/latest/peewee/api.html#BooleanField>`_   |
    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+
    | ``ndarray``  | :guilabel:`import numpy.ndarray`     | Field class for storing numpy arrays.                                                 |
    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+
    | ``datetime`` | :guilabel:`import datetime.datetime` | `DateTimeField <http://docs.peewee-orm.com/en/latest/peewee/api.html#DateTimeField>`_ |
    +--------------+--------------------------------------+---------------------------------------------------------------------------------------+


Adding data to a Table
----------------------

Adding data to a *Table* can be done either line by line either per batch of lines.
In both cases, data must be passed as a dictionary and the index of the created line(s) are returned:

.. code-block:: python

    # Add a batch to the StoringTable
    db.add_batch(table_name='my_StoringTable',
                 batch={'my_Value': [7.4, 5.6, -2.1],
                        'my_Condition': [True, True, False],
                        'my_Color': ['red', 'orange', 'blue']})

    # Add a single line to the ExchangeTable
    db.add_data(table_name='my_ExchangeTable',
                data={'my_Data': 0.5})


Updating data in a Table
------------------------

Updating data is also possible and can be only performed line by line.
The index of the line can be specified (index can be negative to count from the last line).
By default, the last entry will be updated.
The data still needs to be passed as a dictionary, only specified *Fields* will be updated.

.. code-block:: python

    # Update the 3rd line of the StoringTable
    db.update(table_name='my_StoringTable',
              line_id=3,
              data={'my_Value': 1.3,
                    'my_Color': 'green'})

    # Update the last line of the StoringTable
    db.update(table_name='my_StoringTable',
              line_id=-1,
              data={'my_Value': -1.9})


Getting data from a Table
-------------------------

Getting data from a *Table* can be done either line by line either per batch of lines.
By default, all fields are received but a selection can be specified.
With ``get_line`` method, the index of the line can be specified; by default, the last line is selected.
With ``get_lines`` method, a set of lines can be specified; if this set of lines is not specified, a range of lines can
be specified; by default, the whole set of lines is selected.
In both cases, data is received as a dictionary:

.. code-block:: python

    # Get a batch from the StoringTable
    db.get_lines(table_name='my_StoringTable',
                 fields=['my_Value', 'my_Color'],
                 lines_range=[1, -1],
                 batched=True)
    """
    >> {'my_Value': [7.4, 5.6, -2.1],
        'my_Condition': [True, True, False],
        'my_Color': ['red', 'orange', 'blue']}
    """

    # Get a line from the ExchangeTable
    db.get_line(table_name='my_ExchangeTable'
                fields='my_Data',
                line_id=1)
    """
    >> {'my_Data': 0.5}
    """


Connecting Signals
------------------

*Tables* can send two types of signals when data is added: a ``pre_save_signal`` and a ``post_save_signal``.
Signal handler can be connected to these signals by the *Database*.
When data is added to a *Table*, the registered handlers are triggered in the registration order (just before or just
after the data insert depending on the signal type).
Signals must be registered and connected when initializing the *Database*:

.. code-block:: python

    # Create a new Database
    db = Database(database_dir='my_directory',
                  database_name='my_database').new(remove_existing=True)
    # Create an ExchangeTable with one Field
    db.create_fields(table_name='my_ExchangeTable',
                     fields=('my_Data', float, 0.))

    # Define handlers
    def pre_save_handler(table_name, data):
        print(f"Pre-save signal received from {table_name}")

    def post_save_handler(table_name, data):
        print(f"Post-save signal received from {table_name} with data={data}")

    # Register signals
    db.register_pre_save_signal(table_name='my_ExchangeTable',
                                handler=pre_save_handler)
    db.register_post_save_signal(table_name='my_ExchangeTable',
                                 handler=post_save_handler)

    # Connect signals once they are all registered
    db.connect_signals()

    # Adding data to the Table
    db.add_data(table_name='my_ExchangeTable',
                data={'my_Data': 0.5})
    """
    >> Pre-save signal received from my_ExchangeTable
    >> Post-save signal received from my_ExchangeTable with data={'my_Data': 0.5}
    """