Rules

A multidimensional database - like TinyOlap - that can aggregate numbers over hierarchies is already a nice thing. But the real fun begins, when you want to add business logic to your database. In a relational database you can use triggers and write ystore procedures, or define you business logic at query time either by individual SQL statements or by views.

In TinyOlap, all business logic despite simple aggregations can be defined through rules. Rules can be seen as combination of a trigger and a stored procedure. The main difference is, that rules can be written in plain Python, either as a function, a static class method (using the @staticmethod decorator) or even as a lambda function. This allows you to write high structured code and leverage the entire Python universe for you business logic. Cool!

Once you understand the basic concept of how to write rules, you will that it is very simple and straight forward to define your custom business logic.

The basic concept of Rules

Lets take a look at an example to calculate the delta between actual and plan values in a cube called sales. Let’s assume all of these 3 members are defined in the same dimension datatype.

The following rule code fragment represents a proper written rule. Although rules can also be written and registered without the @rule decorator, it should become your best practise to always use the @rule decorator when writing rules. It simply makes you business logic much more readable and maintainable.

Rules usually consists of 3 main parts:

  • The @rule decorator, defining the behavior and assignment of a rule. Primarily this is…

    • the TinyOlap cube the which the rule should be assigned to. Ech individual rule should be assigned to 1 cube only.

    • the trigger for the rule. This normally is a cell address pattern that defines a single member or multiple members from one or multiple dimension. In the sample below it is just 1 member from one dimension called delta. Every time a cell or report is requested that contains (addresses) the member delta, then this rule will be executed and the value returned from the rule will be shown to the user (returned from the cube).

    • some settings that define when and how the rule should behave or treated. We’ll come back to this later.

  • The function signature which is always should look like this def any_function_name(c: Cell):. The name of the function can be anything, but should ideallyc express what the rule is doing. The call signature must contain at least one single parameter c which represents the cell (or address) of a cube that is currently requested from the user. It is highly beneficial to use the Cell type hint at all time, e.g. to benefit from code assistance. If you want to call the rule function on your own and you require additional parameters, then these need to be optional as the TinyOlap engine will call the function only with one parameter, the current cell context.

    The Cell allows you to navigate through the data space. As shown in the example below, you can easily “walk” through the data space by shifting and investigate your data space by shifting to

  • A returned value. This can be any datatype, but it should ideally be a generic type like float, int or string. As TinyOlap is all about numbers, the float datatype is your best friend and choice.

@rule(cube="sales", trigger=["delta"])
def rule_profit(c: Cell):
    return c["actual"] - c["plan"]

Simple Rules

Sample of a proper rule:

@rule("sales", ["avg. price"], scope=RuleScope.ALL_LEVELS, volatile=False)
def rule_average_price(c : tinyolap.context):
    quantity = c["quantity"]
    sales = c["sales"]
    # ensure both values exist or are of the expected type (cell values can be anything)
    if quantity is float and sales is float:
        if quantity != 0.0:
            return sales / quantity
        return "n.a."  # the rule developer decided to return some text. That is totally fine.
    return c.CONTINUE
enum tinyolap.rules.RuleScope(value)

Defines the scope of a rule. Meaning, to which level of data the rule should be applied.

Member Type

int

Valid values are as follows:

ALL_LEVELS

(default) Indicates that the rule should be executed for base level and aggregated level cells.

AGGREGATION_LEVEL

Indicates that the rule should be executed for aggregated level cells only.

BASE_LEVEL

Indicates that the rule should be executed for base level cells only.

ROLL_UP

Indicates that the rule should replace the base level cell value from the database by the results of the rule. This can dramatically slow down aggregation speed. Requires a special trigger to be set.

ON_ENTRY

Indicates that these rules should be executed when cell values are set or changed. This is useful for time consuming calculations which may be too expensive to run at idx_address time.

COMMAND

Indicates that these rules need to be invoked by a command. Requires the decorator parameter ‘command to be specified.

enum tinyolap.rules.RuleInjectionStrategy(value)

Defines the code injection strategy for individual rules.

By default, TinyOlap rules reside and will be executed from with your custom code. This is preferable for a lot of situations, e.g. for development and debugging, or when your business logic require resources that can or should not become a part of a TinyOlap database, like calling other systems or systems. When a TinyOlap database is running in in-memory mode this is anyhow the only available option to provide business logic to a TinyOlap database.

However, when you intend to hand over a TinyOlap database to someone else, or if you want to host it as a generic webservice, then your business logic ideally goes with the database.

To enable this TinyOlap can automatically inject your rule source code into the database and persist it with the database. The next time the database will be opened, your code will be automatically instantiated and run from within the TinyOlap engine itself. You can at anytime override / replace these injected rules with your own code by calling the add_rule(...) method provide by the cube class.

There are 4 different strategies available how to inject rules into a TinyOlap database. Depending on your use case, you should try to use the most restrictive strategy possible, as explained below in the documentation of the different RuleInjectionStrategy enum values.

Please be aware that code injection does work on actual code level, not on file level. If you have created dynamic code, the code should be properly extracted by TinyOlap.

Member Type

int

Valid values are as follows:

NO_INJECTION

(default) Indicates that the rule should not be injected into the database.

FUNCTION_INJECTION

Indicates that only the rule function itself will be injected into the database. All surrounding code of the module or project where the rule function is defined, will be ignored. This requires your rule function to be autonomous. Meaning, without any dependencies to functions or classes from within your code. By default, TinyOlap will only reference the following built-in Python modules using the from [module name] import * trigger, when running your code: math, cmath, statistics, decimal, fractions, random, datetime, time, re, json. FUNCTION_INJECTION should be the preferred strategy for simple business logic that acts upon the data from a TinyOlap database only.

MODULE_INJECTION

Indicates that the entire module in which the rule function is defined will be injected doc: into the database. Code from other modules of your project will be ignored. If your rules are spread or multiple modules, all these modules will be injected. This requires that all modules and Python packages referenced from within your module must also be installed on the target system. TinyOlap will raise an appropriate error if the instantiation of your code module in the target environment will fail. MODULE_INJECTION should be the preferred strategy for more complex business logic or business logic that requires certain initialization (e.g. read exchange rates from a service)

PROJECT_INJECTION

NOT YET SUPPORTED Indicates that the entire project in which the rule function is defined will be injected into the database. This

class tinyolap.rules.Rules(cube: str)

Represemts a list of rules. Rules define custom calculations or business logic to be assigned to a cube.

Rules consist two main components:

  • A trigger or trigger, defining the context for which the rule should be executed

  • A scope, defining to which level of data the rule should be applied. Either for base level cells, aggregated cells, all cells or on write back of values.

  • A function, defining the custom calculation or business logic. This can be any Python method or function.

Attention

Rules functions have to be implemented as a simple Python function with just one single parameter and a return value. The single parameter should be called ‘c’ and will contain an TinyOlap Cell, representing the current cell context the rule should be calculated for.

What happens in a rule function, is up totally to the programmer. The value returned by rules function can either be a certain value (most often a numerical number, but can be anything) or one of the following constants which are directly available from within a cursor object.

  • NONE - Indicates that rules function was not able return a proper result (why ever).

  • CONTINUE - Indicates that either subsequent rules should continue and do the calculation work

    or that the cell value, either from a base-level or an aggregated cell, form the underlying cube should be used.

  • ERROR - Indicates that the rules functions run into an error. Such errors will be pushed up to initially calling cell request.

Sample of a proper rule:

def rule_average_price(c : tinyolap.context):
    quantity = c["quantity"]
    sales = c["sales"]
    # ensure both values exist or are of the expected type (cell values can be anything)
    if quantity is float and sales is float:
        if quantity != 0.0:
            return sales / quantity
        return "n.a."  # the developer decided to return some text, what is totally fine.
    return c.CONTINUE
add(rule: tinyolap.rules.Rule)

Adds a new rule to the list of rules. If a rule with the same trigger pattern already exists, then the existing rule will be replaceed by the new rule. :param rule: The Rule to be added.

first_match(idx_address)

Returns the first trigger match, if any, for a given cell address.

Parameters

idx_address – The cell address in index number_format.

Returns

Returns a tuple (True, function) if at least one trigger matches, function is the actual rules function to call, or (False, None) if none of the patterns matches the given cell idx_address.

match(scope: RuleScope, idx_address: list[tuple[int, int]])

Returns the first trigger match, if any, for a given cell address.

Parameters
  • scope – The rule scope for which a matching rule is requested.

  • idx_address – The cell address (in index int format) to be evaluated.

Returns

Returns a tuple (True, function) if at least one trigger matches, function is the actual rules function to call, or (False, None) if none of the patterns matches the given cell idx_address.

register(function, cube: str, function_name: str, pattern: list[str], idx_pattern: list[tuple[int, int]], scope: tinyolap.rules.RuleScope, injection: tinyolap.rules.RuleInjectionStrategy, code: Optional[str] = None)

Registers a rules function (a Python method or function).

Parameters
  • cube – The cube the rule should be registered for.

  • injection – The injection strategy defined for the rule.

  • code – (optional) the source code of the rule.

  • scope – The scope of the rule function.

  • function_name – Name of the rule function.

  • function – The Python rule function to execute.

  • pattern – The cell trigger to trigger the rule function.

  • idx_pattern – The cell index trigger to trigger the rule function.