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 parameterc
which represents the cell (or address) of a cube that is currently requested from the user. It is highly beneficial to use theCell
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 toA 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.