Advanced Usage¶
Scenarios¶
This decorator registers a function or a class as a scenario in the
given Context
. Registered scenarios can
later be invoked and executed by a play or directly.
When invoked, each scenario gets a Connection
object with an active connection to the database. Scenarios can obtain
cursors from this object in order to pass them to the executed queries.
By default, each scenario gets “infused” with all the SQL queries
from the parsed SQL files. Every parsed query becomes an attribute
of the scenario object. This is done for the convenience
purposes and allows you to invoke any parsed SQL query within the
scenario. In the example below we invoke insert_department
query which was declared in the SQL file:
-- name: insert_department
INSERT INTO DBL_DEPARTMENTS (NAME) VALUES (?);
It can be invoked like this:
@context.scenario
def my_scenario(connection):
with connection.cursor() as cur:
my_scenario.insert_department(cur, "New Department")
Examples of scenarios¶
Register a function as a scenario in context
under the name
“create_users”:
@context.scenario
def create_users(connection):
print("Do something")
Register a class as a scenario in context
under the name
“createusers”:
@context.scenario
class CreateUsers():
def __call__(self, connection):
print("Do something")
Register scenario under a name that differs from the decorated object’s name:
@context.scenario(name="create_users")
class my_scenario(connection):
print("Do something")
Create a cursor within scenario and use it to manually execute a query:
@context.scenario
def create_users(connection):
with connection.cursor() as cur:
stmt = "INSERT INTO USERS VALUES (USERS_SEQ.nextval, 'Alexander', 'Dortmund')"
cur.execute(stmt)
Queries¶
When a function decorated by the this decorator is called it gets
passed two arguments: cursor
and sql
.
cursor
is an individual cursor object created by scenario that
executes this the query function. Cursor object has an execute
method capable of running text queries against connected database.
sql
is a SQL query string that mathes this function. The match
is found either through the match
parameter of the decorator or
by matching decorated function’s or class’s name.
The query might not have a matching SQL query. But if it does
you may use curosr.execute
and sql
arguments to run it.
You can also directly use the cursor.execute
method to run your own
query. Below are few options to declare and invoke a function or a class
decorated with the query
decorator.
This decorator also sets the _is_decorated_by_query
attribute of
decorated function to True. This is later used during safety validation
to make sure that the method is decorated by the @context.query
before being decorated further by such decorators as @context.simple
.
Examples of queries¶
Method that has a matching query called “create_table” in the SQL queries file:
@context.query
def create_table(cursor, sql):
with cursor as c:
c.execute(sql)
and its matching query from the SQL queries file, annotated by the
-- name: create_table
comment line:
-- name: create_table
CREATE TABLE DEPARTMENTS (
ID INTEGER GENERATED ALWAYS AS IDENTITY,
NAME VARCHAR(50) NOT NULL,
PRIMARY KEY (ID),
UNIQUE (NAME)
);
The next method below simply uses cursor directly to execute some text query and ignores any matching queries from the SQL file:
@context.query
def do_whatever(cursor):
rs = None
with cursor as c:
description, rs = c.execute("SELECT * FROM employees;")
print(description)
return rs
Method within a class:
@context.query
def insert_sale(self, cursor, sql, amount=100):
with cursor as c:
c.execute("INSERT INTO SALES VALUES (?)", [amount])
Below is a method that does not use cursor
or sql
arguments
at all. This makes the decorator useless, because the main purpose of
the decorator is to pass the connection cursor and a matching SQL query
to the function during it’s execution:
@context.query
def some_logic():
import time
time.sleep(1)
print("I do whatever I want")
This method will look for query that matches the match
parameter
of the decorator and supply it via sql
argument to the function:
@context.query(match="create_employees_index")
def my_logic(cursor, sql):
with cursor as c:
c.execute(sql)
This method achieves the exact same result. But instead of
looking up the query using the match
parameter it find the
matching query using the __name__
of the decorated function
by default (in this case it’s “create_employees_index”):
@context.query
def create_employees_index(cursor, sql):
cursor.execute(sql)
print(cursor.fetchall())
cursor.close()
SQL files¶
The name:
tag¶
When parser detects a comment that starts with --
and contains
name: my_query
somewhere in the line, it treats every next line
as a part of the query named my_query
:
-- name: select_all
SELECT *
FROM EMPLOYEES
ORDER BY NAME;
When parser encounters another line that contains the name:
tag,
it takes all accumulated lines, glues them together, and saves them
in the context under their annotated name:
-- name: select_all
SELECT *
FROM EMPLOYEES
ORDER BY NAME;
-- name: select_james
/*
* At this moment, right after encountering another line with the "name:"
* tag, parser takes everything between the lines "-- name: select_all"
* and "-- name: select_james" and saves it as the contents of the
* "select_all" query.
*/
SELECT * FROM EMPLOYEES WHERE NAME = 'James';
If there was some text or other queries that were not annotated
at the beginning of the file, then they will be skipped and thrown
out. The parser only considers queries that start with the comment
line that contains name:
tag in it:
-- file: SQL Queries
SELECT 1 FROM DUAL;
/* All this text above will be ignored, including this comment */
-- name: select_all
SELECT *
FROM EMPLOYEES
ORDER BY NAME;
Name tag can contain any alphanumeric characters or, generally
speaking, any characters that matches \w
regex:
[a-zA-Z0-9_]
. It permits lowercase and uppercase characters as
well as numbers and underscore:
-- name: my_query
-- name: c0oL_qUeRy
... etc
Comments without name:
tag¶
If parser encounters a comment line that starts with the --
but does not contain a name:
tag it will just skip it as if
it this line does not exist:
-- name: my_query
SELECT *
-- this comment will be ignored
FROM MY_TABLE;
Parser also ignores multiline-format comments that start with the
/*
and end with the */
symbols:
SELECT *
/* this will be ignored even with "name: my_query" inside */
FROM MY_TABLE;
The scenario:
tag¶
The scenario:
tag is optional. It tells the parser which implicit
scenario this query should belong to. Implicit scenario is a scenario that
was not explicitly declared in the python file where Context
object is defined. Implicit scenarios declared in the query annotations are
automatically created in the Context
object:
-- name: select_james, scenario: single_select
SELECT * FROM EMPLOYEES WHERE NAME = 'James';
The example above will implicitly create a scenario called single_select
.
When executed directly or inside a play, the single_select
scenario will
run a query called select_james
and that’s all. This demonstrates that
defining scenarios explicitly in Python is not mandatory. It is possible to
just mention the scenario across one or many queries in the SQL files and
it will get automatically created and populated with those queries.
A query can belong to multiple implicit scenarios, by specifying the
scenario:
tag multiple times:
-- name: select_all, scenario: run_all, scenario: show_deps
SELECT * FROM DEPARTMENTS;
-- name: insert_employee, scenario: run_all, scenario: insert
INSERT INTO EMPLOYEES(ID) VALUES(EMP_SEQ.NEXT);
In the example above 3 scenarios will be auto-generated and added
to the Context
object: run_all
, show_deps
, and
insert
. All of them do not exist in the python file but they
will now be created in the context.
When executed, run_all
scenario will run 2 queries one after the
other. First, it will execute the select_all
query. Then it will
execute the insert_employee
query.
All return values for queries executed within implicitly generated scenarios are ignored.
Since every explicitly defined scenario is already infused with all the parsed SQL queries, then there is no need to use scenario tag for queries used in explicitly defined scenarios.
Main purpose of the scenario:
tag is to create implicit scenarios
that are burdensome to declare in python and mostly represent
boilerplate code. For example, a scenario that pre-creates or
destroys all tables, schema, sequences, and data required for running
the simulated load.
Ordered scenario[30]
¶
By default, when two or more queries mention the same scenario in their annotation, these queries get executed in the order in which they were declared in the SQL file.
When mentioning implicit scenarios in the annotation of the SQL queries it is possible to specify the order of execution of this particular query within the generated scenario.
For example, these two queries mention the same implicit scenario
called teardown
:
-- name: drop_dbload_schema, simulation: teardown[900]
DROP SCHEMA DBLOAD;
-- name: drop_departments_table, simulation: teardown[100]
DROP TABLE DBLOAD.DBL_DEPARTMENTS;
But because they specify the order in the square brackets, the
drop_department_table
query will get executed before the
drop_dbload_schema
query, since it’s “priority” within the
teardown
scenario is higher (100 < 900
– the smaller the number,
the higher is priority).
Order numbers in the square brackets are sorted in the ascending order,
which means that -100
will be executed before 0
. And 4
will be
executed before 10
.