Prefer manual installation? Skip this section and proceed to the guide for installing ThingsDB from source code on your specific operating system Linux, Mac, or Windows (WSL). Choose the instructions that match your system.
Copyright © 2024
"I extend my heartfelt gratitude to Rik, Anja, Koos, Sasha and Iris for their invaluable contributions to the creation of this book."
The primary objective of this book is to equip readers with a comprehensive understanding of how ThingsDB functions. It is not intended to be an all-inclusive reference guide and therefore does not cover every aspect of the platform. ThingsDB can be applied in a diverse range of applications, and once its potential is fully grasped, it has the power to inspire novel and innovative solutions. Our aim is to provide a balanced overview of ThingsDB's capabilities while also fostering creativity and encouraging readers to explore its multifaceted applications.
Consistent with a simplified approach, we present code blocks in this book without syntax highlighting. While sophisticated IDEs like vscode and dedicated tools like things-prompt and ThingsGUI provide syntax highlighting, coloring known functions, punctuation, etc., we've adopted a more straightforward approach for this book.
Dark blue for promptsOrange for code that the user needs to enterGreen for commentsLight blue for responses
The first five chapters of this book can be read independently of each other and can be skipped if you have a strong grasp of the concepts covered. You can assess your understanding by completing the quizzes at the end of each chapter. However, from chapter 6 until chapter 13, the book will focus on developing a "to-do" application using the "todos" collection. The concepts introduced in these chapters build upon each other, making it essential to follow them in sequence to execute the code examples and fully comprehend the presented material. If you really want to jump directly into a specific chapter, you can download the necessary collection state from our website, which is explained in Appendix I - Chapter Data Import.
Like with learning any programming language, ThingsDB requires a setup to execute code examples. This setup includes at least one ThingsDB node, which acts as the interpreter responsible for executing the code. Throughout the first chapters of this book, we will primarily communicate with ThingsDB using the "things-prompt" application, a tool designed for running small code snippets. As we progress, we will delve into more practical examples and explore the realm of event-driven programming, showcasing the true power of ThingsDB when interacting with other programming languages. While this book includes some Python examples, you are free to choose a language that suits your preference, such as C#, Go, PHP, JavaScript/Node.js or another supported language. The provided examples are relatively straightforward to adapt to other programming languages.
The process running ThingsDB is called a node. ThingsDB is built to run across multiple nodes for high availability and scalability but for most chapters in this book, running a single node will suffice.
Exploring Multiple Nodes:
If you are interested in experimenting with distributed setups, you can run multiple ThingsDB nodes on your machine. However, this involves manually setting up each node with unique data paths and ports (covered in detail in Chapter 15). Be aware that this process can be cumbersome.
Docker to the Rescue:
Docker offers a simpler and more efficient way to manage multiple ThingsDB nodes. Docker is a containerization platform that packages applications and their dependencies into self-contained units, ensuring consistent and portable environments.
This guide will walk you through installing and running ThingsDB using Docker Compose, a convenient tool for managing Docker configurations.
                If you do not already have Docker installed, you can download it from here: https://docs.docker.com/get-docker/
Check your docker version: (version 23 or higher is required)
$docker -vDocker version 25.0.3, build 4debf41
Create a project directory and navigate to that directory:
$mkdir thingsdb_book$cd thingsdb_book~/thingsdb_book$
Paste the following content into a new file named docker-compose.yml
                in your project directory: (or download the file from here:
                https://docs.thingsdb.io/v1/book/docker-compose.yml)
            
x-ti-template: &ti image: ghcr.io/thingsdb/node restart: unless-stopped environment: - THINGSDB_BIND_CLIENT_ADDR = '::' - THINGSDB_BIND_NODE_ADDR = '::' - THINGSDB_HTTP_API_PORT = 9210 - THINGSDB_HTTP_STATUS_PORT = 8080 - THINGSDB_WS_PORT=9270 - THINGSDB_MODULES_PATH = /modules/ - THINGSDB_STORAGE_PATH = /data/ services: node0: << : *ti hostname: node0 container_name: node0 command: "--init" ports: - 9200:9200 - 9210:9210 - 9270:9270 - 8080:8080 volumes: - ./node0/data:/data/ - ./node0/modules:/modules/ - ./node0/dump:/dump/## Uncomment the following sections to add more nodes (optional) # node1: # << : *ti # hostname: node1 # container_name: node1 # command: "--secret pass" # ports: # - 8081:8080 # volumes: # - ./node1/data:/data/ # - ./node1/modules:/modules/ # - ./node1/dump:/dump/
Make sure you are in the directory where your
                docker-compose.yml file is located
                and run the following command to pull the image:
~/thingsdb_book$docker compose pull…
Use this command to launch ThingsDB as a persistent background service:
~/thingsdb_book$docker compose up -d[+] Running 1/1✔ Container node0 Started
                You've successfully set up Docker. Let's move on to installing Python.
While building ThingsDB from source is possible on various Linux distributions, this documentation details the process for Ubuntu due to its popularity. We recognize other distributions might work, but they are outside the scope of this guide.
Step 1: Update and Install Packages:
Start with updating the packages list:
$sudo apt update…
Then, install the required packages:
$sudo apt-get install -y \ libuv1-dev \ libpcre2-dev \ libyajl-dev \ libcurl4-nss-dev \ libssl-dev \ build-essential \ cmake \ git…
Step 3: Clone and Build ThingsDB
Clone the ThingsDB repository:
$git clone https://github.com/thingsdb/ThingsDB.git…
Build ThingsDB:
$./ThingsDB/release-build.sh…
Step 4: Create a Symlink (Optional)
To easily start ThingsDB from any directory, create a symlink:
$sudo ln -sr ./ThingsDB/thingsdb /usr/bin/thingsdb
Step 5: Verify Installation
Check if ThingsDB is installed correctly:
$thingsdb --version_____ _ _ ____ _____ |_ _| |_|_|___ ___ ___| \| __ | | | | | | | . |_ -| | | __ -| |_| |_|_|_|_|_|_ |___|____/|_____| version: 1.6.0 |___| ThingsDB Node 1.6.0 Maintainer: Jeroen van der Heijden <jeroen@cesbit.com> Home-page: https://thingsdb.io
You've successfully built ThingsDB from source. Let's move on to installing Python.
The section describes building ThingsDB on MacOS.
Prerequisites:
Step 1: Update and Install Packages
Update the package lists:
%brew update…
Then, install necessary packages:
%brew install cmake libuv pcre2 yajl curl…
Step 2: Clone and Build ThingsDB
Clone the ThingsDB repository:
%git clone https://github.com/thingsdb/ThingsDB.git…
Build ThingsDB:
%./ThingsDB/release-build.sh…
Step 3: Create a Symlink (Optional)
To easily start ThingsDB from any directory, create a symlink:
%ln -s ~/ThingsDB/thingsdb /opt/homebrew/bin/thingsdb
Step 4: Verify Installation
Check if ThingsDB is installed correctly:
%thingsdb --version_____ _ _ ____ _____ |_ _| |_|_|___ ___ ___| \| __ | | | | | | | . |_ -| | | __ -| |_| |_|_|_|_|_|_ |___|____/|_____| version: 1.6.0 |___| ThingsDB Node 1.6.0 Maintainer: Jeroen van der Heijden <jeroen@cesbit.com> Home-page: https://thingsdb.io
Next Steps:
Now you can start ThingsDB by typing thingsdb in your terminal.
                Let's move on to installing Python.
While ThingsDB does not run natively on Windows, you can leverage WSL (Windows Subsystem for Linux) to create a Linux environment and enjoy all its functionalities! Here's how:
Prerequisites:
Step 1: Install Ubuntu
Open a PowerShell window and run:
PS C:\Users\admin>wsl --install -d UbuntuInstalling: Ubuntu…
Follow the on-screen instructions to provide a username and password for your Ubuntu environment.
Step 2: Update and Install Packages
Once Ubuntu is installed, open a WSL terminal (start menu > search for "Ubuntu") and update the package lists:
ti@LAPTOP:~$sudo apt update…
Then, install necessary packages:
ti@LAPTOP:~$sudo apt-get install -y \ libuv1-dev \ libpcre2-dev \ libyajl-dev \ libcurl4-nss-dev \ libssl-dev \ build-essential \ cmake…
Step 3: Clone and Build ThingsDB
Clone the ThingsDB repository:
ti@LAPTOP:~$git clone https://github.com/thingsdb/ThingsDB.git…
Build ThingsDB:
ti@LAPTOP:~$./ThingsDB/release-build.sh…
Step 4: Create a Symlink (Optional)
To easily start ThingsDB from any directory, create a symlink:
ti@LAPTOP:~$sudo ln -sr ./ThingsDB/thingsdb /usr/bin/thingsdb
Step 5: Verify Installation
Check if ThingsDB is installed correctly:
ti@LAPTOP:~$thingsdb --version_____ _ _ ____ _____ |_ _| |_|_|___ ___ ___| \| __ | | | | | | | . |_ -| | | __ -| |_| |_|_|_|_|_|_ |___|____/|_____| version: 1.6.0 |___| ThingsDB Node 1.6.0 Maintainer: Jeroen van der Heijden <jeroen@cesbit.com> Home-page: https://thingsdb.io
Next Steps:
Now you can start ThingsDB by typing thingsdb in your
                WSL terminal.
                Remember to open a WSL terminal (e.g., Ubuntu)
                every time you want to use ThingsDB.
While the code examples in this book are compatible with Python versions as old as 3.7, it is recommended to use version 3.8 or higher to ensure optimal performance and stability. We won't cover the complete Python installation process in this guide, as there is plenty of documentation available online. You should also check if Python is already installed on your system as it is often pre-installed.
After installing Python, install PIP as well. PIP is the package manager for Python and is the simplest way to install the ThingsDB Python module. You can install the module using the following command:
                $pip install python-thingsdb…Successfully installed deprecation-2.1.0 msgpack-1.0.7 python-thingsdb-1.0.7
To verify that the installation was successful, open Python and try to import the Client from the thingsdb module.
$python>>>from thingsdb.client import Client>>>
If this command runs without errors, the installation was successful.
Just like installing the ThingsDB Client module, you can install ThingsDB Prompt using PIP, Python's package manager.
$pip install thingsprompt…Successfully installed install-1.3.5 setproctitle-1.3.3 thingsprompt-1.0.9
Once the installation is complete, ThingsDB Prompt will be installed in your system's PATH. You should now be able to run it directly from your terminal. To verify the installation, run the following command:
                $things-prompt --version1.0.9
If the version is displayed, the installation was successful.
                If you have not launched ThingsDB before, you will need to invoke
                the --init argument to initiate the creation
                of necessary files within a designated data directory, serving as the
                repository for all ThingsDB entities.
                $thingsdb --init_____ _ _ ____ _____ |_ _| |_|_|___ ___ ___| \| __ | | | | | | | . |_ -| | | __ -| |_| |_|_|_|_|_|_ |___|____/|_____| version: 1.6.0 |___|
                In a separate shell, we can establish a connection to ThingsDB using the things-prompt tool. We'll utilize this tool for some straightforward exercises in this book. The things-prompt is one of several methods for interacting with the interpreter.
                Okay, lets connect to the ThingsDB interpreter:
$things-prompt -u admin -p pass -s //stuff127.0.0.1:9200 (//stuff)>
To use the command we must specify a username with the -u
                argument. Optionally, we can also provide a password using the -p
                argument (if not given, the prompt will ask for the password). We can also provide a scope
                with the -s argument and choose the //stuff
                scope which represents the "stuff" collection. A collection acts like a container for your data,
                it is the place where things are stored. You can create as many collections as you need.
                When you start ThingsDB for the first time, a single collection "stuff" is created
                which is used in this first example.
The prompt shows the IP address (or hostname) of the ThingsDB node you are connected to. It also includes the port number and the scope where the queries will run in.
                If you start using ThingsDB in a production environment, you should start with more than one node, preferably three or more. ThingsDB is designed from the start to run on multiple nodes and utilizes multiple nodes to perform, for example, garbage collection without blocking! Garbage collection is a problem for languages like Python, Go and JavaScript. Although it is usually fast, it might result in shortly blocking applications, especially when the application requires a lot of memory due to the initialization of many objects. ThingsDB has solved this problem by ensuring garbage collection will never run on more than one node at the same time. Queries which are received by the node who is busy with garbage collection will forward the query to another node so your client will not notice anything. The same technique is used for creating backups as well.
Congratulations! You're now equipped to embark on your ThingsDB journey. With this book as your guide, you'll gain a comprehensive understanding of this powerful data management solution and its capabilities. So, dive in, explore the concepts, and let ThingsDB unleash your creativity and innovation.
While ThingsDB is technically a programming language, it breaks the mold by not being primarily designed for writing traditional programs. Instead, it is geared towards providing developers with an intuitive and flexible approach to storing and retrieving data. This might seem more akin to SQL, another language primarily focused on data management. However, ThingsDB's syntax and structure closely resemble programming languages like JavaScript and Python, making it accessible to developers familiar with those paradigms.
ThingsDB shines as the connecting force within your application, integrating various components seamlessly. Unlike managing separate relational databases and message brokers, ThingsDB offers a unified solution. It provides relational data storage for structured information, a built-in pub/sub system for real-time event handling, and scheduled task automation. This simplifies your architecture and eliminates the need for multiple tools.
ThingsDB excels at scaling to ensure high availability, keeping your application running smoothly even under load. However, for specialized data like large logs or time-series data, dedicated databases might be more suitable. In such cases, ThingsDB's strength lies in its modular connectivity. You can connect to various external databases using pre-built or custom modules, giving you fine-grained control over data storage and retrieval, ensuring the right tool for the right job.
        In contrast to languages like C/C++, Go, and Rust, which require compilation before execution, ThingsDB utilizes an interpreter much like, for example, Python. However, ThingsDB distinguishes itself from other interpreted languages by maintaining state persistence, ensuring uninterrupted data access and manipulation even after script termination.
To better grasp this concept, consider what transpires when Python is launched.
$python…>>>value = 'Remember me'
Upon assigning the string "Remember me" to the variable
        value, Python retains this
        value as long as the interpreter remains open. To verify this, you can execute the
        following code snippet:
>>>print(value)Remember me
However, if we terminate the interpreter and relaunch Python, our
        value variable will be permanently lost:
>>>exit()$python…>>>print(value)Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name value is not defined
We encounter an error message indicating that the variable value is undefined. To
        retain this value, a programming language like Python must resort to external
        mechanisms such as writing the value to a file and reading from a file or
        utilizing a database connection.
This is where ThingsDB emerges as a solution. Similar to Python, ThingsDB is a programming language with a rich feature set encompassing task scheduling, type definitions, procedures, pub/sub system, and much more. Before delving into the technical details, let's examine how the aforementioned example would be implemented using ThingsDB.
The following code shows how to assign and store a variable:
(//stuff)>.value = 'Remember me';"Remember me"
Notice the dot (.) in front of .value,
        this dot is important because it tells
        ThingsDB to create a property to our collection object. The property in this
        example is value and the actual value of the property is a
        string "Remember me". It is important to understand that
        without the dot (.) in front of value, we
        would have created a variable, just like we did in Python.
We also finished our statement with a semicolon (;).
        Although ThingsDB can be forgiving when you forget the semicolon at the end
        (in fact, the code above would yield the same result when omitting the semicolon)
        you should always end your statements with a semicolon to prevent unwanted behavior.
In the example we also used a single space before and after the equal
        sign (=). This white space is ignored by ThingsDB and could
        be left out, or multiple spaces or even tabs and newlines would not make any difference.
        The code below, for example, doesn't look very appealing, but is just as valid as the
        example above.
(//stuff)>. value = 'Remember me' ;"Remember me"
        Now that we have assigned the property value to the root of the collection, exit the prompt by pressing CTRL+d.
Stop ThingsDB (Choose the method that matches your setup):
docker-compose.yml file and run docker compose stop.Now start ThingsDB again and also start the prompt again.
        (//stuff)>.value;"Remember me"
As you can see, ThingsDB still knows about the property value.
        You might also notice that however this example is very simple, the syntax is more
        comparable with programming languages than it is with something like SQL.
You might also wonder how the client is able to perform the query. The protocol for
        communication with ThingsDB is done with MessagePack. ThingsDB follows the same
        rules as MessagePack. Strings are assumed to be UTF8. This, however, is not guaranteed
        by ThingsDB but depends on if the MessagePack protocol is correctly followed. Usually,
        when working with a trusted client, this is the case. If you are not sure about this,
        ThingsDB has a function is_utf8() to test if a string
        really is UTF8. It also can define this on a type but we shall explore types later.
        Besides MessagePack, ThingsDB has support for JSON but this is strictly used for
        the HTTP API.
In the code sample below we verify that .value contains a
        string with valid UTF-8 encoding:
(//stuff)>is_utf8(.value);true
If we want to know which type .value was, we could use
        the type() function.
        This function always returns a string with type information.
(//stuff)>type(.value);"str"
In ThingsDB it is allowed to use statements as arguments. The code below shows that
        the is_utf8() function returns with a boolean value:
(//stuff)>type(is_utf8(.value));"bool"
Where is the property "value" stored?
As we mentioned earlier, the value has been assigned to the
            root object. We refer to these objects as "things", which explains the name ThingsDB.
            Every collection has a root which is a regular thing. It is not possible to remove the root,
            unless you choose to remove the complete collection. Every stored thing also receives a
            unique ID from ThingsDB. To see which ID our collection has, you can use the
            id() method.
(//stuff)>.id();1
The initial stuff collection most likely has ID 1, but it might be different on your computer
            depending on the version of ThingsDB used when the collection was created. We also start the
            method id() with a dot (.) in front of it.
            That is because id() is a method of a thing object.
ThingsDB enables the placement of code within code blocks, which are enclosed by curly braces
            {} and must contain at least one statement. It is crucial to note
            that code blocks themselves are interpreted as statements, and, consequently, must be terminated
            with a semicolon.
(//stuff)>{ .value = 'Remember me'; };// this semicolon ends the "block" statement"Remember me"
Code blocks enable the grouping of statements, which can be particularly beneficial when employed
            in conjunction with conditional statements (if-statements), iteration
            constructs (for-loops), or closures, all of which are detailed in
            subsequent chapters.
Just as we did in Python, we can also assign a value to a variable.
(//stuff)>value = "I'm a variable";"I'm a variable"
Without the dot, this is just a variable. Variables only exist within a single query.
            Therefore we cannot ask for value in the next query. If we did,
            ThingsDB would return with a LookupError.
(//stuff)>value;LookupError: variable `value` is undefined
In this example we used value as the name of our variable. Note that this can be named as you wish,
            for example, x, y, Foo,
            myValue and my_value are all good examples of
            variable names. Variables, like almost everything in ThingsDB, are case sensitive. This means
            that VALUE is not the same as value and they
            both can exist as different variables. A variable must start with a charter of a-z (or capital A-Z) or
            an underscore (_) and may be followed by the same or a number. The variable is not allowed to start
            with a number.
Here are some examples:
dog = nil;// validDog = nil;// valid_dog = nil;// validcat-or-dog = nil;// invalid – no hyphen (-) allowed in a variable namecatOrDog = nil;// validcat_or_dog = nil;// valid2dogs = nil;// invalid – a variable name must not start with a number
The code above also includes comments, which are denoted by the // prefix.
            Everything behind the // on a line will be disregarded by ThingsDB.
            An alternative comment syntax using /* and */
            allows for comments spanning multiple lines. However, in this book, we'll primarily utilize
            the // syntax, as it is less prone to errors and avoids the risk of
            forgetting to close the comment section.
Properties, on the other hand, have different naming rules. Almost anything is possible except for a
            few reserved single character names. For example, we can't use # as this
            is a reserved property name and is being used by ThingsDB to expose the ID. Keep in mind that it is
            possible to create a property which starts with a reserved character. For example, the
            property "# 007" is valid and can be used but if we wanted to assign this
            property, we can't just write it like before as this syntax is invalid.
(//stuff)>.# 007 = "James Bond";SyntaxError: error at line 1, position 1, unexpected character `#`
Instead, we can use the method set(key, value) which is available on every
            object of type thing.
(//stuff)>.set("# 007", "James Bond");"James Bond"
If we need to read the property, we require the get(key) method.
(//stuff)>.get("# 007");"James Bond"
The get() method is also useful for when you are not sure a property exists.
            By default, the return value is nil for when a property is not found.
(//stuff)>.get("I do not exist");null
Did you notice I mentioned nil? Well, the output shows null,
            so let's clarify the situation. The reason for seeing null is that our prompt is
            written in Python, where "nil" is called "None". However, when the output is converted to JSON format for
            display in the console, JSON uses null to represent the None
            type. Be mindful of this when translating between different programming languages.
If we wanted an alternative value we could provide the get() method with an
            alternative value.
(//stuff)>.get("I do not exist", "but I do");"but I do"
Arguments to build-in methods and functions in ThingsDB are lazy evaluated. Lazy arguments evaluation
            means that the code which is used as the argument is not executed in case the argument is not being used.
            In case of the get() method this means that we are allowed to raise an error
            as the second argument which will only be executed in case the property is not found.
(//stuff)>.get("foo", raise(lookup_err()));LookupError: requested resource not found
The error is raised because the property "foo" does not exist. If we would use
            the same code with a property which does exist, no exception would be raised. 
(//stuff)>.get("# 007", raise(lookup_err()));"James Bond"
We don't go into detail for the code to raise an error. For now it is enough to see lazy evaluation of method and function arguments at work.
Up to this point, the code snippets presented for each query involved single statements, and the response
            reflected the output of that statement. However, query code in ThingsDB can encompass as many statements
            as needed. The query response always encapsulates the outcome of the final executed statement. Typically,
            this refers to the last statement in the provided code, unless an exception is triggered or
            the return keyword is explicitly utilized.
Consider these examples:
(//stuff)>// press CTRL+n for a new line1 / 0;"Hello!";ZeroDivisionError: division or modulo by zero
In this instance, the division by zero raises an exception, and the subsequent
            statement, "Hello!", is not executed. The response, therefore, reflects the
            error message generated by the exception.
            On the other hand, if the return keyword is explicitly used to terminate
            the query, the response will be the value specified by the return statement:
(//stuff)>return 123;"Hi!";123
Here, the return statement halts the execution of the query and delivers the
            value 123 as the response, effectively skipping the subsequent
            statement, "Hi!".
In summary, the response of a ThingsDB query mirrors the outcome of the final executed statement,
            unless an exception arises or the return keyword is explicitly employed.
            This flexibility allows for more complex and versatile query operations.
Before we dive deeper into the ThingsDB language, we must first explain more about scopes. The example above uses the "stuff" collection, which is automatically created on a new ThingsDB initialization. As mentioned before, a collection is the place where data is stored. If you like to compare with SQL then you can think of a collection like a database. In SQL, you may have multiple databases running on a single SQL instance and in ThingsDB you can have multiple collections.
ThingsDB creates a unique scope for each collection. The full scope name for the "stuff" collection
            is /collection/stuff. You can shorten the prefix "collection" or even omit
            it altogether, so /c/stuff and //stuff are considered
            equivalent scopes. Additionally, you can use the "@" symbol to represent a scope. This syntax replaces
            the first "/" with "@" and the second with a semicolon, resulting in @collection:stuff
            or @:stuff for short.
Management of collections happens within the /thingsdb scope. ThingsDB allows
            you to abbreviate this scope name as much as you like, so you might commonly see the shortened
            version /t being used. Similarly to the collection scope example, you can
            also use the "@" symbol. So, /thingsdb, @thingsdb,
            /t, and @t all refer to the same scope.
            For switching within the prompt, we must use the @ syntax since otherwise the prompt does not understand that we want to switch from scope. This is because the "@" character is not part of the ThingsDB language and thus can be recognized by the prompt as a different action.
            Let's switch the prompt to the @thingsdb scope:
(//stuff)>@t(@t)>
We see that we switched to another scope. The @thingsdb scope is not able to store
            regular data as it contains no root object (thing) to attach properties to.
(@t)>.cat = 'Leo';LookupError: the `root` of the `@thingsdb` scope is inaccessible; you might want to query a `@collection` scope?
Instead, we use the @thingsdb scope to manage users, access rights, collections,
            modules, nodes and more.
For now we do not go into details but we create a new empty collection using
            the new_collection() function which is available in
            the @thingsdb scope.
(@t)>new_collection('chapter2');"chapter2"
The return value is the name for the newly created collection. While internally ThingsDB assigns a unique ID to the collection for communication purposes, it is the name that you will use for interactions with the collection. In the next chapter we shall use the collection we have just created.
How is ThingsDB different from other databases and programming languages?
What is the purpose of the --init argument when starting a ThingsDB node?
The provided code contains a mistake. Can you identify and correct it?
{a = 1;} b = 2;
Suppose we have the following code:
a = 1; .b = 2;
Which sentence is correct?
a and b are variablea and b are properties of the collection roota is a property of the collection root and b is a variablea is a variable and b is a property of the collection rootWhich of the following are valid variable names?
4you_Colormy-hobbyPMode_1What is the value of property x after executing the following code?
.x = 1; .get("x", .x = 2);
What comprises the response generated by a query?
Is it possible to create a property user with the value "Alice" while using the @thingsdb scope?
Which scope can be used to execute the following code? (multiple answers are possible)
new_collection("book");
/t@:t//thingsdb@things/nodeThingsDB is different in that it uses a distributed interpreter which preserves its state making it a programming language with database capabilities.
The --init argument is essential when initiating ThingsDB for the first time. It establishes the data path and creates a foundational collection. Subsequent invocations of ThingsDB with the --init argument will be disregarded unless accompanied by the --force argument to override existing configurations.
While the code block is not strictly necessary, it is missing a crucial semicolon to terminate the code block statement. As a result, running this code would generate an error message: ..unexpected character `b`, expecting: ; or end_of_statement. To rectify the issue, either add a semicolon after the code block or remove the code block entirely.
Answer "d" is correct. In this code snippet, a is a variable, and .b is a property of the collection root. The equivalent code would be root().b;, which explicitly references the root collection and then accesses its b property.
The variable names "b", "d" and "e" are valid. A variable name cannot start with a number making 4you invalid. A variable name may only consist of the letters a-z, A-Z, underscores (_) and digits 0-9 (except for the first character which is not allowed to be a digit). Therefore, my-hobby is invalid as it contains the "-" character.
The value of property x is 1. The get() method effectively retrieves the value of property x, which is 1. It optionally takes a second argument to provide a fallback value in case the property does not exist. In this instance, since the property x is found, the second argument is never evaluated, demonstrating the concept of "lazy evaluation" in action.
The response of a query encapsulates the outcome of the final statement executed within the query. Typically, this refers to the last statement within the provided code, unless an exception arises or the return keyword is explicitly employed.
No. The @thingsdb scope is specifically designed for managing your ThingsDB cluster, not for storing data. It allows you to handle administrative tasks such as user access control, collection creation, node management, and more. While you can create procedures within the @thingsdb scope, it does not provide a root thing to store properties. Therefore, you must use a collection scope to store data.
Both "a" and "d" are correct. New collections can be created in the @thingsdb scope and both /t and @thing are valid abbreviations. Both "b" and "c" are collection scopes and "e" is an example of a node scope.
For this chapter we switch to the newly created collection (see the previous chapter on how to create the collection).
(@t)>@ //chapter2(//chapter2)>
        In ThingsDB integers, strings, boolean and floating point values are all immutable. That is, you cannot change an
            immutable value once it is created. An integer is a number without a fractional component. Examples
            are 1, 2, 415 but
            also 0 and negative integers like -15. As we explained,
            integer values are immutable which means that we cannot change the value once assigned.
For example, if we assign the integer to the variable x, we cannot change the value.
(//chapter2)>x = 42;// x will be immutable42
We cannot change the value as x is immutable, but we can assign a new value to x.
(//chapter2)>// press CTRL+n for a new linex = 2;x = x + 1;// overwrite x with a new integer value3
The return value is indeed the last assignment.
Aside from the basic assignment operator (=), ThingsDB supports other assignment
            operations that allow you to modify values in more complex ways. For instance, you can use the addition
            assignment operator (+=) to add a value to another and assign the result to a variable.
            For example:
(//chapter2)>// press CTRL+n for a new linex = 2;x += 1;// read as: x = x + 13
Table 2.1 - Assignment operators
| Operator | Description | 
|---|---|
= | Assignment operator | 
*= | Multiplication assignment | 
/= | Float division assignment | 
%= | Modulo assignment | 
+= | Addition assignment | 
-= | Subtraction assignment | 
&= | Bitwise AND assignment | 
^= | Bitwise XOR assignment | 
|= | Bitwise OR assignment | 
The range of integers is limited to a minimum and maximum value. You can ask this minimum and maximum value
            with the keywords INT_MIN and INT_MAX.
(//chapter2)>INT_MIN;-9223372036854775808(//chapter2)>INT_MAX;9223372036854775807
When you try to create an integer value outside this range, an Overflow error is raised as ThingsDB cannot store this value.
(//chapter2)>INT_MAX + 1;// this will errorOverflowError: integer overflow
In addition to the familiar base-10 notation, integers can also be represented using hexadecimal (hex), octal (oct), and binary notation. Each of these notations has its own unique set of symbols and prefixes that distinguish it from the others.
Hexadecimal notation (hex) uses the digits 0-9 and the letters A-F to represent values from 0 to 15.
            Hex numbers are prefixed with the identifier "0x" to indicate their hexadecimal nature. For instance,
            the number 0xFF represents the decimal value 255.
Octal notation (oct) employs the digits 0-7 to represent values from 0 to 7. Octal numbers are prefixed with
            the identifier "0o" to distinguish them. For example, the
            number 0o77 stands for the decimal value 63.
Binary notation (bin) utilizes only the digits 0 and 1 to represent values from 0 to 1. Binary numbers are
            prefixed with the identifier "0b". For example, the
            number 0b1010 equals the decimal value 10.
Here are a few examples:
0xff;// hex notation (255)0x1F;// hex notation (31) - capital letters are allowed0X10;// !! error !! - the "x" must be lowercase123;// decimal notation0o77;// octal notation (63) - the "o" must be lowercase0b1010;// binary notation (10) - the "b" must be lowercase
Assignments work from right to left. The example below first assigns 30 to
            apples, then apples is assigned to grapes
            and finally grapes is assigned to lemons.
(//chapter2)>lemons = grapes = apples = 30;30
Floating-point numbers are used to represent numbers with decimal parts, such as 0.5
            and 3.14159. ThingsDB supports two notations for representing floating-point
            numbers: standard decimal point notation and scientific notation (E notation).
Standard decimal point notation is commonly used for representing everyday numbers, such as 1.0,
            56.0, and 0.5. The E notation is more compact and is
            typically used for representing very large or very small numbers. For example, the number 1.5e+3,
            written in E notation, represents the same value as 1.5 * 1000. Similarly,
            1.2e-4, written in E notation, represents the same value as 1.2 * 0.0001.
ThingsDB adheres to strict rules for writing E notation. The "e" must always be lowercase, and the sign (+ or -) is required to indicate whether to multiply by 10 to the positive or negative power.
Due to the limited precision of 64-bit floating-point numbers in ThingsDB, some decimal values may not be
            represented accurately. For instance, the result of dividing 10.0 by
            3.0 is displayed as 3.3333333333333335, even though the
            actual decimal representation extends infinitely. This is because the 64-bit format can only store a finite number
            of decimal digits.
(//chapter2)>10.0 / 3.0;// the single / means division3.3333333333333335
ThingsDB follows the standard floating-point behavior when performing arithmetic operations. If either operand is
            a float, the result will also be a float. However, if both operands are integers, the result will be an integer
            and will be rounded towards the nearest integer towards zero. For example, 10 / 3 will
            be 3, and 10 / -3 will be -3.
            This is because integer division in ThingsDB truncates the fractional part, discarding any decimal places.
            Two special floating-point values exist in ThingsDB: inf (infinity) and nan
            (NaN, not a number). Infinity represents an infinitely large number, while NaN represents a value that cannot be
            represented accurately. In other programming languages, nan may be the result of operations
            like 0/0, but in ThingsDB, such operations will throw a division by zero error.
In ThingsDB, you will encounter nan only when a value is explicitly set to nan
            or when incoming arguments contain nan. One important distinction to remember is that
            comparing nan values to other values will always yield false results. Instead, use
            the is_nan() function to explicitly check whether a value is nan.
(//chapter2)>nan == nan;// this is false!!false(//chapter2)>is_nan(nan);// use is_nan() to test for NaNtrue
In addition to the arithmetic operators, ThingsDB provides a set of handy mathematical functions for performing
            calculations on numbers. These functions typically accept either a float or integer as input and return the
            calculated result. As an example, the sqrt() function calculates the square root
            of a given number. Here are a few examples:
(//chapter2)>sqrt(9);// Square root of 93.0(//chapter2)>pow(5, 3);// 5 raised to the power of 3125.0(//chapter2)>round(loge(5.12), 3);// Combine round() and loge()1.633(//chapter2)>ceil(6.1034);// Ceil of a given number7
For a comprehensive list of all available mathematical functions, refer to the official ThingsDB documentation: https://docs.thingsdb.io/v1/collection-api/math/
The bool type, named after George Boole, is used to represent truth values. ThingsDB automatically converts bool
            values to integers when needed. This conversion follows the standard convention of representing
            true as 1 and false as 0.
            For instance, adding two true values would result in 2.
            Similarly, subtracting two false values would result in 0.
            Consider this example:
(//chapter2)>true + true;2
In this example, the expression true + true is evaluated to 2,
            even though both operands are bool values.
In ThingsDB, you can also convert other data types to the bool type. For example, an integer value
            of 0 converts to false, and any other integer value
            converts to true. This applies to other data types as well, such as floats and
            strings. An empty string converts to false, while a string with any content, even
            whitespace, converts to true.
Here is an example demonstrating this:
(//chapter2)>bool(0.0);// float 0.0 (false)false(//chapter2)>bool("");// empty string (false)false(//chapter2)>bool(-4.13);// non-zero float (true)true(//chapter2)>bool("Hello");// non-empty string (true)true
The conversion rules for other data types are described later in this book, but the important thing to remember is that any type can be converted to the bool type.
Occasionally, you might encounter code that employs two exclamation marks (!!)
            instead of the bool() function. The single exclamation mark serves as the "not"
            operator, which first converts the value following it to a boolean and then flips that value.
             When a second exclamation mark is used, the inverted value is inverted once more.
             As you can see, this ultimately yields the same result as using the bool() function.
(//chapter2)>!!"Cool";// non-empty string (true)true
This usage serves as a concise alternative to explicitly calling the bool() function.
Strings are used to store textual data and are typically encoded using UTF-8. Whether the encoding is indeed UTF-8 depends on whether the MessagePack protocol is followed during communication with ThingsDB. Strings are sequences of characters and can be indexed, meaning they have a defined length.
(//chapter2)>.b = "bike";"bike"(//chapter2)>.b.len();// len() is a method of type "str"4
As demonstrated, strings are objects with associated methods. The len() method is an example
            that can be used to determine the length of a string.
(//chapter2)>.b[0];// the first byte is at index 0"b"
Indexing for strings begins at position 0. Negative indexing is also supported.
(//chapter2)>.b[-1];// last byte, read as: .b[.b.len()-1]"e"(//chapter2)>.b[.b.len() - 1];// last byte, the hard way"e"
Notice that any expression can be used for indexing strings. ThingsDB supports this level of flexibility.
Besides indexing, strings (and other sequences) support slicing, which allows you to extract a specific segment from the sequence. Slicing involves specifying a starting position (inclusive), an ending position (exclusive), and an optional step size.
(//chapter2)>.b[1:3];// slice from position 1 up to,// but not including, position 3"ik"
Slicing operations create a new string, leaving the original string unchanged. This is because strings in ThingsDB are immutable, meaning they cannot be directly modified.
The general format for a slice is [start:end:step], where start
            defaults to 0, end defaults to the length of the string,
            and step defaults to 1.
(//chapter2)>.b[1:];// equivalent to .b[1:.b.len()]"ike"(//chapter2)>.b[:-2];// equivalent to .b[0:.b.len()-2]"bi"(//chapter2)>.b[:];// equivalent to .b[0:.b.len()]"bike"
The step parameter enables you to select specific elements of the sequence by skipping over certain values. Negative step values can also be used to begin from the end of the sequence.
(//chapter2)>.b[::2];// slice with an even step size,// extracting only even-indexed bytes"bk"(//chapter2)>.b[::-1];// slice in reverse order"ekib"
In prior discussions, we have often used the term "bytes" instead of "characters" when referring to strings. This is because the length of a string in ThingsDB is measured in bytes, not characters. This can be important to keep in mind, especially when dealing with UTF-8 encoded strings, which can use multiple bytes to represent a single character.
(//chapter2)>.u = "Hi! 😁";// UTF-8 string with smiley"Hi! \ud83d\ude01"(//chapter2)>.u.len();// Length of the string in bytes8(//chapter2)>is_utf8(.u);// Check if UTF-8 encodedtrue
In this example, the string "Hi! 😁" has a length of 8 bytes, even though it contains
            only 5 characters. This is because the Unicode character for the smiley face, 😁, requires 4 bytes to represent.
            When working with strings in ThingsDB, it is crucial to remember that strings are internally treated as sequences
            of bytes, not characters.
To further illustrate the distinction between bytes and characters, consider this example:
(//chapter2)>.u[-3:];// don't try this(!! your client will likely crash or at least display an unpacking error)
This code attempts to index the string .u starting from the third-last character.
            However, since UTF-8 encoded characters can span multiple bytes, attempting to index directly by character
            position can lead to unexpected behavior, as demonstrated by the potential client error.
On the other hand, ThingsDB internally handles strings as sequences of bytes. Therefore, while this specific indexing operation might cause issues on the client-side, ThingsDB itself can still process the string correctly. This is evident from the following command:
(//chapter2)>is_utf8(.u[-3:]);// No problem for ThingsDBfalse
This command confirms that ThingsDB can still determine that the extracted substring is not valid UTF-8 even though it is accessed using an invalid character index. This demonstrates that ThingsDB handles strings consistently as sequences of bytes, regardless of the specific indexing operations performed.
We have already explored the len() method for determining the length of a string.
                However, ThingsDB offers a wealth of additional methods for string manipulation like, for example,
                the upper() method.
(//chapter2)>.b.upper();// Produces a new string in uppercase"BIKE"
The upper() method generates a new string with all characters converted to
                uppercase and exemplifies the various methods available for the string type. Other methods, such
                as starts_with(), accept arguments and can be used to determine if a given
                string begins with another string.
(//chapter2)>"this is a test".starts_with("this is");true(//chapter2)>"another test".starts_with("Ano");false
To explore a comprehensive list of all the methods available for the string type,
                including upper() and starts_with(),
                refer to the official ThingsDB documentation here:
                https://docs.thingsdb.io/v1/data-types/str/
Until now, we have primarily employed double quotes to delimit strings. However, what if we want to include double quotes within the string itself? In this situation, we have two options. The simplest approach is to enclose the string in single quotes, which ThingsDB also supports:
(//chapter2)>'"This is an example", she said';"\"This is an example\", she said"
                Alternatively, we can escape double quotes by placing another double quote directly before them:
(//chapter2)>"""This is as well"", he said";"\"This is as well\", he said"
This escaping technique also applies to single quotes within single-quoted strings.
In ThingsDB, all strings are inherently multiline. Simply utilize newline characters to introduce line breaks within a string.
(//chapter2)>"All strings in ThingsDB are multiline!!";"All\nstrings in ThingsDB\nare multiline!!"
As before, the response is presented as JSON format, and therefore the newline characters are
                displayed as \n.
String concatenation can be accomplished using the + operator.
(//chapter2)>"My " + .b + " is black";"My bike is black"
Take note that this does not work with other data types unless we explicitly convert them to strings:
(//chapter2)>// CTRL-n for a new line.n = 3;// Assign integer 3 to property "n""I've " + .n + " apples";TypeError: `+` not supported between `str` and `int`
                To rectify the example above, we can utilize the str() method to convert
                the integer to a string:
(//chapter2)>"I've " + str(.n) + " apples";"I've 3 apples"
In practice, it is advisable to avoid string concatenation. Instead, opting for t-strings is a
                superior choice. A t-string starts and ends with a backtick (`) and
                enables us to embed variables or even entire expressions within curly braces.
(//chapter2)>`I've {.n} apples`;"I've 3 apples"
This approach does not only enhance readability but also eliminates the risk of forgetting to convert to a string, as the conversion is done automatically when using the t-string syntax.
If you need to include curly braces or backticks within a t-string, you can follow the same escaping method used for single and double-quoted strings. To escape a curly brace, use two consecutive curly braces, and for a backtick, use another backtick.
(//chapter2)>`Sentence with ``Backticks`` and {{Curlies}}.`;"Sentence with `Backticks` and {Curlies}."
This approach ensures that the braces and backticks are interpreted as part of the string and not as special characters.
We've encountered
            nil before as the return value of functions, methods or statements that do not produce
            any meaningful output. This concept extends to queries where we don't expect a substantial response.
Consider the following:
(//chapter2)>.quote = "'So many books, so little time.' -- Frank Zappa";"'So many books, so little time.' -- Frank Zappa"
This query assigns a quote but also sends the quote back to the client, consuming unnecessary network bandwidth.
            Since we have already stored the quote, we do not need it echoed back in the response. In such cases,
            it is considered a best practice to terminate the code with the nil type to prevent this unnecessary response:
(//chapter2)>.quote = "'So many books, so little time.' -- Frank Zappa";nil;null
By appending nil, we avoid sending the quote back to the client, minimizing network
            overhead. Explicitly ending a query with nil also enhances the code's readability and intent. It clearly
            indicates that the query does not expect a response, making the code more self-explanatory and easier to follow.
The nil data type often serves as a placeholder for empty values. While this
                can be convenient, it is crucial to avoid relying solely on the fact that nil
                evaluates to false. This is because other data types, such as an integer
                value of 0 or an empty string, also evaluate to false.
                To accurately identify nil, use the dedicated is_nil()
                function. This ensures that you are explicitly checking for the absence of a value rather than relying on
                potentially misleading behavior.
Before delving into the quiz for this chapter, let's explore the error data type. Throughout this chapter, we have encountered a few errors in our queries. While errors themselves are normal types in ThingsDB, raising an error interrupts the query execution and triggers the dedicated error handling protocol.
Consider the zero_div_err, which represents the error raised when attempting to
            divide by zero. We can create an instance of this error type and access its properties:
(//chapter2)>zero_div_err().msg();"division or modulo by zero"(//chapter2)>zero_div_err().code();-58
In this example, we only returned the error information without raising it. However, we can explicitly
                raise an error using the raise() function:
(//chapter2)>raise(zero_div_err());ZeroDivisionError: division or modulo by zero
The client, in this case the ThingsDB Prompt, recognizes the error and returns a corresponding exception:
                ZeroDivisionError.
The default message can be overwritten. Here is an example of the zero_div_err
                error with a customized message:
(//chapter2)>zero_div_err("division by zero is not allowed");"division by zero is not allowed"
As you can see, the default message is now replaced with your custom message. Importantly, when an error
                is returned (without being raised), it is sent to the client as a string containing the message
                (equal to explicitly calling the msg() method).
ThingsDB also allows you to define custom error codes. These codes must fall within a specific range: -127 to -50.
                Here is an example of raising a custom error with a code:
(//chapter2)>raise(err(-100, "My Custom Error"));CustomError: My Custom Error
It is worth noting that -100 is the default code for custom errors. You can
                achieve the same result by raising the message directly:
(//chapter2)>raise("My Custom Error");CustomError: My Custom Error
Unlike traditional try-catch blocks found in many languages, ThingsDB utilizes a try()
                function for error handling. This function requires at least one argument, which is the code you want to execute.
                If an error arises within the code, try() returns with the error, preventing
                disruption of the entire query execution.
Let's see this in action:
(//chapter2)>a = 7.0; b = 0.0;x = try(a / b);// try dividing a with b`{a} / {b} = {is_err(x) ? 'NaN' : x}`;"7 / 0 = NaN"
Dividing by zero triggers an error, which is captured by the try() function.
                Use the is_err() function to check if the returned value from try()
                contains an error.
While try() captures all errors by default, you can refine its behavior to
                handle only specific errors. To achieve this, provide the desired error types as additional arguments to
                the try() function.
(//chapter2)>try(1 / 0, zero_div_err()); "OK";"OK"
This code only captures the zero_div_err error, any other errors would
                still be raised.
This chapter covered some fundamental types of ThingsDB, including the concept of error handling. Test your understanding with the quiz and join us in the next chapter where we delve into lists and tuples.
What data type will be the result of adding an integer to a float?
If you have a value of 0.0, what will be the result of putting two exclamation marks in front of it?
Can you guess the result of evaluating the following expression?
sqrt(25);
Is it possible to change the string "alarm" to "ALARM" by converting all characters to uppercase?
Choose the correct answer:
What string is created by the following code:
"slicing"[2:5]
Can you guess which string is produced by the following code without executing it?
"blue blue grass, blue blue sky".replace("blue", "green", 2);
(Hint: consult the online documentation for the replace method)
When and why is it considered a good practice to end a query with nil?
What happens when executing the following statement in ThingsDB?
try({2 / 0; .A = true;}); .B = true;
.A nor .B will be set because the division by zero raises an error.A will be set because the assignment happens within the try() block.B will be set because try() catches the error and execution continues.A and .B will be set to trueThe result will be a float. When one operand is a float, the other operand is internally promoted to a float and the result will also be a float.
The exclamation mark before the value 0.0 converts the value to a boolean (false) and then flips the value (true). The second exclamation mark inverts the result again, so the final result is false.
The result of evaluating the expression sqrt(25); is 5.0. While you might initially think of the answer as simply 5, remember that the sqrt() function always returns a float, even when the input is an integer.
No, strings in ThingsDB are immutable, meaning they cannot be changed after they are created. You can create a new string with uppercase characters by using the upper() method.
The correct answer is "c", strings are internally represented as sequences of bytes. A string should be encoded in UTF-8 but this is not guaranteed. Strings can be defined across multiple lines and an empty string evaluates to false when converted to bool, but is still a valid string.
The string "ici". Indexing starts at zero, so index 2 corresponds to the third character. The slicing operation results in a substring consisting of the characters from index 2 (inclusive) to index 5 (exclusive).
The resulting string is "green green grass, blue blue sky". The replace() method can be employed to replace a specified sequence of characters within a string with another string. Optionally, you can specify the number of instances to be replaced. Had the optional argument not been set to 2, the result would have been "green green grass, green green sky".
(The replace() method offers more capabilities than described here. Consult the documentation for a comprehensive overview of its power: https://docs.thingsdb.io/v1/data-types/str/replace/).
When you execute a query that does not produce a meaningful response. It prevents unnecessary transmission of data back to the client and improves clarity and readability as it clearly indicates that the query does not expect a response.
The answer is "c". Only .B is set to true. The division by zero in try() halts its execution, preventing the assignment to .A. Execution continues outside try(), allowing the assignment to .B.
Before we proceed, let's create a new collection named "chapter3" and switch to its scope. If you need a refresher on creating new collections, refer back to section 1.6 Scopes.
This chapter delves into the realm of lists and tuples, exploring their nature as array-like data structures with a crucial distinction: lists are mutable, allowing their contents to be modified, while tuples are immutable, preserving their values once created. Additionally, we'll uncover the concept of closures, unnamed functions that capture their surrounding scope, and examine their role when used as arguments.
Lists are positionally ordered collections of arbitrarily typed objects, and they have no fixed size. They are also mutable. Unlike strings, lists can be modified in-place by assignment to offsets and by a variety of list method calls.
(//chapter3)>.L = [true, "Eggs", 0.1];[ true, "Eggs", 0.1 ]
Similar to strings, you can get the length of a list and use indexing and slicing:
(//chapter3)>.L.len();// Length of the list3(//chapter3)>.L[1];// Access the second item at index 1"Eggs"(//chapter3)>.L[-2:];// Get the last 2 items[ "Eggs", 0.1 ]
Since lists are mutable, you can modify their contents directly:
(//chapter3)>.L[0] = false;// Set the first item to falsefalse(//chapter3)>.L[1:1] = ["Ham", "&"];// Insert "Ham" and "&" at index 1null(//chapter3)>.L;[ false, "Ham", "&", "Eggs", 0.1 ]
More common is to use one of the list methods to update the list.
(//chapter3)>.L.shift();// Remove the first itemfalse(//chapter3)>.L.pop();// Remove the last item0.1(//chapter3)>.L.unshift("The", "best");// Insert "The" and "Best" at the// beginning5(//chapter3)>.L.push("recipes");// Append "recipes" to the end6(//chapter3)>.L.extend(["big", "world"]);// Append another list8(//chapter3)>.L.splice(6, 1, "in", "the");// Remove 1 item at index 6 and// replace it with "in" & "the"[ "big" ]
The return value of each method varies depending on its functionality. In general, update methods return the
            removed values or the new list length if they only add items. Among the methods discussed,
            splice() stands out for its versatility. It can remove multiple items, insert
            new ones, and even return the removed items in a separate list.
If you have followed the examples above in the correct order, your list should now contain only strings.
            You can concatenate these strings into a single string using the join() method:
(//chapter3)>.L.join(" ");// Join using a space"The best Ham & Eggs recipes in the world"
While lists in ThingsDB do not have a fixed size, accessing an index beyond the list's bounds will still trigger a LookupError.
(//chapter3)>.L[99];LookupError: index out of range(//chapter3)>.L[99] = 1;LookupError: index out of range
Instead of silently expanding the list, ThingsDB raises a lookup_err. To extend a list, utilize methods like push or slice assignments,
                which were previously demonstrated.
While ThingsDB adheres to conventional list behavior in most cases, it employs a unique approach when lists are embedded within a thing.
When lists are assigned to variables, ThingsDB maintains a single reference to the underlying data structure. This means that modifying the list through one reference will also affect any other references to the same list.
(//chapter3)>A = ["Alice", "Bob"];// Assign a list to variable "A"B = A;// Assign "A" to "B"B.push("Charlie");// Append "Charlie" to the listA;// Return with the list using "A"[ "Alice", "Bob", "Charlie" ]
As we can observe, assigning A to B establishes
                a reference to the same underlying list data structure. This is evident from the fact that
                appending "Charlie" to B also modifies the list
                accessed through A. Both variables A and
                B effectively point to the same list entity.
However, when a list is assigned to a property, ThingsDB creates a distinct copy of the list. We refer to this type of list as a maintained list because it is stored and managed within a ThingsDB collection.
(//chapter3)>a = ["Alice", "Bob"];// Assign a list to variable "a".b = a;// Assign variable "a" to property ".b".b.push("Charlie");// Append "Charlie" to the list on "b"a;// Return the list assigned to variable "a"[ "Alice", "Bob" ]
In summary, ThingsDB creates references when assigning lists to variables and copies them when lists are assigned to properties of a thing. We refer to the latter lists as maintained lists to differentiate them from the referenced lists used in variables.
ThingsDB allows you to store and retrieve multidimensional data using arrays. However, when you nest arrays within an array, the nested arrays are converted to tuples. Tuples are immutable, meaning they cannot be changed after they are created. This is because ThingsDB cannot track and synchronize changes to nested lists.
(//chapter3)>.m = [ [1, 2, 3], [4, 5, 6], ];[[1, 2, 3], [4, 5, 6]]
This code creates a multidimensional array .m with two nested arrays.
            You can verify that .m is a list using the type()
            function:
(//chapter3)>type(.m);"list"
You can also extend the list by appending another array to it:
(//chapter3)>.m.push([7, 8, 9]);3
However, when you append a nested list to another list, the nested list is converted to a tuple. This means that you cannot modify the nested list after appending it to the outer list.
For example, the following code attempts to append an element to the last nested array:
(//chapter3)>.m.last().push(10);LookupError: type `tuple` has no function `push`
Similarly, you cannot modify the elements of a nested tuple using index assignments:
(//chapter3)>.m.last()[0] = 10;TypeError: type `tuple` does not support index assignments
Tuples have a subset of the methods available to lists. Only methods that do not manipulate the data structure are available for both tuples and lists.
If you assign a tuple to a variable, the variable holds a reference to the tuple. However,
            if you assign a tuple to a thing, it will be converted to a maintained list. This means that
            you can modify the elements of the new list after it has been assigned to a thing. For example,
            the following code assigns the first nested array from .m to a
            variable a:
(//chapter3)>a = .m.first();// Get the first item which is tuple [1, 2, 3]type(a);// Verify that this is still a tuple"tuple"
However, if you assign the tuple to a property on a thing, for example to .o, it will be converted
            to a maintained list:
(//chapter3)>.o = .m.first();[1, 2, 3]
The variable .o is now a list, and you can modify its elements:
(//chapter3)>.o[0] = 10;// This works!!10
Table 3.2 - Summary of when ThingsDB employs copying or reference assignment for a list
| Assignment | Description | 
|---|---|
a = b | By reference (b is a list) | 
a = .b | By reference (.b is a maintained list) | 
a = t | By reference (t is a tuple) | 
.a = b | Copy to new maintained list (b is a list) | 
.a = .b | Copy to new maintained list (.b is a maintained list) | 
.a = t | Copy to new maintained list (t is a tuple) | 
a.push([]) | Copy to new tuple (a is a list) | 
Prior lessons have covered accessing a list item using its index. However, we have yet to explore iterating through all list elements. To illustrate this concept, let's create a list:
(//chapter3)>.numbers = [3, 8, 9, 0, 2];[3, 8, 9, 0, 2]
Here is one approach to iterating through the list:
(//chapter3)>odd = [];for (n in .numbers) {if (n % 2) {odd.push(n);// If n is odd, append it to the list};};// Do not forget this semicolon!!odd;// Return the list[3, 9]
            The for-loop approach offers more flexibility. In addition to the value,
            they also provide the index of the value. A for-loop also supports two
            keywords: continue, which skips all code after continue and commences
            the next iteration of the loop, and break, which halts iteration.
(//chapter3)>even = [];for (n, i in .numbers) {if (n % 2) {continue;// Skip to the next iteration for odd numbers};even.push(`{n} at index {i}`);// Append a t-string to the listif (i == 3) {break;// If the index is 3, terminate the loop};};even;// Return the list[ "8 at index 1", "0 at index 3" ]
While for-loops offer the most flexibility, for many use cases, methods
            like filter(), map(), each()
            and reduce() provide a more concise and elegant approach to iterating over and
            manipulating data structures. Let's examine the filter() method to replace
            the first for-loop:
(//chapter3)>.numbers.filter(|n| n % 2);[3, 9]
The filter() method takes a closure as its first argument, which is essentially
            an unnamed function. The filter() method iterates over the list and executes the
            closure for each item. The arguments that the closure accepts are specified between the two pipe
            characters (||), and in the case of a list or tuple, the filter method provides
            both the value and index of the item to the callback function. You are not required to use both arguments,
            as demonstrated in our example.
As you can see, the filter() method with a closure provides a more concise and
            readable way to filter a list compared to a for-loop. It is a common pattern in
            functional programming, and it can significantly improve the readability and maintainability of your code.
The second for-loop, which utilized the continue and
            break keywords, seems more complex to replace by list methods. While the
            map() method can be helpful, it is not the ideal solution in this context.
            The map() method produces a new list of the same length as the original list,
            but each value in the list is generated by the callback function. In our example, we want to create a list
            of even numbers with an index smaller than 4. Combining the filter() and
            map() methods seems like a viable approach, but it introduces a new issue:
(//chapter3)>.numbers.filter(|n, i| n % 2 == 0 && i < 4).map(|n, i| `{n} at index {i}`);[ "8 at index 0", "0 at index 1" ]
As you can observe, the index values are not as expected. This is because the filter()
            method creates a new list, and the map() method operates on this new list instead
            of the original list. Consequently, the index values are no longer preserved.
To address this challenge, we can employ the reduce() method, which offers a
            functional approach to iterating over and manipulating data structures. The reduce()
            method takes a closure and an initial value as arguments. The closure is executed for each item in the list,
            and the initial value is the first argument in the callback function. For subsequent iterations, the argument
            passed to the callback function is the accumulated value from the previous iteration, and the result of the last
            iteration will be the result for the reduce method.
(//chapter3)>.numbers.reduce(|o, n, i| { if (n % 2 == 0 && i < 4) { o.push(`{n} at index {i}`); }; o; }, []);[ "8 at index 1", "0 at index 3" ]
Admittedly, this solution is more complex than the for-loop. However, it demonstrates
            the power and versatility of functional programming techniques.
Most challenges related to lists can be addressed by combining looping methods with methods for modifying lists. However, ThingsDB provides convenient shortcuts for handling common tasks. Here are a few examples:
(//chapter3)>.numbers.sum();// Calculates the sum of all values// in the list22(//chapter3)>.numbers.sort();// Sorts the list in ascending order// and returns a new list[0, 2, 3, 8, 9](//chapter3)>.numbers.is_unique();// Checks whether all values in// the list are uniquetrue(//chapter3)>.numbers.index_of(9);// Finds the index of the first// occurrence of the value 9 in// the list2
For a complete list of available methods, refer to the official documentation: https://docs.thingsdb.io/v1/data-types/list/
As ThingsDB evolves, new methods are added. Therefore, when encountering a problem, it is advisable to first consult the documentation to see if there is an existing method that can assist you. If you have a specific requirement, you can submit a pull request with your proposed method, and one of the ThingsDB developers may consider implementing it for future releases.
Within ThingsDB, "lists" offer a powerful way to retrieve multiple values from a single query.
Imagine you want to fetch both a status code and a corresponding message from your query. Using lists, you can achieve this elegantly by simply returning a list containing both desired values.
(//chapter3)>status = 0; message = "success";[status, message];// List containing both status code and message[ 0, "success" ]
By embracing lists for multi-value returns, you unlock a flexible and efficient approach to retrieving data in ThingsDB.
If we execute the following code:
a = [1, 2, 3];
.b = a;
What will happen?
.b contains a copy of aa and property .b reference the same listWhat will happen if we add a list A as a value into another list B?
Consider the following code:
a = [["foo", "bar"]];
b = a[0];
c = a;
Which answer is correct?
a is a tuple, b is a reference to a list and c is a maintained lista, b and c are all listsa is a list, b is a reference to a tuple and c is a maintained lista and c are the same list and b is a reference to a tupleCan you tell which technique ThingsDB supports for looping over a list or tuple?
each() methodmap() methodreduce() methodfilter() methodfor-loopWhat is the final result of the following code snippet?
range(2, 5).reduce(|t, n, i| t += i * n, 10);
(Try to answer without executing the code. For reference, the range() documentation is available at: https://docs.thingsdb.io/v1/collection-api/range/)
Suppose we have the following input:
input = [[1, 2], [3, 4]];
Can you write code which returns with [1, 2, 3, 4] from that given input?
Answer "a" is correct. When assigning a list to a property, a copy is created, resulting in a maintained list. Changes to the original list will not reflect in the maintained list.
A copy of list A will be created as a tuple. Since the nested tuple is immutable, you cannot modify its contents after creation.
Answer "d" is correct. The code does not contain a maintained list as only variables are used and nothing is assigned to a thing. Nested lists are always converted to tuples and are accessed by reference when assigned to a variable.
The correct answer is "f". ThingsDB supports all the listed techniques for iterating over lists and tuples. Each method serves a distinct purpose, and the for-loop proves particularly valuable when you need to utilize the break keyword to prematurely terminate the iteration.
The result is 21. The function call range(2, 5) produces the list [2, 3, 4] and the reduce method uses a closure which adds the index multiplied by the number to a total with a start value of 10. The result is 10 + 0*2 + 1*3 + 2*4 = 10 + 0 + 3 + 8 = 21.
While you could use a for-loop or the reduce() method to achieve this, ThingsDB provides a dedicated method called flat() that simplifies this process:
input.flat();
Let's start this chapter with a new collection named "chapter4" and switch to its scope. If you are unsure of the process, revisit the end of Chapter 1.6 for a refresher.
Why is this programming language named ThingsDB?
In ThingsDB, data is organized and stored within things, which are essentially objects with properties.
        Every collection starts with an empty root "thing". New "things" can be created
        by using the thing() function
        without arguments, or, more commonly, you can use curly braces {}:
(//chapter4)>t = {};// Create an empty thingt.x = 4;// Assign property "x" with value 4 to "t"t.y = 2;// Assign property "y" with value 2 to "t"t;// Return "t"{ "x": 4, "y": 2 }
This can be simplified by initializing the thing with the properties in curly braces:
(//chapter4)>t = {x: 4, y: 2,};{ "x": 4, "y": 2 }
This initialization syntax only works for properties that adhere to the ThingsDB naming convention, which is the same as the convention for variable names (see 1.3 Variables and Properties). The properties are defined as key-value pairs separated by commas. You can omit the comma after the last property, since ThingsDB will ignore it if no other key-value pairs follow.
        ThingsDB supports a shorthand notation to initialize a thing when using a variable and using the same variable name as the property name:
(//chapter4)>foo = 6;bar = 7;t = {foo:, bar:,};{ "bar": 7, "foo": 6 }
This syntax is equivalent to writing t = {foo: foo, bar: bar}; but it eliminates
        the need to repeat variable names.
While lists, as shown in Chapter 3.5, provide a simple way to return multiple values, things provide a more descriptive alternative. They allow assigning meaningful names to each value, enhancing clarity and reducing reliance on positional interpretation. This structure improves readability and understanding, but comes at a slight network bandwidth cost compared to lists.
Example:
(//chapter4)>status = 0; message = "success";{ status:, message:, };// Thing containing both status code and message{ "message": "success", "status": 0 }
            Once a thing is stored, meaning it is connected to the collection root, it is assigned a unique identifier or ID.
            Back in Chapter 1.1 we already saw that our root thing had an ID and that we
            could ask for this ID using the id() method.
(//chapter4)>.id();// ID for the collection root1(//chapter4)>{}.id();// Unattached thing lacks an IDnull(//chapter4)>(.t = {}).id();// Assign a new thing to property "t",// guaranteeing a unique ID.2
A thing possesses no ID as long as it remains unattached to the collection. In the last example, we attached a
            new thing to property t within the root and immediately queried its ID. The
            response displayed a unique identifier.
Instead of explicitly requesting the ID, it is also accessible as a reserved property
            named "#" within the thing's response.
(//chapter4)>.t;{ "#": 2 }
However, it is important to note that "#" is not a genuine property of the thing,
            as evidenced by the following code snippets:
(//chapter4)>.t["#"];// Key ‘#' does not existLookupError: thing `#2` has no property `#`(//chapter4)>.t.len();// The length is 00(//chapter4)>bool(.t);// Empty thing evaluates as false and .t is emptyfalse
As ThingDB implicitly assigns IDs to things, it is crucial to grasp when this occurs. While the earlier example might seem straightforward, it is essential to recognize its nuances.
The code below employs the assert() function, which triggers an exception
            if the provided expression evaluates to false. This, along with the
            is_nil() and is_int() functions,
            facilitates straightforward testing for ID existence.
(//chapter4)>arr = [];// Create an empty lista = {name: "Alice"};// Create a thing with a name propertyassert(is_nil(a.id()));// Confirm that the thing has no IDarr.push(a);// Add the thing to the listassert(is_nil(a.id()));// Verify that the thing still lacks an ID.arr = arr;// Assign the list to a collection root propertyassert(is_int(a.id()));// Verify that the thing now has an IDa;{ "#": 3, "name": "Alice" }
In this example, we observe that the ID is generated when the thing is linked to the collection. Initially, we assigned the thing to a variable, followed by adding the variable to the list. Since the list remained unattached to the collection, the thing retained its lack of an ID. It is only when we incorporated the list as a property of the collection root that it became associated with the collection, triggering ID assignment.
Another noteworthy aspect is that ThingsDB always interprets things by reference. This observation is evident
            in the fact that we kept checking the original variable "a".
Once an ID is assigned to a thing, it remains permanently associated with that thing and cannot be altered or removed.
ThingsDB supports multiple references to the same ID, allowing you to link distinct properties or collections to the same underlying thing. This is demonstrated in the code snippet:
(//chapter4)>.t.p = .arr.first();{ "#": 3, "name": "Alice" }
As evident, property p of thing t points to the same
            thing as the first element in the list.
If you know the ID of a specific thing, you can directly retrieve it using the thing()
            function. When provided with an integer as the first argument, ThingsDB searches the collection for the
            thing with that ID. This becomes particularly useful in practical scenarios.
(//chapter4)>thing(3).name = "Bob";"Bob"(//chapter4)>thing(99).name = "Charlie";LookupError: collection `chapter4` has no `thing` with ID 99
The first example successfully modifies the name property of the thing with ID 3 to "Bob".
            The second query fails because the collection does not contain a thing with ID 99.
Let's review the current state of the collection by querying the root() thing:
(//chapter4)>root();{ "#": 1, "arr": [{"#": 3}], "t": {"#": 2} }
This output might raise an eyebrow. While the root() with properties
            arr and t is fully displayed, the
            p property of the thing with ID 2 and the name
            property of the thing with ID 3 are missing.
This is because ThingsDB, by default, returns things only one level deep. In this instance,
            the root() represents the first level, and the things within list
            arr and the thing within t are considered
            the second level. This safety measure prevents accidentally returning the entire collection.
            While this might be acceptable in our example due to its small size, it could pose issues with
            larger collections.
To address the issue of returning only one level deep, we can utilize the return
            keyword, which was introduced in Chapter 1.5. This statement accepts a
            second argument that specifies the deep level:
(//chapter4)>return root(), 3;{ "#": 1, "arr": [{"#": 3, "name": "Bob"}], "t": {"#": 2, "p": {"#": 3, "name": "Bob"}} }
With this modified query, we now see the root() and its properties at level 1,
            the thing with ID 3 within arr at level 2, the thing with ID 2 at level 2,
            and the thing with ID 3 again, this time residing on property p at level 3.
            The return statement accepts an optional third argument, which allows you to specify flags that modify the
            returned data. Currently, only one flag is supported: NO_IDS.
            When enabled, this flag prevents ThingsDB from including ID values in the response.
(//chapter4)>return root(), 3, NO_IDS;{ "arr": [{"name": "Bob"}], "t": {"p": {"name": "Bob"}} }
While you might initially consider using the return statement frequently to control the deep level or
            incorporate the NO_IDS flag, ThingsDB offers a more elegant and streamlined
            approach to retrieving the desired data. This will be introduced in Chapter 10, where we delve into typed things and wrapping.
We can use a for-loop to iterate through the key value pairs of a thing.
(//chapter4)>.gps = {lat: 51.36, long: 5.23}; for (key, value in .gps) { log(`{key}: {value}`); };WARNING:root:ThingsDB: lat: 51.36 (2) WARNING:root:ThingsDB: long: 5.23 (2) null
The for-loop is similar when used on a thing compared to when iterating a list.
            Only the arguments are different. While iterating over a list accepts a value and index, a thing accepts a
            key and value.
            Similar to lists, a thing also offers methods for iterating over its contents. The provided example
            demonstrates how to achieve the same result as the for-loop using the
            each() method:
(//chapter4)>.gps.each(|key, value| log(`{key}: {value}`));WARNING:root:ThingsDB: lat: 51.36 (2) WARNING:root:ThingsDB: long: 5.23 (2) null
Additional looping methods include filter(), map(),
            and vmap().
While keys in ThingsDB are always restricted to strings, values can have any type. However, if a specific type is required for all values, a value restriction can be applied to enforce this constraint.
Consider our existing "gps" example:
(//chapter4)>.gps.restriction();null
The restriction() method returns nil,
            indicating that no value restriction is currently applied to the "gps" thing.
Attempting to apply an invalid restriction to a non-empty thing will result in an error. For instance, trying to restrict "gps" to integer values will fail because it already contains float values:
(//chapter4)>.gps.restrict("int");ValueError: at least one of the existing values does not match the desired restriction
To successfully apply a value restriction, all existing values must adhere to the specified type. In this case, we can successfully apply a float restriction:
(//chapter4)>.gps.restrict("float");{ "#": 5, "lat": 51.36, "long": 5.23 }
Once a value restriction is applied, attempting to assign a value of an incompatible type will trigger a type error:
(//chapter4)>.gps.lat = nil;TypeError: restriction mismatch
This error prevents invalid value assignments and ensures data integrity.
Lists and other data types in ThingsDB do not allow for self-references due to the creation of copies (tuple conversion) when lists are nested. However, self-references are common within the structure of things.
Consider a simple example of creating a book and its author:
(//chapter4)>// Create a book and authorauthor = {name: "Katja Hoyer"}; .book = { title: "Beyond the wall", author:, };{ "#": 6, "author": {"#": 7}, "title": "Beyond the wall" }
Since we only retrieve the first deep level of detail, we do not see the entire author thing, but we can see that both the book and author have unique IDs (6 and 7, respectively).
To retrieve the author of the book, we can simply access the author property of the book thing:
(//chapter4)>thing(6).author.name;// ID 6 is the book"Katja Hoyer"
To retrieve the books written by the author, we can add a books property
            to the author thing and populate it with the book:
(//chapter4)>thing(7).books = [.book];// ID 7 is the authornil;// Return nil as we do not need the assignment as responsenull
Now, we can retrieve data in both directions, but we have created a nested self-reference for both the book and the author.
You might wonder what will happen if we use a large deep value and ask for either the book or the author? ThingsDB intelligently handles the situation by only returning the IDs of self-referential objects. This prevents infinite loops and ensures data integrity:
(//chapter4)>return .book, 99;{ "#": 6, "author": { "#": 7, "books": [{"#": 6}], "name": "Katja Hoyer" }, "title": "Beyond the wall" }
Establishing two-way relationships between things is a common practice in ThingsDB, but managing these connections manually can be cumbersome and error-prone. To address this challenge, ThingsDB introduces a sophisticated solution that simplifies and streamlines relationship management.
In Chapter 9, we'll delve into the details of this solution and explore its capabilities in detail. But before we embark on this journey, we encourage you to assess your comprehension by taking the quiz provided. This will help solidify your understanding of things and prepare you for the next chapter, where we'll explore the powerful concept of sets in depth.
Looking at the following code, will the thing "t" receive an unique ID and if so, at which line? (a-e)
t = {};p = [t];.p = p;.t = t;// "t" still lacks an IDWhat will be the response for a query using the following code?
return {x: 1, y: 2}, 0;
What function is available to verify the deep value for a collection using a collection scope?
How can you see the ID of a thing in a query response? For example, in the response for this code?
.x = {};
Which of the following code examples will work without returning an error?
{4: 2}.restrict("int");{}.restrict("int").x = 123;{m: 'Hello!'}.restrict("str");{}.restrict("int").x = 1.0;{pi: 3}.restrict("float").pi = MATH_PI;Can a list in ThingsDB be a part of itself?
Can a thing in ThingsDB be a part of itself?
Given the existing thing:
.point = {x: 5, y: 7};
Can you create a query to return the following desired response:
{"x": 5.0, "y": 7.0}
The returned object should contain float values instead of integer values for the x and y properties.
At line "c". The thing is a member of the list p which on this line is assigned to the collection root via property p.
The deep value has been set to 0, so the query will return an empty object: {}
The deep() method can be used to retrieve the current deep value for a collection in the collection scope. 
The ID of a thing is returned as the # property in the thing object unless explicitly the NO_IDS flag is used.
The code samples "b" and "c" will execute without errors. Code snippet "a" will fail because keys must be of type string, and snippet "d" will fail because the thing is restricted to integers and the value 1.0 is a float. Snipped "e" fails because the initial value of pi is an integer, and attempting to apply the restriction to type float will result in an error.
No, a list cannot be a part of itself. Lists are copied when nested, so they cannot be referenced by themselves.
Yes, a thing can be a part of itself. Things are always accessed by reference, so they can be self-referential.
The easiest way to accomplish this is by using the vmap() method which returns a new thing with equal keys but values computed as a result of a given closure callback.
.point.vmap(|v| float(v));
Just like in previous chapters, we shall begin by creating a new collection and switching the prompt to that collection.
In this chapter, we'll explore the concept of sets in ThingsDB. Let's start by creating an empty set.
(//chapter5)>.mammals = set();[]
Notice that despite creating a set, the response displays an empty list. This is because MessagePack, the underlying data serialization format for ThingsDB, does not have explicit support for sets. As a result, sets are always converted to lists in responses.
To add elements to a set, we can utilize the add() method. Sets can only
        accommodate things. Attempting to add other data types will trigger a type error.
(//chapter5)>.mammals.add("dog");TypeError: cannot add type `str` to a set
Let's add some mammals to the set. We can add multiple things simultaneously by separating them with commas.
(//chapter5)>.mammals.add( {animal: "dog"}, {animal: "cat"}, {animal: "elephant"}, {animal: "lion"}, );4
The number of things successfully added to the set is returned. In this instance, we added four new elements.
Let's examine the set we've created:
(//chapter5)>.mammals;[ {"#": 2, "animal": "dog"}, {"#": 5, "animal": "lion"}, {"#": 4, "animal": "elephant"}, {"#": 3, "animal": "cat"} ]
As you can see, all things have been assigned unique IDs, but the order of elements does not match our expectation. This is because sets are inherently unordered.
Due to this lack of order, retrieving an element by index is not possible.
(//chapter5)>.mammals[0];TypeError: type `set` is not indexable
Each thing can only exist once within a set. Remember that ThingsDB compares things by reference, so even if two things have identical properties, they are considered distinct entities.
(//chapter5)>.mammals.add({animal: "dog"});// Add another dog1
Despite having the same properties (animal: "dog"), this dog is
        considered a different entity due to reference comparison. It is not the same "dog" previously added,
        so it gets included in the set.
(//chapter5)>animal = .mammals.one();// Take one element from the set.mammals.add(animal);// Try to add that animal0
The final query yielded 0. This is because the animal already exists within
        the set, and calling the add() method did not introduce any new elements.
Before moving forward, let's create two more sets, one for birds and another for reptiles. (We apologize to any fish or other animal species that weren't included; we'll focus on these three classes for now ;-)
(//chapter5)>.birds = set( {animal: "parrot"}, {animal: "flamingo"}, ); .reptiles = set( {animal: "turtle"}, {animal: "snake"}, );nil;// Return nil as we do not need a responsenull
Now that we have our sets ready, let's create a zoo and populate it with some animals.
(//chapter5)>animals = ["lion", "elephant", "flamingo", "turtle", "snake"]; all = .mammals | .birds | .reptiles; .zoo = all.filter(|x| animals.has(x.animal)); .zoo.len();5
Let's break down what happened here:
animals) containing the animals we want in the zoo.|), we created a new set (all) that combines the mammals, birds, and reptiles sets.filter() method to iterate over the all set and select only those animals that are also present in the animals list.len() method to count the number of animals in the zoo set.Sets in ThingsDB provide a range of useful operations that enable efficient manipulation of data. These operations, summarized in Table 5.1, facilitate tasks such as combining, filtering, and finding specific subsets of things within sets.
Table 5.1 - Set operations
| Operator | Operation | Description | 
|---|---|---|
| (union) | 
                    A | B | 
                    The set of things that are in either A or B. | 
                
& (intersection) | 
                    A & B | 
                    The set of things that are in both A and B. | 
                
- (difference) | 
                    A - B | 
                    The set of things that are in A but not B. | 
                
^ (symmetric difference) | 
                    A ^ B | 
                    The set of things that are in either A or B, but not in both. | 
                
We have already employed the union operation to populate our zoo with a diverse set of animals. Now, let's delve into additional examples showcasing how set operations can be utilized within the context of our zoo.
The - operator allows us to find birds that are not part of the zoo:
(//chapter5)>.birds - .zoo;[ {"#": 8, "animal": "parrot"} ]
This command retrieves the birds set and subtracts the
                zoo set, resulting in a new set containing only the birds that
                do not reside in the zoo.
We can combine the | and &
                operators to find warm-blooded animals that are also in the zoo:
(//chapter5)>(.mammals | .birds) & .zoo;[ {"#": 7, "animal": "flamingo"}, {"#": 5, "animal": "lion"}, {"#": 4, "animal": "elephant"} ]
This expression combines the mammals and
                birds sets using |,
                then intersects the resulting set with the zoo set,
                effectively selecting only the warm-blooded animals that are in the zoo.
These examples demonstrate how set operations can effectively manage and analyze data within sets, providing a powerful tool for building efficient and data-driven applications.
In addition to the set operations we've covered, ThingsDB sets provide several additional useful operators and methods. These enable us to determine whether a set contains all things of another set, verify whether a set is a subset or superset of another, and check whether a particular thing belongs to a set.
The has() method can be employed to determine whether a specific
                thing exists within a set. This method is particularly efficient for sets compared to lists,
                as it does not require iterating through the entire collection to perform the check.
(//chapter5)>flamingo = .birds.find(|b| b.animal == "flamingo"); .zoo.has(flamingo);true
To determine whether one set is a subset of another, we can use the
                <= operator. This operator checks if all elements of the first set
                are also present in the second set.
(//chapter5)>.reptiles <= .zoo;// Determines if "reptiles" is a// subset of "zoo"true
Conversely, to verify whether a set is a superset of another, we employ the
                >= operator. This operator checks if the second set contains all
                the elements of the first set.
(//chapter5)>.zoo >= .reptiles;// Determines if "zoo" is a// superset of "reptiles"true
To distinguish between proper subsets and supersets, which explicitly exclude the possibility of equality,
                the < and > operators are employed.
                These operators function similarly to their respective <= and
                >= counterparts, but they return false when
                the sets are equal.
Table 5.2 - Subset and Superset operations
| Operator | Operation | Description | 
|---|---|---|
<= | 
                        is subset | Determines if all things of the first set are contained within the second set, ignoring equality. | 
< | 
                        is proper subset | Determines if all things of the first set are contained within the second set, and the two sets are not equal. | 
>= | 
                        is superset | Determines if all things of the second set are contained within the first set, ignoring equality. | 
> | 
                        is proper superset | Determines if all things of the second set are contained within the first set, and the two sets are not equal. | 
Assignments with sets follow a similar pattern to lists. When assigning a set to a variable, it is treated as a reference. However, if you assign a set to a property of a thing, a copy of the set will be created.
            Table 5.3 - Summary of when ThingsDB employs copying or reference assignment for a set
| Assignment | Description | 
|---|---|
a = b | 
                    By reference (b is a set) | 
                
a = .b | 
                    By reference (.b is a maintained set) | 
                
.a = b | 
                    Copy to new maintained set (b is a set) | 
                
.a = .b | 
                    Copy to new maintained set (.b is a maintained set) | 
                
a.push(set()) | 
                    Copy to new tuple (a is a list) | 
                
What data types can be stored in a ThingsDB set?
Determine the resulting set after performing the following operation:
set(a, b) - set(b, c)
Which of the following expressions evaluates to true?
set(a, c) <= set(a, b, d)set(a, c) >= set(a, b, d)set(a, c) > set(a, c)set(a, c) < set(a, b, c)How does a client receive a set when it is returned in a response from ThingsDB?
Can you explain what will happen when you append a set as an element to a list?
Only unique "things" are allowed in a set.
The resulting set after performing the operation is set(a).
            The - operator in ThingsDB is used to perform set difference,
            which removes elements from the first set that are also present in the second set.
The only expression which evaluates to true is the expression "d".
            The set with things a and c is a
            subset of the set with things a, b and
            c.
Because sets are not directly supported by the ThingsDB communication protocol, they must be converted to a more fundamental data type when sent to a client. Therefore, the set is converted into a list, which is a sequence of ordered elements. While this conversion enables clients to receive and process sets, it is essential to remember that sets are inherently unordered collections. As a result, you cannot rely on the order of things in the returned list as they may not reflect the order in which the things were added to the set.
When a set is appended to a list, ThingsDB will create a copy and convert the set into a tuple, which is an immutable collection of elements.
In ThingsDB, we've encountered built-in functions like is_nil() and
        methods like .len(), which are functions called on instances of a type.
        We've also explored closures, which are unnamed functions commonly used as callback arguments to
        iterate over lists, sets or things.
ThingsDB introduces another variation of these concepts called procedures.
        Procedures bind a name to a closure and expose the procedure for direct execution. This means we
        can call procedures directly, eliminating the need for queries. Procedures are typically created
        within a collection, but ThingsDB also offers the /thingsdb scope for
        procedures that simplify management tasks like user creation which we will explore later in this chapter.
In this chapter, we'll begin crafting a to-do application, empowering you to manage Alice's tasks and keep her organized.
First, let's create a new collection named "todos" using the /thingsdb scope
        and switch to the //todos scope.
(/thingsdb)>new_collection("todos");"todos"(/thingsdb)>@ //todos(//todos)>
To manage Alice's to-do items, we'll create a list to store them. This list will hold strings representing the to-do items. To control what gets added to the list, we'll use a procedure.
(//todos)>.todos = [];// Create an empty list for to-do items[]
Instead of directly adding to-do's to the list, we'll create a procedure to manage this process.
(//todos)>new_procedure("add_todo", |body| {"Adds a to-do to the list.";// Docstringassert(is_str(body) && body);// Check if the input is a// non-empty string.todos.push(body);// Add the to-do to the listnil; });"add_todo"
This creates a procedure named add_todo that takes a
        single todo argument. The docstring specifies the procedure's purpose
        and is optional. The assert() function ensures that the input is a valid,
        non-empty string.
To utilize the add_todo procedure, we can simply call it directly in a query,
        similar to a regular function.
(//todos)>add_todo("Read a book");OperationError: closures with side effects require a change but none is created; use `wse(...)` to enforce a change; see https://docs.thingsdb.io/v1/collection-api/wse
ThingsDB throws an error indicating that the procedure contains a closure which may introduce changes to the collection and requires a change to synchronize the state. This is because ThingsDB must ensure data consistency across all nodes when changes occur.
To explicitly signal to ThingsDB that the query involves side effects and requires a change,
            we can use the wse() function. This function informs ThingsDB that
            the query will modify the collection and necessitates a change to maintain data integrity.
            Here is the corrected code snippet with the wse() function:
(//todos)>wse();// Tell ThingsDB to initiate a changeadd_todo("Read a book");null
By including the wse() function, ThingsDB recognizes the potential data
            modification and initiates the necessary change operations to maintain consistency across nodes.
            This approach ensures that any changes made by the procedure are applied in a coordinated manner,
            preserving the integrity of the data managed by ThingsDB.
In addition to calling procedures within queries, ThingsDB also allows direct procedure calls without the need for queries. While the ThingsDB Prompt does not support direct procedure calls, this opens up the opportunity to utilize Python code to interact with ThingsDB. As mentioned earlier, Python is not the only programming language supported by ThingsDB; there are also clients available for C#, Go, PHP, JavaScript/Node.js and maybe more by the time you read this book. Furthermore, procedures can be invoked through the ThingsDB HTTP API, a topic we'll explore in Chapter 16.
To streamline the development process, we'll create a reusable template that serves as a foundation for our code examples. This template will encapsulate the common connection and authentication steps, allowing us to focus on the specific procedure call logic.
Create a file named template.py and save the following code:
import asyncio from thingsdb.client import Client async def work(client: Client): pass async def main(): client = Client() client.set_default_scope('//todos') await client.connect('localhost')# Replace with the actual ThingsDB# server addresstry: await client.authenticate('admin', 'pass')# Replace with your# ThingsDB credentialsawait work(client) finally: client.close() await client.wait_closed() asyncio.run(main())
This template assumes ThingsDB is running on localhost. If this is not the case, modify the
            connect() function call to specify the correct address and,
            optionally, a port number. Similarly, update the authenticate()
            function call with your actual ThingsDB credentials.
To test the template, run the following command:
$python template.py$
The code should execute without errors, demonstrating the successful connection and authentication with ThingsDB.
To execute the add_todo procedure directly, we'll create a copy of the template Python script
                and name it add_todo.py.
Within the work function, we'll modify the code to handle user input and invoke the procedure:
async def work(client: Client):todo_body = input("To-do body: ")# Prompt user for to-do itemawait client.run('add_todo', body=todo_body)# Call the add_todo# procedure
                Save the file and run the following command to execute the code:
$python add_todo.pyTo-do body:Brush your teeth$
If successful, no errors should be shown.
Next, create another file based on the template and name it search_todos.py:
async def work(client: Client):needle = input("Search for: ")# Prompt user for search inputres = await client.query("""//ti .todos.filter(|todo| todo.contains('""" + needle + """')); """) print(res)
                Execute the following command to run the search_todos.py file:
$python search_todos.pySearch for:book['Read a book']
The output confirms that the to-do item "Read a book" has been
                added and retrieved successfully.
While the code works, it also contains a potential security flaw. The direct embedding of user input into the query structure makes it vulnerable to code injections, allowing malicious code to manipulate the collection's properties.
Consider the scenario where a malicious user inputs the following:
$python search_todos.pySearch for:' + {.hacked = true; "book";} + '['Read a book']
This input, when passed directly into the query, would effectively create an unauthorized
                property .hacked on the //todos
                collection root. This highlights the potential for code injections to compromise the integrity
                of ThingsDB data.
To address this vulnerability, it is essential to separate user input from the query structure by using variables. This technique allows for a more secure and controlled handling of user-provided data.
To enhance security and prevent code injections, we'll modify the search_todos.py
                code to use variables for user input:
async def work(client: Client):needle = input("Search for: ")# Prompt user for search inputres = await client.query( """//ti .todos.filter(|todo| todo.contains(needle)); """, needle=needle) print(res)
The original code, which directly embedded the user input into the query, was vulnerable to code injections. By using variables, we can isolate the user input and prevent it from affecting the query structure. This safeguard protects the integrity of the ThingsDB database and ensures that only authorized operations are executed.
                To enhance code organization, we'll migrate the "search_todos" query to a reusable procedure. This approach encapsulates the search logic and makes it easily accessible from multiple parts of the application.
Create the procedure in a tool like things-prompt or ThingsGUI. Having explored code-based query execution, you might even prefer to use Python or some other language for code execution. In this book we'll stick with the things-prompt for simple code snippets:
(//todos)>new_procedure("search_todos", |needle| {i "Search for todo's"; .todos.filter(|todo| todo.contains(needle)); });"search_todos"
Update the work method in search_todos.py
                to call the procedure:
async def work(client: Client):needle = input("Search for: ")# Prompt user for search inputres = await client.run("search_todos", needle=needle) print(res)
By using a procedure, we've encapsulated the search logic and made it reusable. This approach promotes code modularity, improves maintainability, and reduces development complexity.
Procedures are stored within a scope, but they are not directly attached to the root of the collection.
            To retrieve information about procedures within a specific scope, the procedures_info()
            function is employed. This function returns a list of procedure information objects, each containing
            detailed metadata about the procedure.
Calling procedures_info() within the //todos scope
            produces the following output:
(//todos)>procedures_info();[ { "arguments": ["todo"], "created_at": 1706363614, "doc": "Adds a to-do to the list.", "name": "add_todo", "with_side_effects": true }, { "arguments": ["needle"], "created_at": 1706540544, "doc": "Search for todo's", "name": "search_todos", "with_side_effects": false } ]//-- some information is left out to keep the sample comprehensible
The properties of each procedure in this list provide valuable insights. The
            with_side_effects property indicates whether invoking the procedure
            modifies the dataset or not. Additionally, each procedure's arguments are listed, and a brief
            description of its functionality is provided in the doc property
            (this is the docstring we provided earlier in the closure body).
Now that we have a basic understanding of procedure information objects, let's focus on extracting just the procedure names.
(//todos)>type(procedures_info());"list"(//todos)>type(procedures_info().first());"mpdata"
Observing the data types involved, we can see that the procedures_info()
                function returns a list of mpdata objects, not things. MPdata stands
                for "MessagePack-Data" and represents serialized MessagePack data. Information objects
                in ThingsDB are pre-serialized to minimize work and enhance performance.
To extract the procedure names, we can utilize the load() method
                provided by the mpdata type. This method allows us to deserialize mpdata objects into
                ThingsDB objects, enabling us to access their properties.
Applying load() to a procedure information object results in a
                thing object. This enables us to access the name property of
                the procedure, which provides the procedure's identifier.
With this approach, we can efficiently retrieve a list of procedure names using the following code:
(//todos)>procedures_info().map(|mpdata| mpdata.load().name);[ "add_todo", "search_todos" ]
This concise code demonstrates the ability to extract relevant information from pre-serialized MessagePack data.
We've covered the new_procedure() and procedures_info()
                functions, but ThingsDB offers a few more functions that you can find in a dedicated
                documentation section.
For detailed information on these additional procedure functions, please refer to the ThingsDB documentation: https://docs.thingsdb.io/v1/procedures-api/
While the current setup of the to-do application works well for Alice, it is important to consider scalability and maintainability from the start. While full admin access might be sufficient for now, granting such extensive permissions to the Python code might not be the best approach in the long run.
To address this, we can create a dedicated user for the Python code and grant them only the necessary permissions. This will provide a more secure and controlled environment for managing user access.
Using the grant() function, we can assign the appropriate permissions to
            the newly created user. The table below summarizes the various permission flags available in ThingsDB:
Table 6.4 - Summary of permission flags
| Mask | Description | 
|---|---|
QUERY (1) | Grants access to execute queries | 
CHANGE (2) | Grants access to make changes | 
GRANT (4) | Grants administrative privileges, allowing users to grant and revoke permissions to other users | 
JOIN (8) | Grants join (and leave) privileges to a room | 
RUN (16) | Grants access to run procedures directly | 
USER (27) | Combination of QUERY, CHANGE, JOIN and RUN | 
FULL (31) | Combination of all privileges | 
Since we've migrated all the queries from the Python code to procedures, we no longer
            require QUERY access. Therefore, we can safely grant the user
            CHANGE and RUN permissions, which will allow them to
            modify data and execute procedures within the "todos" scope.
By adopting this approach, we can effectively differentiate between the administrative user (Alice) and the Python code, ensuring that the code has the appropriate level of access to perform its tasks while maintaining security and control over the system.
Instead of creating users manually, we can create a procedure to automate the process. This will make it easier to manage users and create new ones as needed.
Let's create a procedure called new_todo_user in the
            /thingsdb scope that takes a user name and access flags as input and generates a unique
            token for authentication.
(//todos)>@ /t;// Switch to the /thingsdb scope(/t)>new_procedure("new_todo_user", |name, access| { "Creates a new user with access to the todos scope."; new_user(name); grant('//todos', name, access); new_token(name); });"new_todo_user"
This procedure first creates a new user with the specified name using the new_user()
            function. Then, it grants the desired user permission for the //todos scope
            using the grant() function. Finally, it generates a unique token for the user
            using the new_token() function.
            To create a user named py and obtain their token, run the following command:
(/t)>wse(); new_todo_user('py', RUN | CHANGE);"9U0unWQglFlxl1DUBX5JsR"
This will create a user named py and provide you with their token,
            which you should copy and paste into the corresponding Python script.
Now, modify the authentication call in the template.py and
            search_todos.py scripts to replace the default token with
            the generated one:
… try: await client.authenticate('9U0unWQglFlxl1DUBX5JsR') await work(client) …
Replace "9U0unWQglFlxl1DUBX5JsR" with the actual token you obtained
            from the previous command.
With this change, the Python scripts should now use the py user's token
            to authenticate with ThingsDB and access the //todos scope.
            This provides a more secure and maintainable approach to managing user access.
Under what circumstances is the wse() function necessary?
Will the following query succeed in executing the set_x() procedure
            and modifying the corresponding property in the collection?
set_x(123);
How can you invoke a procedure in ThingsDB?
In the following query, what is the data type of the variable info?
info = procedure_info("set_x");
type(info);  // ???
Which authentication methods does ThingsDB support?
If you want to restrict your application to executing only predefined procedures that do not modify the state, which authentication flag(s) should you use?
QUERYREADCHANGERUNUSERVerify if the .hacked property is present in the todos collection. If so, can you remove the property to ensure the integrity of the data?
ThingsDB employs intelligent algorithms to identify queries with side effects that modify system state. However, in certain scenarios, these algorithms may fail to detect side effects, necessitating the explicit use of the wse() function. One such instance arises when invoking procedures that themselves encompass side effects.
No, the provided query will not work without explicitly invoking the wse() function.
Answer "d". All methods can be used to execute procedures in ThingsDB.
While the client response for procedure_info() might initially seem to imply that the return value is a thing, it is actually an instance of mpdata, a specialized data type that represents serialized MessagePack data. To convert mpdata, you can call the load() method on the mpdata object. This will effectively de-serialize the MessagePack data and return the corresponding thing instance.
ThingsDB offers two primary authentication methods: token-based and username/password. ThingsDB provides the new_token() function for generating secure tokens and the set_password() function for managing user credentials.
The correct answer is RUN.
QUERY flag enables users to execute queries that retrieve and process data from ThingsDB. However, the goal is to restrict the application to executing only pre-defined procedures without modifying the system state.READ flag is not a valid authentication flag in ThingsDB.CHANGE flag allows users to modify data. Since the goal is to prevent the application from modifying the state, the CHANGE flag is not suitable.USER constant serves as a convenient shorthand for combining the commonly used QUERY, CHANGE, JOIN, and RUN permissions into a single name.In Section 6.2.3, we explored code injection vulnerabilities. If you executed the code snippet provided in that section, a new property named .hacked may have been added to the collection. To remove this unintended property, you can use the following code:
.del("hacked");
Currently, Alice can add and search for to-do items, but she lacks a straightforward method to mark them as completed. While we could implement a procedure to remove completed to-do items, Alice prefers to retain them for future reference.
To achieve this, we'll transform the current "todo", which is simply a string, into a thing with
        both a body property and a boolean property which we call done.
        We initially set done to false and update
        the property to true when the to-do is marked as completed.
However, using things without proper type constraints can lead to errors due to incorrect property names or unexpected values. To enhance control and prevent such issues, we'll explore the concept of types in this chapter. In practical applications, it is rare to store plain things; instead, we mainly choose to employ typed things.
        To create a type, you'll need to use the new_type() function to define
            the type and the set_type() function to initialize it with property
            definitions. While it is technically possible to skip new_type() and
            call set_type() directly, this approach may lead to issues with
            self-referential type definitions. To avoid potential problems, it is recommended to follow the
            conventional practice of using new_type() first, followed by
            set_type(). Unlike procedures, types can only be created within a
            collection's scope.
(//todos)>new_type("Todo"); set_type("Todo", { body: "str", done: "bool", });null
Both new_type() and set_type() require a type
            name as their first argument. It is considered best practice to use CamelCase naming convention for types,
            starting with a capital letter. The set_type() function takes a second
            argument, which is a thing object that defines the properties and their respective definitions.
            Property definitions are always expressed as strings.
Once the Todo type is created, you can initialize an instance of it by simply
            writing Todo{}. Every type can be initialized without explicitly specifying
            the properties. This implies that every property must have a default value at initialization.
For instance, here's how to initialize a Todo instance using its default values:
(//todos)>Todo{};// Initialize a Todo using the default values{ "body": "", "done": false }
This creates a new Todo instance with an empty body
            property and a done property set to false. As you
            can see, the default values for body and done are
            automatically assigned when the instance is created.
We defined the Todo type to accept empty strings for the
                body property. However, this is not ideal, as we want to ensure that
                to-do's always have a meaningful description. To address this, we can utilize property definitions
                to impose stricter constraints on the body property.
Instead of simply "str", we can specify a range condition
                using "str<1:>". This indicates that the body must be at least
                one character long. We could also define maximum length restrictions or additional default values
                if needed.
To modify the Todo type, we'll employ the
                mod_type() function. This function is specifically designed for type
                modification and is commonly used in migration scripts to streamline data migration processes.
                It is somewhat analogous to the ALTER TABLE syntax in SQL, often
                employed when introducing new features or enhancing existing applications.
The mod_type() function takes the type name as its first argument,
                followed by an action string specifying the desired operation. In this case, we'll use
                the "mod" action, which indicates a property definition modification.
Here's a table summarizing the available mod_type() actions:
Table 7.1.1 - Summary of mod_type() actions
| Action | Description | 
|---|---|
"add" | Adds a new property or method to the type | 
"all" | Iterates over all instances of a given type | 
"del" | Deletes a property or method from the type | 
"hid" | Enables or disable hide-id for the type | 
"mod" | Modifies a property or method definition | 
"rel" | Creates a relation between types | 
"ren" | Renames a property or method | 
"wpo" | Enables or disable wrap-only mode for the type | 
The specific arguments required depend on the chosen action. For "mod", we
                need to provide the property name, the new definition, and a migration callback if the new definition
                is incompatible with the old one. This is the case here, as the old definition allowed empty strings,
                while the new one does not. The migration callback will be invoked for each instance of the type and
                should return either nil to retain the original property value or a new
                value that complies with the updated definition.
Now, let's migrate the "todos" collection to utilize the updated Todo type:
(//todos)>// Migrate existing string values to the Todo type.todos = .todos.map(|body| Todo{body:});// Modify the Todo type to disallow empty bodiesmod_type("Todo", "mod", "body", "str<1:>", |todo| !todo.body ? "-" : nil);// Migrate procedures to ensure compatibility with the updated typemod_procedure("add_todo", |body| { "Adds a to-do to the list."; .todos.push(Todo{body:}); nil; }); mod_procedure("search_todos", |needle| { "Search for to-do's"; .todos.filter(|todo| todo.body.contains(needle)); });null
By applying the migration in a single query, we eliminate the risk of using deprecated procedures or performing operations on incompatible data structures.
To verify that the migration to the Todo type has been successful,
                let's execute the search_todos.py script again:
$python search_todos.pySearch for:book[{'#': 2, 'body': 'Read a book', 'done': False}]
The previous example demonstrated how to retrieve a list of Todo objects
                using the search_todos.py script. The response included the properties
                of the Todo type along with the unique identifier for each object, represented by the
                # key.
While this approach works, it is possible to gain more control over how IDs are returned in
                responses by leveraging type definitions. Specifically, we can utilize
                the "#" definition to specify the desired key for the ID.
To achieve this, we'll employ the mod_type() function again. This time,
                we'll modify the Todo type to return the ID as "id"
                instead of "#".
(//todos)>mod_type("Todo", "add", "id", "#");null
This modification tells ThingsDB to return the unique identifier for each Todo
                object as the "id" key in responses.
Now, let's re-execute the search_todos.py script to observe the change:
$python search_todos.pySearch for:book[{'id': 2, 'body': 'Read a book', 'done': False}]
As expected, the response now includes the "id" key instead of
                "#", providing a more consistent and user-friendly representation
                of the object identifier.
While we've established a well-defined structure for Todo objects,
            the .todos list still accepts items of other types, potentially
            introducing inconsistencies. Additionally, the collection root lacks strict structure, allowing
            for arbitrary property assignments.
To address these concerns, we'll create a new type which we call Root to
            represent the collection root. The name Root is just a choice, another
            good name would be Collection or a more descriptive name like
            TodoCollection. This type will solely contain a property named
            todos with type definition "[Todo]", ensuring
            that only Todo objects can be added to the list.
Let's create the Root type:
(//todos)>new_type("Root"); set_type("Root", { todos: "[Todo]", });null
To convert the collection root to the Root type, we'll use the
            to_type() method.
(//todos)>.to_type("Root");null
            With the Root type applied to the collection root, we have achieved stronger
            type enforcement. Attempts to assign arbitrary properties or add non-Todo objects
            to the .todos list will trigger errors.
Here are some examples illustrating this enhanced type safety:
(//todos)>.x = 1;LookupError: type `Root` has no property `x`(//todos)>.todos.push(123);TypeError: type `int` is not allowed in restricted array(//todos)>type(root());"Root"(//todos)>.todos.restriction();"Todo"
By leveraging typed roots, we've significantly enhanced the structure and type safety of our collection, promoting data integrity and consistency.
As your application grows and evolves, you may need to adapt your data structures to accommodate new
            requirements or enhance existing features. To address such changes, the earlier
            discussed mod_type() function is available, enabling you to modify existing
            types by adding new properties, changing data types, or introducing restrictions.
If, for any reason, you need to revert a type back to its original "thing" form,
            the to_thing() method is your go-to solution. This method effectively
            de-structures the typed thing, removing any imposed constraints and allowing for arbitrary property
            assignments.
In Chapter 4.2, we learned how to retrieve a specific thing using
            the thing() function by providing its ID. This method also applies to
            typed things, but for greater control, we can directly invoke the type name and pass the ID as an
            argument. This approach ensures that only instances of the specified type are returned.
Consider the following examples:
(//todos)>Todo(2);// ID 2 is a Todo item{ "body": "Read a book", "done": false, "id": 2 }(//todos)>Todo(1);// ID 1 is the collection rootTypeError: `#1` is of type `Root`, not `Todo`
In the first example, the Todo(2) call successfully retrieves
            the Todo object with ID 2. However, when attempting to access the
            collection root using Todo(1), an error is raised because the collection
            root is a Root object, not a Todo object.
Up to this point, the Todo type has a property named done
            to indicate whether a to-do item is completed. However, if Alice finishes reading a book, she needs a way to
            mark the corresponding to-do item as completed. While we could simply set the done
            property to true, it is better to adopt a more flexible approach that allows
            for potential future changes in data representation.
Type "methods" are self-contained functions within a type definition, allowing us to encapsulate specific
            behavior for that type. Instead of directly modifying the done property,
            we'll create a method named mark_as_done that handles the task completion logic.
In contrast to properties, which are defined using strings, methods are defined using closures. These closures
            encapsulate the method's logic and receive the this object as their first argument,
            which represents the instance of the type where the method is being called on.
We'll use the mod_type() function to add the mark_as_done
            method to the Todo type:
(//todos)>mod_type("Todo", "add", "mark_as_done", |this| this.done = true);null
Next, we'll create a procedure to interact with the mark_as_done method.
            This procedure will receive a todo_id as input and call the
            mark_as_done method on the corresponding Todo instance:
(//todos)>new_procedure("mark_as_done", |todo_id| { wse(); Todo(todo_id).mark_as_done(); nil; });"mark_as_done"
            To test the new mark_as_done procedure, we'll create a Python file
            named mark_as_done.py based on the template and modify the work method to
            capture user input for the todo_id and call the procedure:
async def work(client: Client):todo_id = int(input("To-do ID: ")) await client.run('mark_as_done', todo_id=todo_id)
Running this Python file will prompt you to enter a todo_id, which will
            then be used to invoke the mark_as_done procedure.
$python mark_as_done.pyTo-do ID:2
If no errors occur, it indicates that the operation was successful.
To verify that the to-do item has been marked as completed, you can run the
            search_todos.py script again and observe the
            updated done property:
$python search_todos.pySearch for:book[{'id': 2, 'body': 'Read a book', 'done': True}]
With the types_info() function, you can explore all the types defined
            within a collection. The function returns a list of information objects.
For detailed information about a single type, use the type_info() method.
            The Todo type information, for example, would look like this:
(//todos)>type_info("Todo");{ "created_at": 1706696007, "fields": [ ["id", "#"], ["body", "str<1:>"], ["done", "bool"] ], "hide_id": false, "methods": { "mark_as_done": { "arguments": ["this"], "definition": "|this| this.done = true", "doc": "", "with_side_effects": true } }, "modified_at": 1707128380, "name": "Todo", "relations": {}, "type_id": 0, "wrap_only": false }
ThingsDB empowers you to remove existing types using the del_type() function.
            However, exercise caution, as this function operates even when instances of the
            targeted type still exist.
Consider the following example:
(//todos)>new_type("T");// Create type Tset_type("T", {name: "str"});// Define type Tt = T{name: "test"};// Create an instance of Tassert(type(t) == "T");// Verify the typedel_type("T");// Delete type Tassert(type(t) == "thing");// Verify type is now "thing"t;{ "name": "test" }
As observed, deleting the type converts all existing instances to plain "things", discarding the specific type information. This transformation is not easily reversible unless you possess precise knowledge of the original type belonging to each instance.
Furthermore, attempting to remove a type referenced by another type or enumerator will fail. To successfully delete such a type, you must first address the dependencies. Refer to Chapter 9 for in-depth exploration of type connections and Chapter 11 for detailed discussions on enumerators.
In this book we'll explore more definitions than discussed so far. We already used a definition to specify a string with a range, a boolean and a list restricted to a specific data type.
As always, use the documentation page for a full description of all the type property definitions which can be found here: https://docs.thingsdb.io/v1/overview/type/
Choose which statement declares a new type?
del_type("A");new_type("A");mod_type("A");set_type("A");Can you identify which strings fit the property definition "str<4:8>"?
"ThingsDB""Hi"nil"Hello""Bicycle Race"How can you seamlessly integrate the ID of type T instances as a property
            named "identifier" in responses, given that the ID is currently exposed
            as "#"?
Suppose a type T exposes the ID for each instance as key "identifier" (see previous question).
            How can you change the name for this key to "id"?
Complete the set_type("P", ???); statement to create type P, enabling the following code to function flawlessly:
a = P{x: 3.0, y: 2.0};
b = P{x: 7.0, y: 8.0};
d = a.distance(b);  // Should produce 7.211102550927978
Hint: recall the distance formula: √((x₂ - x₁)² + (y₂ - y₁)²)
Can you produce a query that returns a list containing only the type names present in the collection?
When a type is removed using del_type(), what happens to existing instances of that type?
The correct answer is "b". While set_type() can technically be
            used to create a new type, it is primarily used to set the properties and/or methods for a type.
            It requires an additional argument, even when not setting any properties or methods.
            del_type() is the opposite, it removes a type instead of creating one.
            mod_type() is used to modify a type and at least requires an "action"
            as second argument.
Both "a", "ThingsDB" and "d", "Hello" are valid.
            The defined range accepts strings from 4 up till 8 characters, both inclusive.
            The string "ThingsDB" contains exactly 8 characters so it is a valid string.
            Type nil is not allowed for this definition since only type string is allowed. 
By executing the following statement:
mod_type("T", "add", "identifier", "#");
A type can only have a single "#" definition. By executing the
            following statement the key will be renamed from "identifier" to "id".
mod_type("T", "ren", "identifier", "id");
To enable distance calculations between points, create type P with the following masterful definition:
set_type("P", {
  // Properties for point coordinates
  x: "float",
  y: "float",
  
  // Method for calculating distance:
  distance: |this, other| {
    // Distance formula
    sqrt(pow(other.x - this.x, 2.0) + pow(other.y - this.y, 2.0));
  },
});
Use types_info().map(|info| info.load().name); to get a list of type names.
            This iterates through type info objects, extracting and returning only the name property.
Instances of the removed type are downcast to regular things. This means that while their existing properties and values remain intact, the original type information becomes unavailable.
Effectively managing to-do lists involves not only tracking tasks themselves, but also the timing of their creation and completion. In this chapter, we delve into two core methods for capturing time in ThingsDB: timestamps, the datetime type and the task type used for scheduling code execution.
Timestamps represent time as a single numerical value, specifically the number of seconds elapsed since January 1, 1970, 00:00:00 UTC. This representation offers compact storage and is suitable for calculations involving relative time differences.
The now() function retrieves the current timestamp:
(//todos)>now();1707145945.4736154
While timestamps offer efficient storage and ease of calculating time differences between events, their single numerical representation can pose challenges when dealing with absolute dates or time zones. These scenarios often require intricate calculations to extract the desired information.
ThingsDB simplifies time-related tasks with the datetime type, offering a human-readable and efficient approach to date and time manipulation.
Unless explicitly overridden with the set_time_zone() function,
            datetime objects in ThingsDB default to Coordinated Universal Time (UTC), the global standard
            for timekeeping.
Generate a datetime object reflecting the current date and time by simply calling
            the datetime() function without arguments:
(//todos)>datetime();"2024-02-05T15:22:05Z"
The datetime object is returned in a response as a string using the ISO 8601 format.
            The datetime() function offers a variety of methods to create datetime objects,
            ensuring adaptability to diverse data sources and formatting preferences.
Construct datetime objects with year, month, and day:
(//todos)>datetime(2024, 2, 6);"2024-02-06T00:00:00Z"
Add optional hour, minute, and seconds:
(//todos)>datetime(2024, 2, 6, 9, 57, 30);"2024-02-06T09:57:30Z"
When the final argument passed to datetime() is a string, ThingsDB
            interprets it as a time zone identifier. This allows you to "ground" your datetime object in a
            specific geographical location, ensuring accurate representation across regions:
(//todos)>datetime(2024, 2, 6, 9, 58, "Europe/Kyiv");"2024-02-06T09:58:00+0200"
In this example, the time zone "Europe/Kyiv" is specified, resulting in a
            datetime object reflecting the time in Kyiv, Ukraine. For a comprehensive overview of all available
            time zones supported by ThingsDB, use the time_zones_info() function.
Create datetime objects from ISO 8601 formatted strings:
(//todos)>datetime("2024-02-06T10:05:00Z");"2024-02-06T10:05:00Z"
Accommodate diverse date and time representations using format specifiers:
(//todos)>datetime("2024-2-6", "%Y-%m-%d");"2024-02-06T00:00:00Z"
Convert a timestamp to a datetime object and return using a custom format:
(//todos)>datetime(1707211569).format("%a, %d %b %Y %H:%M:%S %Z");"Tue, 06 Feb 2024 09:26:09 UTC"
As illustrated in the provided examples, datetime object initialization in ThingsDB boasts remarkable flexibility. For a comprehensive overview of possible initialization options, delve into the detailed online documentation: https://docs.thingsdb.io/v1/collection-api/datetime/
Once initialized, datetime objects uphold immutability, safeguarding the integrity of their
            original data. Consequently, methods like move(), to(),
            and replace() always create new datetime objects,
            leaving the original unaltered.
Need to tweak specific components within a datetime object? The replace() method
            is your friend. It allows you to craft new instances with adjusted values, ensuring pinpoint accuracy:
(//todos)>datetime("2024-02-06T09:26:09Z").replace({ minute: 0, second: 0, });"2024-02-06T09:00:00Z"
Accurate time representation across different regions is crucial. With the to() method,
            you can seamlessly apply different time zones to your datetime objects:
(//todos)>datetime("2024-02-06T09:26:09Z").to("Europe/Kyiv");"2024-02-06T11:26:09+0200"
Move through time with ease using the move() method. It allows you to
            generate new datetime objects adjusted by specified intervals, empowering you to navigate temporal
            data efficiently:
(//todos)>datetime("2024-02-06T09:26:09Z").move("years", -1);"2023-02-06T09:26:09Z"
By mastering these and other methods, you'll unlock a powerful toolkit for manipulating datetime objects within ThingsDB projects. This ensures precision and flexibility in managing your time-related data.
Converting a datetime object to a timestamp can be achieved effortlessly by casting it as an integer.
(//todos)>int(datetime("2024-02-06T09:26:09Z"));1707211569
ThingsDB also offers a data type called timeval. While functionally equivalent to datetime, it has a key difference: instead of returning an ISO 8601 formatted string, timeval returns a raw UTC timestamp integer in its responses. Think of it as a more compact and optimized version of datetime for timestamp-related tasks.
(//todos)>timeval("2024-02-06T09:26:09Z");1707211569
Now that you've grasped the power of datetime for representing and manipulating temporal data,
            let's apply this knowledge to enhance our understanding of the Todo type
            in ThingsDB. We'll focus on adding two useful properties: creation
            and completion.
We want to keep track of when each Todo item was created. To achieve this,
            we'll add a new property named creation with the datetime type:
(//todos)>mod_type("Todo", "add", "creation", "datetime");null
This command seamlessly integrates the creation property into the Todo type.
            However, since we do not possess historical data about the exact creation dates of existing to-do's,
            the system automatically assigns the current date and time to them. This ensures consistency while
            acknowledging the limitations of our historical data.
Next, we want to record the moment when a Todo item is marked as completed.
            But here's a twist: not all to-do's might be completed yet. For this, we'll introduce a property
            named completion, but with the "datetime?" definition. The question
            mark (?) signifies that this property is optional, allowing it to hold
            either a datetime value (representing the completion time) or nil (indicating
            no completion yet).
Here's the command to add the completion property:
(//todos)>// Adds the completion propertymod_type("Todo", "add", "completion", "datetime?", |this| { this.done ? datetime() : nil; });// Fix the mark_as_done methodmod_type("Todo", "mod", "mark_as_done", |this| { this.done = true; this.completion = datetime(); });null
This code snippet does more than simply adding the property. It also defines a closure to initialize
            the completion value appropriately for existing to-do's and it also fixes
            the mark_as_done method to set the completion value
            after the to-do will be marked as done.
Tasks allow you to schedule code execution at specific dates and times.
Key features of the task type:
set_owner() method, granting ownership and associated permissions to a different user.Let's explore what happens when we create tasks in ThingsDB:
(//todos)>task(datetime(), || nil);"<task:4 owner:admin run_at:2024-02-07T14:49:36Z status:nil>"
This task is scheduled for immediate execution as datetime() specifies the
            current date and time. It doesn't perform any actions (|| nil) but serves
            as an example of task creation.
(//todos)>task(datetime(), || 1/0);"<task:5 owner:admin run_at:2024-02-07T14:49:50Z status:nil>"
This task attempts to perform a division by zero, which will result in an error when executed. It is useful for demonstrating how ThingsDB handles task errors.
(//todos)>task(datetime().move("days", 1), || nil);"<task:6 owner:admin run_at:2024-02-08T14:50:01Z status:nil>"
This task is scheduled to run a day later using datetime().move("days", 1), showcasing how to schedule actions for specific future moments.
Use the tasks() function to view all tasks within the current scope:
(//todos)>tasks();[ "<task:5 owner:admin run_at:nil status:err>", "<task:6 owner:admin run_at:2024-02-08T14:50:01Z status:nil>" ]
Note that successfully completed tasks are removed from the list, while those with errors and pending tasks remain.
Task with ID 5 has encountered an error, preventing its successful completion. To uncover the cause
            of the problem, we'll utilize the err() method:
(//todos)>task(5).err();"division or modulo by zero"
There are two ways to stop tasks from running. Using the cancel() method
                preserves the task information for later use and marks it with
                a cancelled_err while the del() method
                permanently removes it.
The code below illustrates the use of both methods:
(//todos)>task(6).cancel();// Cancel task with ID 6null(//todos)>task(6).err();// Observe cancelled error"operation is cancelled before completion"(//todos)>task(6).del();// Delete tasknull(//todos)>task(6);// Attempt to access task with ID 6 (now deleted)LookupError: task with id 6 not found
Incorporating again_in() within your task's code allows you to
                reschedule its execution based on desired time units and values. It acts like a built-in calendar
                reminder, prompting the task to reactivate itself after a defined period.
Let's illustrate this with an example: Alice needs a daily reminder to send reports to Bob. She can achieve this with a single task coded as follows:
(//todos)>task(datetime(), |task, body| {task.again_in("days", 1);// Reschedule the task in 1 dayadd_todo(body);// Add the "Send report to Bob" to-do}, ["Send report to Bob"]);"<task:7 owner:admin run_at:2024-02-07T14:52:25Z status:nil>"
In this code, the task immediately executes using datetime(). Within
                the closure, task.again_in("days", 1) reschedules it to run again in
                one day. The task then adds a to-do with the message "Send report to Bob".
But what if you need to modify the report content later? No worries! The task type offers
                the set_args() method, enabling you to dynamically change
                task arguments without recreating the entire task.
While again_in() offers convenient rescheduling relative to the
                original task time, the task type also provides again_at() for
                pinpoint accuracy in setting future execution.
Choosing the right method:
again_in():
task.again_in("days", 1) reschedules the task to run in 1 day.again_at():
task.again_at(datetime(2024, 02, 09, 10, 00)) reschedules the task for February 9th, 2024, at 10:00 AM.As we explored, completed tasks vanished from the ThingsDB list, leaving no trace of their successful execution. But what about repeating tasks? How do we track their progress and ensure they are functioning smoothly?
A successfully completed run of a repeating task changes its status from "nil"
                to "ok" indicating a successful execution. You can check this status using
                the task() function, providing the task ID of your repeating task:
(//todos)>task(7);"<task:7 owner:admin run_at:2024-02-08T14:52:25Z status:ok>"
This example shows that the task with ID 9, scheduled to repeat on February 8th, 2024, at 2:52 PM UTC, has successfully completed its previous run.
If you need to check the task status in code, use the err() method.
                This reveals any errors encountered during the last execution.
(//todos)>task(7).err();null
If everything went well, the output will be nil, indicating no errors.
Before starting the quiz, let's clean up the tasks we created during this chapter. Can you guess how to delete all of them at once?
Indeed! The following code snippet achieves this efficiently:
(//todos)>tasks().each(|task| task.del());null
This expression iterates through every task using tasks(), and for
                each task, it calls the del() method
                to permanently remove it.
To verify, you can check the list again:
(//todos)>tasks();[]
As you can see, the list is now empty, indicating all tasks have been successfully deleted.
Remember the done property for marking completed to-do's?
            We can actually remove it! The completion property serves this purpose perfectly.
Here's how we clean up the data model:
(//todos)>// Remove the done propertymod_type("Todo", "del", "done");// Update the mark_as_done methodmod_type("Todo", "mod", "mark_as_done", |this| { this.completion = datetime(); });null
This updated method only sets the completion property to the current date
            and time, indicating the to-do is complete.
What data type is returned by the now() function, and what does it represent?
How would you calculate the timestamp for January 25, 2025, at 13:05 Kyiv time, considering time zone differences and potential daylight saving time adjustments?
Remember the range definition from the previous chapter? Consider the property definition "int<-1:1>?". What range of values would be allowed for this property?
You have a task that you no longer want to execute, but you still want to keep it for reference. How can you achieve this without permanently removing the task from the task list?
Which of the following will create a daily repeating task?
task(datetime(), |task| nil).repeat("daily");task(datetime(), |task| task.again_in("days", 1));task(datetime(), |task| task.again_at(datetime(2025, 1, 1)));Write a single line of code that evaluates all tasks and returns true only if all tasks are in a non-error state. If at least one task is in an error state, the code should return false.
Which of the following statements is true?
The now() function returns a floating-point number representing the current time as the number of seconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). 
The code timeval(2025, 1, 25, 13, 5, "Europe/Kyiv"); constructs a datetime object representing the specified date and time in the Europe/Kyiv time zone and directly returns a UNIX timestamp in the response. This differs from the datetime() function, which returns an ISO 8601 formatted string representation of the datetime object. To obtain a UNIX timestamp from a datetime object you can convert the datetime object to an integer using the int() function.
The property definition "int<-1:1>?" allows integer values from -1 to 1, inclusive. The question mark (?) denotes that the property can also be left nil. So, valid options are -1, 0, 1 and nil.
The cancel() method prevents a task from executing while keeping it in the list. The task's error state will be set to cancelled_err. This is different from using the del() method, which completely removes the task from the list, including its data.
Option "b" is the correct approach for creating a daily repeating task. The again_in() method with a "days" argument allows you to define a relative delay from the current time. In this case, a recurring delay of 1 day, effectively scheduling the task to run every day until it is explicitly canceled. While again_at() can be used, it schedules the task for a specific time on that date only. Using again_at(datetime(2025, 1, 1)) would run the task once on January 1, 2025, not daily. In fact, if the current date is past January 1, 2025, it would indeed result in a value_err ("start time out-of-range") error.
The following line of code would accomplish this task:
tasks().every(|task| is_nil(task.err()));
The function tasks() returns a list with all tasks and
                the every() method on the list iterates through each task and only
                returns true if the provided closure evaluates to true for
                every task. Within the closure, is_nil(task.err()) checks if
                the err() method of each task returns with nil.
Statement "c" is correct: datetime and timeval represent the same information, but differ in response format. The datetime and timeval types offer only second precision. Both types are immutable, so attempting to change their time zone creates a new object reflecting the change.
Alice's trusty to-do app has caught Bob's eye, and now it's time to take it multi-user! Let's dive into how we can transform the app to accommodate different users and their to-do's.
Let's start with creating a new type called User with properties
        for their name and todos:
(//todos)>new_type("User"); set_type("User", { name: "str", todos: "{Todo}", });null
        Now comes the migration magic! Though slightly longer than previous code, keep in mind this single query transforms the entire collection from single-user to multi-user. It seamlessly adds users for Alice and Bob, migrates Alice's existing to-do's, and updates all procedures to function with the new setup.
(//todos)>// Add user lookupmod_type("Root", "add", "users", "thing<User>");// Add procedure for adding a usernew_procedure("add_user", |name| { "Add a new todo user."; uid = name.lower(); assert(!.users.has(uid)); user = .users[uid] = User{name:,}; user.id(); });// Add users and migrate Alice's to-do'suser_id = add_user("Alice"); User(user_id).todos |= set(.todos); add_user("Bob");// Remove todos from Rootmod_type("Root", "del", "todos");// Update search_todosmod_procedure("search_todos", |needle| { "Search for to-do's"; .users.values().reduce(|total, user| { total |= user.todos.filter(|todo| todo.body.contains(needle)); total; }, set()); });// Update add_todomod_procedure("add_todo", |name, body| { "Adds a to-do to a user's todo list."; todo = Todo{body:,}; .users[name.lower()].todos.add(todo); todo.id(); });null
So far, everything seems familiar. We can easily find a user's to-do's because
        they are directly stored under the todos property.
        But what if we have a specific to-do? How do we know which user it belongs to
        without searching through all users?
This becomes evident when using our existing search_todos.py script:
$python search_todos.pySearch for:report[{'id': 8, 'body': 'Send report to Bob', 'creation': '2024-02-07T14:52:26Z', 'completion': None}]
As you can see, the search does not reveal which user this to-do belongs to.
To address the issue described in the previous paragraph, we can add
            a user property to the Todo type.
            However, manually maintaining its accuracy could be cumbersome. ThingsDB offers a more
            elegant solution: relations.
Let's add a relation between the Todo and User
            types using the following code:
(//todos)>// 1. Add the optional user property to Todomod_type("Todo", "add", "user", "User?");// 2. Create the bidirectional relationmod_type("Todo", "rel", "user", "todos");null
Let's break down the key steps:
Todo type.
                This property holds a reference to a User object, but here is the crucial part:
                it is nillable (indicated by the question mark ?). Why is this important?
                Imagine we remove a Todo from a user's set.
                With a nillable property, ThingsDB can effortlessly set the user property
                to nil, reflecting the broken relationship. Without
                nillable properties, such changes would lead to errors or inconsistencies.
            Todo and User. We do
                this by specifying the user property on
                the Todo side and the todos property on
                the User side. This tells ThingsDB how they're connected. As a result,
                you can easily access a user's to-do's through their todos property
                and identify the owner of a specific Todo using
                its user property.
            
            Now that we've built the bridge between Todo
            and User, let's see if it holds. Let's use our
            trusty search_todos.py script:
$python search_todos.pySearch for:book[{ 'id': 2, 'body': 'Read a book', 'creation': '2024-02-07T14:47:49Z', 'completion': '2024-02-07T14:48:06Z', 'user': {'#': 10} }]
Look! ThingsDB automatically populated the user property for us.
            But the real magic happens when we modify this relationship. Imagine changing
            the "Read a book" to-do's owner from Alice to Bob.
            Thanks to the relation, this change would seamlessly ripple through the system.
            The to-do automatically gets removed from Alice's todos set
            and added to Bob's! This dynamic behavior ensures your data stays consistent and
            reflects real-world ownership of to-do's.
Our to-do example showcased a one-to-many relationship between a single
            property user and a set todos. But ThingsDB's relational
            power doesn't stop there! Let's dive into the other types which are briefly
            described in Table 9.2 below.
Table 9.2 - Relationship Types
| Relation | Definition | Description | Example | 
|---|---|---|---|
| One-on-One | T? <-> T? | Type to Type | Passport and owner | 
| One-to-Many | T? <-> {T} | Type to Set | User and their to-do's | 
| Many-to-Many | {T} <-> {T} | Set to Set | Users and their friends | 
Let's take a quick break from our current topic and set up a dedicated space for
            exploring the different types of relationships in ThingsDB. Using
            the /thingsdb scope, we'll create a new collection
            called "relations" to use for upcoming examples.
Here's how we'll do it:
(//todos)>@t(/t)>new_collection("relations");"relations"(/t)>@ //relations(//relations)>
Now, with the "relations" collection ready and waiting, we can
            now examine the different types of relationships available in ThingsDB.
Imagine a Passport type linked to its owner through
                a user property. Each owner can have only one passport,
                and each passport belongs to exactly one owner. This scenario represents
                a one-on-one relationship, denoted
                as T? <-> T?
                in Table 9.2.
One-on-One relationships in ThingsDB offer flexibility beyond just connecting different types. You can create a relationship where one instance connects to exactly one other instance of the same type. This even allows for reflexive relationships, where an instance connects to itself!
Defining the type Pair to demonstrate this type of relationship:
(//relations)>new_type("Pair");set_type("Pair", {other: "Pair?"});// Create a relationship to the same propertymod_type("Pair", "rel", "other", "other");null
Creating a Pair in action:
(//relations)>.p = Pair{}; .p.other= Pair{}; .p.other.other == .p;// Returns true, confirming the linktrue
Building on the idea of self-referencing relationships, let's dive into chain relationships. These are perfect for modeling parent-child scenarios where each parent has only one child, and vice versa. Imagine a chain of connected elements, where each element points to the next one in line.
Creating the Chain Type:
(//relations)>new_type("Chain"); set_type("Chain", {prev: "Chain?", next: "Chain?"});// Create a relationship between "prev" and "next"mod_type("Chain", "rel", "prev", "next");null
Now, let's create some Chain instances and connect them:
(//relations)>.chain = Chain{}; .chain.next = Chain{}; .chain.next.next = Chain{}; .chain.next.next.prev == .chain.next;// Returns true, confirming the linktrue
See how each element's next property points to the next one in
                the chain, and the last element's prev points back to the second element!
Now, consider a social network scenario where users have friends. Adding Bob as a friend for Alice should automatically make Alice a friend of Bob. This many-to-many relationship connects two sets.
Let's create a Person type in the //relations
                scope to model this scenario:
(//relations)>new_type("Person"); set_type("Person", {friends: "{Person}"});// Define the many-to-many relationshipmod_type("Person", "rel", "friends", "friends");null
Now, let's see how Thelma and Louise become friends:
(//relations)>.Thelma = Person{}; .Louise = Person{};.Thelma.friends.add(.Louise);// Thelma adds Louise as a friend.Louise.friends.has(.Thelma);// Returns true, confirming their friendshiptrue
By adding Louise to Thelma's friends set, the relationship automatically
                applies to Louise's friends set as well, thanks to the defined relationship.
Like other relationship types, many-to-many relationships aren't limited to connecting instances
                of the same type. As long as both participating entities have sets defined with each other's type,
                you can create a bidirectional connection. In Table 9.2, we used the
                notation {T} <-> {T} to represent this
                flexibility.
We've explored different relationships in ThingsDB. Now, let's head back to
                the //todos collection to continue building our to-do app!
Adding a relation in ThingsDB might seem like a simple task, but there's more to it than meets the eye.
            Before officially establishing a relation, ThingsDB performs a thorough check to ensure everything is in order. Think of it like a safety net to prevent potential data conflicts.
These checks are crucial for maintaining data integrity and consistency. Imagine accidentally creating a relation that would lead to duplicate data or inconsistent information. By proactively identifying such issues, ThingsDB safeguards your data and prevents headaches down the line.
Now that we understand the importance of pre-checks, let's remember a key requirement for relations:
            they only work with stored data. This means that at least one of the entities involved in
            creating a relation (e.g. a Todo and its User) needs to have an ID assigned by ThingsDB.
Consider this code snippet that tries to create a relation without this requirement:
(//todos)>t = Todo{};// Todo without IDu = User{};// User without IDu.todos.add(t);TypeError: relations must be created using a property on a stored thing (a thing with an Id)
In this example, we attempt to link a Todo and a User
            by adding the Todo object to the user's todos property.
            However, the User object lacks an ID, which is essential for establishing the
            relation. Remember from Chapter 4.2 that things only get IDs when they are
            assigned to a collection.
To correctly create the relation, first assign the User to the collection before
            adding the Todo. This will grant them IDs, enabling ThingsDB to establish the
            desired connection.
            Previously, adding to-do's was limited to a specific user. Now, we'll prompt the user for a name during
            the process. This information will be passed to the add_todo procedure,
            ensuring the to-do gets linked to the correct user.
Here's the updated add_todo.py script:
async def work(client: Client):name = input("Name: ") todo_body = input("To-do body: ") todo_id = await client.run('add_todo', name=name, body=todo_body) print(todo_id);
With the add_todo.py back on track, it's time to test your knowledge with
            the quiz! Then, in the next chapter, we'll master controlling ThingsDB responses.
Which of the following are valid relation types which can be handled by ThingsDB?
T? <-> T?T? <-> [T]{T} <-> {T}T? <-> {T}T <-> {T}Can you identify which statement(s) establish a relation between type X on
            property one with definition "Y?" and
            type Y on a property called many using definition "{X}"?
mod_type("X", "rel", "one", "many");mod_type("X", "rel", "many", "one");mod_type("Y", "rel", "one", "many");mod_type("Y", "rel", "many", "one");Imagine we're building a to-do app with users and their tasks. We have a Todo type and a User type connected by a one-to-many relation on the user and todos properties. Now, consider the following code:
Todo{user: User(10)};
This code tries to create a new Todo object and assign
                a User object with ID 10 to its user property.
                The User with ID 10 exists, so why would this code fail?
                Can you identify the issue and explain why it would not work in ThingsDB?
Remember our to-do app with the Todo and User types
            linked by a one-to-many relation? Now, consider the following code snippet:
todo = Todo(2);
user = User(10);
assert(todo.user == user);
user.todos -= set(todo);
todo.user;  // ??? what will it be?
Given that this code runs without errors, can you predict what value will be assigned to todo.user after the final line?
Valid relation types are "a", "c" and "d".
T? <-> T? (One-on-One relation)T? <-> [T] (!!! Invalid - A relation with a "list" is not possible){T} <-> {T} (Many-to-Many relation)T? <-> {T} (One-to-Many relation)T <-> {T} (!!! Invalid - The property must be marked nillable)The correct statements are "a" and "d". When establishing a relationship, ThingsDB relies on the third argument to locate the other type based on the provided property. Subsequently, the fourth argument serves as a pointer, directing ThingsDB to the target property on that type where the relationship will be formed.
In ThingsDB, establishing a relation between two types relies on stored data and their associated IDs. In your code, the User object with ID 10 exists, but you are trying to assign it to a new Todo without an ID. The solution is to use the stored user:
User(10).todos.add(Todo{});
When working with relations in ThingsDB, always keep in mind the importance of stored data and IDs. Utilize existing objects with IDs to establish connections.
The final value of todo.user in the given code snippet will
            be nil. The key lies in the line user.todos -= set(todo).
            Here, we're using set subtraction to remove the Todo object from
            the user.todos set. ThingsDB then automatically updates the user property
            of the Todo object to nil, reflecting the "broken" relationship. This behavior ensures consistency and eliminates the possibility of orphaned references.
Remember how search_todos.py returned a Todo instance?
        While it is functional, imagine customizing the response to something like this:
{
    "id": 2,
    "body": "Read a book",
    "done": true,
    "user": {
       "name": "Alice",
    }
}
    This example showcases the desired structure:
"id" and "body" remain unchanged.completion property is replaced with a "done" flag."name".While code manipulation can achieve this, ThingsDB offers a more efficient approach: Wrap-Only types. These types act as templates defining how to present existing data. They can't be directly created, but they can "wrap" other types, transforming their output.
Let's dive into defining our wrap-only type for to-do's.
(//todos)>new_type("_Todo", true);"_Todo"
The name we choose for our wrap-only type plays a role. While not strictly required, many developers
            adopt the convention of starting it with an underscore (_Todo in our case).
            This helps visually distinguish wrap-only types from regular ones at a glance.
The second boolean argument (true) passed to new_type()
            serves as a flag, enabling the "wrap-only" mode for the defined type (_Todo).
            This signifies that direct instantiation of instances is prohibited. Instead, its functionality lies in
            dynamically transforming existing data structures based on the properties and computed values specified
            within its definition.
Having declared our _Todo wrap-only type, we proceed to define its structure and behavior
            using set_type():
(//todos)>set_type("_Todo", { id: "#", body: "any", done: |this| is_datetime(this.completion), });null
Let's dissect the key aspects of our wrap-only type configuration:
id: "#"
This familiar selection directly includes the unique identifier (id) of the wrapped Todo object in the response.
body: "any"
This indicates we want to incorporate the entire text content of the body property, regardless of its original data type. To include a property in the wrapped data, its definition in the wrap-only type must be equal or less restrictive compared to the original. In this case, "any" covers any possible data type for the body.
done: |this| is_datetime(this.completion)
This is where things get interesting! We're utilizing a computed property. This method dynamically examines the completion property of the wrapped Todo. If it finds a datetime value present, it sets the done flag to true, otherwise it remains false. This demonstrates the power of computed properties to not only select specific data but also transform it based on defined logic, adding new insights to the wrapped representation.
Fundamental to the design of wrap-only types is the restriction on direct instance creation. Let's confirm this behavior:
(//todos)>_Todo{};TypeError: type `_Todo` has wrap-only mode enabled
As expected, attempting to directly create an instance of the _Todo type throws an error.
To leverage the power of _Todo, we employ the wrap() method, available on thing objects. Let's see how it works:
(//todos)>Todo(2).wrap("_Todo");{ "body": "Read a book", "done": true, "id": 2 }
We've achieved a significant transformation. The wrapped Todo now exhibits the
            desired structure, including the calculated done property. However, one
            crucial element remains missing - the user information.
To incorporate user information, let's create another wrap-only type specifically for this purpose:
(//todos)>new_type("_TodoUser", true, true);"_TodoUser"
The first true argument activates the familiar wrap-only mode functionality.
            However, the inclusion of the second true argument introduces a privacy feature.
            By specifying this additional flag, we instruct ThingsDB to exclude the ID property within the response.
We'll keep the _TodoUser type simple, focusing on the essential name property:
(//todos)>set_type("_TodoUser", {name: "str"});null
Now, let's merge _TodoUser into our _Todo type:
(//todos)>mod_type("_Todo", "add", "user", "&_TodoUser?");null
Let's delve into the meaning of the "&_TodoUser?" definition assigned to the user property.
The ? symbol is crucial as it preserves the original nillability "User?"
            of the user property. If we omitted the ?,
            it would remove the nillability flag, making the definition stricter and preventing the user property
            to be included in a response.
The & symbol at the beginning of the "&_TodoUser?"
            definition is a prefix flag in ThingsDB. Several such flags exist, each influencing how properties are returned
            in a query response. Remember the optional deep value in the return
            statement? It is actually rarely needed to define this deep value. The &
            flag acts similarly, instructing ThingsDB to directly include the specified property in the response without
            manual deep level specification. It effectively maintains the same deep level as the parent property.
Table 10.1 - Summarizing prefix flags
| Flag | Description | 
|---|---|
"&" | Use parent's deep level | 
"+" | Force the maximum deep level of 126 (127 in parent) | 
"-" | Force deep level of 0 (1 in parent) | 
"^" | Apply NO_IDS return flag | 
"*" | Return enumerator names instead of values | 
            Now that we've incorporated the user property, let's witness the magic!
(//todos)>Todo(2).wrap("_Todo");{ "id": 2, "body": "Read a book", "done": true, "user": { "name": "Alice" } }
This output aligns perfectly with our desired format!
No need for a return statement or manual deep level specification! Our wrap-only types seamlessly weave together to-do information and the user's name, presenting a cohesive response in the desired format.
Now that we have a robust wrap-only type for to-do's, let's adapt the search_todos
            procedure to leverage its functionality.
Initially, we could employ the familiar map() method to wrap each filtered Todo instance:
.users.values().reduce(|total, user| {
    total |= user.todos.filter(|todo| todo.body.contains(needle));
    total;
}, set()).map(|todo| todo.wrap("_Todo"));
        However, ThingsDB offers a dedicated and more efficient option specifically designed for this common wrapping scenario:
            the map_wrap() method.
Update the the search_todos procedure by incorporating the map_wrap() method:
(//todos)>mod_procedure("search_todos", |needle| { "Search for to-do's"; .users.values().reduce(|total, user| { total |= user.todos.filter(|todo| todo.body.contains(needle)); total; }, set()).map_wrap("_Todo");// map_wrap() for efficient conversion});null
To confirm the successful integration of the Todo type, let's re-run
            the search_todos.py script:
$python search_todos.pySearch for:book[{'id': 2, 'body': 'Read a book', 'user': {'name': 'Alice'}, 'done': True}]
As expected, the output now reflects the desired structure, incorporating the to-do details and user information within the expected format.
In this chapter, we crafted the _Todo wrap-only type, but did you
            notice a key detail? We never explicitly specified the target type for wrapping.
            That's because wrap-only types possess remarkable versatility! Any thing, any instance, can be
            wrapped using _Todo, regardless of its original definition.
Let's observe what happens when we wrap an empty thing:
(//todos)>{}.wrap("_Todo");{ "done": "thing `#0` has no property `completion`" }
While not ideal, the result is understandable. The computed done property
            returns an error message as the wrapped thing lacks the required completion property.
Now, let's try wrapping an object with some properties:
(//todos)>{completion: nil, body: 123, smile: true}.wrap("_Todo");{ "body": 123, "done": false }
As expected, the done property is calculated correctly based on the
            provided completion property. Remember, the body
            property in _Todo is defined as "any",
            allowing it to accept various data types like integers here. Since smile is not defined
            in _Todo, it is simply ignored.
            In this chapter, we've explored creating wrap-only types and defining hide-ID behavior. While we covered the standard approach, you might be wondering what happens if you forget to set these flags initially?
The mod_type() function provides two key actions for modifying these
            configurations after creation: "wpo" for managing wrap-only mode
            and "hid" for controlling hide-ID behavior.
The following code demonstrates how to apply these actions in practice:
(//todos)>mod_type("_Todo", "wpo", true);null(//todos)>mod_type("_TodoUser", "hid", true);null
Enabling wrap-only mode for an existing type is restricted to when no instances of that type currently exist. Attempting to activate it with existing instances will result in an error.
We trust you've gained a solid grasp of the transformative potential of wrap-only types. By
            leveraging them effectively, you can streamline your code, eliminating custom deep levels
            within return statements and achieving your desired data structures
            effortlessly.
We wish you the best of luck with the quiz! In the next chapter, we'll delve into the exciting world of implementing flags and enumerators, further expanding your data manipulation capabilities within ThingsDB.
Both type A and B have an email property.
            On type A the property is defined as "email?". In which
            scenario will the email property appear in the response when an instance of
            type A is wrapped with type B?
Choose the correct definition(s) for the email property in type B:
"any""str""str?"email"Consider the following code snippet:
new_type("T", true, true);
What do the two boolean arguments in the provided new_type() function call signify?
true activates wrap-only mode, the second activates the hide-ID flag.true activates wrap-only mode, the second activates the show-ID flag.true activates the hide-ID flag, the second activates wrap-only mode.true activates the show-ID flag, the second activates wrap-only mode.Wrap-only types empower you to define properties or methods. What purpose do methods serve in this context?
Imagine you have a variable people holding a list of things of
            type Person. Your task is to return this list with each Person
            instance transformed and represented using the _Person wrap-only type. Write the most concise and efficient statement possible to achieve this transformation.
Imagine you're creating a wrap-only type to transform existing instances of the Person type. Which of the following names are valid for your new wrap-only type?
Beer_Personperson_PersonCompactWhat happens when a computed property encounters an error during its calculation? Choose the correct answer:
nil.Both "str?" and "any" can inherit
            the email property due to their less restrictive nature compared to the
            original "email?" definition. This follows the principle of compatibility
            where definitions should be either the same or less restrictive for properties to appear.
            While "str" enforces string type, it loses the nillability aspect from
            the original. On the other hand, "any" allows any type,
            including nil, effectively inheriting the nillability due to its broadness.
Answer "a". The first true enables wrap-only mode, and the second true activates the hide-ID flag. Both arguments default to false if omitted.
Methods in wrap-only types define computed properties: properties whose values are dynamically calculated on response creation.
The most efficient way to achieve this transformation is using the built-in map_wrap()
            method. Simply calling people.map_wrap("_Person"); will wrap each Person object in the
            list with the _Person type. While other methods like map() can
            also be used, map_wrap() offers the most concise and efficient solution in this case.
All the proposed names technically work. However, best practices favor naming conventions for clarity
            and consistency. Options like _Person or the more descriptive
            name _PersonCompact using the underscore prefix instantly reveal the
            wrap-only purpose, enhancing code understanding and avoiding confusion.
The correct answer is "c". ThingsDB handles errors in computed properties gracefully by incorporating the error message itself as the property's string value within the result. This approach ensures visibility of any issues that may arise during calculation, promoting transparency and facilitating troubleshooting.
This chapter equips you with more powerful tools to manage data efficiently in ThingsDB: bitwise flags, regular expressions and enumerators. Let's jump right in with enumerators!
Consider a severity property for your to-do items. An enumerator allows you
        to establish a finite list of valid options, such as "Low", "Medium", and "High". This enforces data
        integrity by ensuring only permitted values are assigned, preventing inconsistencies or invalid entries
Unlike conventional data types, enumerators in ThingsDB are created directly using
            the set_enum() function. Here's the code to define the Severity enumerator
            for our to-do application:
(//todos)>set_enum("Severity", { Low: 0, Medium: 1, High: 2,// Optional method for customized string representationstr: |this| `{this.name()} ({this.value()})`, });null
Key Technical Properties:
str() method in our example, used for custom string formatting.Like other ThingsDB elements, enumerators possess default members. To avoid unexpected behavior,
                it is highly recommended to explicitly define these defaults using the mod_enum()
                function. Here's how to set Medium as the default member for
                the Severity enumerator:
(//todos)>mod_enum("Severity", "def", "Medium");null
To verify that the default member is correctly configured, simply call the Severity enumerator without providing a specific value to get the default member:
(//todos)>Severity();1
As demonstrated, when a member is returned in a response, its underlying value is displayed by default.
                In this case, 1 (representing Medium) is returned.
Each enumerator member in ThingsDB offers two built-in methods: name()
                and value(). These methods provide programmatic access to the
                enumerator's name and value, respectively. Additionally, any custom methods defined within
                the enumerator become available to its members.
Let's delve into practical examples using the Severity enumerator we defined earlier:
(//todos)>Severity().name();"Medium"(//todos)>Severity().value();1
As expected, name() returns the string "Medium",
                while value() retrieves the corresponding value, 1.
Remember the optional str() method we defined for formatting? We can access it through a member:
(//todos)>Severity().str();"Medium (1)"
This demonstrates how custom methods enhance the functionality of enumerator members.
Need a member with a specific value? Simply pass the value directly to the enumerator constructor:
(//todos)>Severity(2).name();// Value 2 for High"High"
This straightforward approach is ideal when the value is readily available.
Know the enumerator name beforehand? Use static access with curly braces:
(//todos)>Severity{High}.str();"High (2)"
This method is useful when the name is known and fixed.
Working with dynamic names stored in variables? Employ a closure within curly braces:
(//todos)>name = "High"; Severity{||name};2
While the example might seem unusual, the name variable is simply for demonstration. In practice, you might receive it as an input argument or retrieve it dynamically. This method ensures security against code injection vulnerabilities, as described in Chapter 6.2.3.
When you attempt to access an enumerator using an invalid value or name, it raises an error instead of silently continuing. This prevents unexpected behavior and ensures data accuracy.
Here's what happens when you try to use non-existent values or names:
(//todos)>Severity(9);LookupError: enum `Severity` has no member with value 9(//todos)>Severity{Insane};LookupError: enum `Severity` has no member `Insane`(//todos)>Severity{||"NotAtAll"};LookupError: enum `Severity` has no member `NotAtAll`
As you can see, ThingsDB throws a lookup_err in all cases,
                clearly indicating the invalid input.
ThingsDB offers enums_info() to explore all defined enumerators within a collection, returning a list
            of information objects. Each object encapsulates details like its name, members, and methods.
For a specific enumerator, use enum_info(), which retrieves a single
            information object. Remember that information methods like enum_info()
            return "mpdata" requiring further processing. Utilize the load() method to
            convert it into a usable structure where you can extract individual properties.
For instance, verifying the default member of the Severity enumerator involves:
(//todos)>enum_info("Severity").load().default;"Medium"
This code retrieves the Severity information object, loads it,
            and extracts the default property,
            revealing the default member: "Medium".
Accessing an enumerator's member can be valuable for integrating with other platforms or systems. Ideally, we want these members readily available as a single "thing" for simpler handling.
While enum_info() exposes members, processing them requires code to
                convert the returned list of tuples into a desired format. Here's an example
                using reduce():
(//todos)>enum_info("Severity").load().members.reduce( |t, m| {t.set(m[0], m[1]); t;}, {} );{"Low": 0, "Medium": 1, "High": 2}
As you can see, this approach involves several steps and can be cumbersome.
Fortunately, ThingsDB provides a built-in solution: enum_map().
                This function directly returns the enumerator members as a convenient "thing":
(//todos)>enum_map("Severity");{"Low": 0, "Medium": 1, "High": 2}
Need to modify an existing enumerator after its creation? Don't worry, the mod_enum()
            function comes to the rescue! We previously saw it in action for setting the default member.
            But its power extends beyond that. It offers a versatile suite of actions to tailor your
            enumerators to your evolving needs.
Here's a quick reference table summarizing the available actions:
Table 11.3 - Summary of mod_enum() actions
| Action | Description | 
|---|---|
"add" | Adds a new member or method to the enumerator | 
"def" | Sets the default member for the enumerator | 
"del" | Deletes a member or method from the enumerator | 
"mod" | Modifies a member value or method closure | 
"ren" | Renames a member or method | 
While we've created a custom Severity enumerator, ThingsDB offers other
            built-in solutions for common purposes. Let's briefly compare these options before proceeding.
Recall that ThingsDB empowers you with precise control over integer values using range definitions.
                Let's revisit how to implement this for the severity property within
                the Todo type:
(//todos)>mod_type("Todo", "add", "severity", "int<0:2:1>");null
This code snippet concisely bolsters data integrity within the Todo type
                by introducing the severity property. This new property restricts
                values to integers within the strict 0-2 range, effectively preventing
                invalid input. Furthermore, a default value of 1 is established,
                ensuring a meaningful starting point for both existing and new instances. Notably, this constraint
                is retroactively applied to all existing Todo instances, guaranteeing
                data integrity throughout the entire dataset.
Let's demonstrate the default value and error handling of the severity property:
Creating a Todo instance without specifying severity assigns the default value:
(//todos)>Todo{}.severity;1
Attempting to create a Todo with an invalid severity triggers an error:
(//todos)>Todo{severity: 99};ValueError: mismatch in type `Todo`; property `severity` requires an integer value between 0 and 2 (both inclusive)
While enumerators offer a structured and type-safe approach to defining value sets, you can also achieve similar results using regular expressions. ThingsDB supports regular expressions through the "regex" data type.
Here's how to write a regular expression in ThingsDB:
/pattern/flags
            Regular expressions in ThingsDB go beyond just validating strings. They act as powerful tools for
                various string manipulation tasks. By using string methods like replace()
                and split(), you can transform and extract portions of strings
                based on defined patterns.
A key method for pattern matching is the regex method test(), which
                returns true if a given string matches the pattern,
                and false otherwise.
Let's see the test() method in action:
(//todos)>/^(Low|Medium|High)$/.test("Low");true(//todos)>/^(Low|Medium|High)$/.test("MEDIUM");false(//todos)>/^(Low|Medium|High)$/i.test("LOW");// Case-insensitive matchingtrue
While crafting regular expressions is beyond this book's scope, let's briefly break down the pattern used in the example:
^: Matches the beginning of the string.(Low|Medium|High): Matches one of the listed options ("Low", "Medium", or "High").$: Matches the end of the string.i flag: Enables case-insensitive matching.Leveraging the understanding of regular expressions, let's define the severity
                property using a pattern-based approach. Remember, definitions in ThingsDB are always strings. We
                encapsulate the regular expression within quotes and specify a default value. Crucially, this
                default value must strictly adhere to the defined pattern for successful definition acceptance.
(//todos)>mod_type( "Todo", "mod", "severity", "/^(Low|Medium|High)$/<Medium>", |this| ["Low", "Medium", "High"][this.severity], );null
Key Points:
/^(Low|Medium|High)$/) to constrain valid severity values to "Low", "Medium", or "High".<Medium> suffix sets the default value to "Medium". Remember, default values must adhere to the defined pattern for acceptance.0, 1, 2) to their corresponding string equivalents ("Low", "Medium", "High").Let's validate the default value and constraint for the string-based severity property:
Creating a new Todo without specifying severity assigns the default value:
(//todos)>Todo{}.severity;"Medium"
Attempting to create a Todo with an invalid severity triggers an error:
(//todos)>Todo{severity: "Unknown"};ValueError: mismatch in type `Todo`; property `severity` has a requirement to match pattern /^(Low|Medium|High)$/
Having explored both integer range and string-based solutions, let's migrate
                the severity property to leverage our custom Severity enumerator:
(//todos)>mod_type( "Todo", "mod", "severity", "Severity", |this| Severity{||this.severity}, );"Medium"
As before, a migration closure is necessary due to the change in data representation. This closure
                translates existing string values ("Low", "Medium", "High") to
                their corresponding enumerator members (Severity{Low}, Severity{Medium}, Severity{High}).
To confirm successful migration, let's create a new Todo with default severity:
(//todos)>Todo{}.severity.str();"Medium (1)"
Excellent! We can now leverage the str() method we defined, indicating that
                the severity property is now strictly bound to the enumerator's members.
We can further elevate the _Todo wrap-only type by incorporating
                the severity property. Additionally, ThingsDB offers the
                optional * prefix flag to specify returning the enumerator
                member's name instead of its value. Let's demonstrate this in action:
(//todos)>mod_type("_Todo", "add", "severity", "*Severity");null
Now, let's test this modification on an empty Todo:
(//todos)>Todo{}.wrap("_Todo");{ "body": "-", "done": false, "severity": "Medium", "user": null }
As you can see, the severity property now displays the member
                name "Medium" instead of its numerical
                value (1).
Before we proceed with the quiz, let's explore a use case for flags in ThingsDB.
            Before utilizing flags, we need to establish them within your ThingsDB project. Enumerators provide a convenient and safe approach for this task.
While it may seem unrelated to our to-do app, let's imagine Alice and Bob, with no more tasks
                remaining, decide to play cards. We'll create a Suits enumerator
                to represent the four card suits:
(//todos)>set_enum("Suits", {Spades: 1<<0,// 0b0001 (1)Clubs: 1<<1,// 0b0010 (2)Diamonds: 1<<2,// 0b0100 (4)Hearts: 1<<3,// 0b1000 (8)});null
We've used the bitwise left shift operator (<<) to assign
                values to each suit. While not mandatory, this approach helps prevent errors during
                flag definition, as it leverages the inherent binary representation of integers. Using
                simple numbers (1, 2,
                4, 8) would also work, but
                bit shifting offers additional safety and clarity.
Now that our Suits enumerator is defined, let's delve into how
                to use these flags effectively in your code.
                Bitwise OR (|) Operator: Combines multiple flags into a single variable:
(//todos)>// Sets both Spades and Hearts flagsmy_suits = Suits{Spades}.value() | Suits{Hearts}.value();9
Bitwise OR-Equals (|=) Operator: Adds a flag to an existing value:
(//todos)>my_suits = Suits{Spades}.value();// Initially set Spades flagmy_suits |= Suits{Hearts}.value();// Adds Hearts flag9
Bitwise AND (&) Operator: Checks if a specific flag is present:
(//todos)>my_suits = Suits{Spades}.value() | Suits{Hearts}.value();// Test for Hearts flagassert(bool(my_suits & Suits{Hearts}.value()) == true);// Test for Diamonds flagassert(bool(my_suits & Suits{Diamonds}.value()) == false);null
Bitwise NOT (~) Operator: Inverts all bits in a value, effectively removing a specific flag when combined with bitwise AND:
(//todos)>my_suits = Suits{Spades}.value() | Suits{Hearts}.value(); my_suits &= ~Suits{Hearts}.value();// Unsets the Hearts flag1
Bitwise XOR (^) Operator: Flips the bits of a specified flag, switching its state on or off:
(//todos)>my_suits = Suits{Spades}.value() | Suits{Hearts}.value();my_suits ^= Suits{Hearts}.value();// Toggles the Hearts flagmy_suits ^= Suits{Diamonds}.value();// Toggles the Diamonds flag5
Flags offer a powerful and compact way to store multiple boolean values within a single integer, enhancing data efficiency and representation. This can be particularly advantageous for managing access control, permissions, and various configuration settings.
                Best of luck with the quiz! In the next chapter, we embark on the exciting journey of events and rooms in ThingsDB, where you'll explore powerful mechanisms for real-time communication and data interactions within your applications.
Select all statements that are true about enumerators:
new_enum() function.While creating an enumerator, why is it recommended to use mod_enum() at least once?
Imagine you have a variable x containing the name of a member within the Colors enumerator. How would you retrieve the corresponding member using that variable?
What built-in methods are available on every member of an enumerator?
What does the * prefix flag signify when used in a type property definition for an enumerator? Explain its purpose and impact on data representation.
Write a statement using a regular expression that checks if the string stored in the variable animal matches either "Dog" or "Cat", regardless of case sensitivity.
Which code snippet accurately initializes the Colors enumerator with R, G, and B members so they can be independently used as distinct flags for bitwise operations?
set_enum("Colors", {R: 0, G: 1, : 2});set_enum("Colors", {R: "#FF0000", G: "#00FF00", B: "#0000FF"});set_enum("Colors", {R: 1, G: 2, : 3});set_enum("Colors", {R: 1<<0, G: 1<<1, : 1<<2});The correct choices are "a" and "c". Enumerators enforce strict data type consistency for their members and require unique names and values. While new_enum() might seem fitting, remember that ThingsDB uses set_enum() to create enumerators. Enumerators are mutable, allowing you to add, rename, delete, and change members and methods using the mod_enum() function.
Setting a default value using mod_enum() is generally recommended for enumerators, particularly those used within a type to influence its behavior. While less crucial for enumerators solely used for storing values like flags, defining a default value through mod_enum() provides additional control and clarity in various use cases.
To dynamically retrieve the Colors member based on the name stored in x, you can use a closure expression enclosed in curly braces:
Colors{||x};
The two basic methods accessible to every enumerator member are:
name(): This method returns the string name of the member as defined in the enumerator declaration.value(): This method returns the value associated with the member.Remember that enumerators themselves can have additional methods defined beyond these two, depending on how they were created using set_enum() or mod_enum().
The * prefix flag, when used in a type property definition for an enumerator, influences the value returned when the instance is wrapped for the response. It instructs the system to return the name of the corresponding enumerator member instead of its value.
The following regular expression, combined with the test() method, can be used to check if the variable animal matches either "Dog" or "Cat" (case-insensitive):
/^(dog|cat)$/i.test(animal);
The correct answer is "d":
set_enum("Colors", {R: 1<<0, G: 1<<1, : 1<<2});
This option uses the bitwise left shift operator (<<) to assign distinct binary values to each member. Answer "a" and "c" are using consecutive integer values (0, 1, 2) and (1, 2, 3) which do not provide distinct binary representations for bitwise operations. Answer "b" is using string values for their members, which cannot be used for bitwise operations.
Imagine Alice and Bob want to build a live dashboard displaying the number of pending to-do's for all users in their to-do application. However, simply polling the collection every few seconds is inefficient and creates unnecessary network traffic.
Enter the world of Events and Rooms in ThingsDB!
        This chapter focuses on a specific type of event: those emitted to rooms.
Think of a room as a lightweight, dedicated communication channel within your collection. Each room receives a unique ID when it is associated with the collection, similar to individual things.
Creating a room is simple:
(//todos)>room();"room:nil"
You'll see "room:nil" because no ID has been assigned yet. Assigning the
            room to the collection grants it an ID, just like how it works with things.
Since we converted the root of the collection to type Root back
            in Chapter 7.2, modifying its structure requires
            the mod_type() function. We'll add a property named dashboard of type
            room to the Root type.
(//todos)>mod_type("Root", "add", "dashboard", "room");null
            Let's verify if the room was created by accessing the dashboard property:
(//todos)>.dashboard;"room:12"
You should see a unique room ID like "room:12", indicating a successfully
            created room associated with the collection.
We can retrieve the plain ID of the dashboard room using the id() method.
            This method comes in handy when we later want to join the room for event listening.
(//todos)>.dashboard.id();12
With the "dashboard" room in place, we're ready to emit our first event!
The process is simple: use the emit() method on your room object.
                The first argument is the event name, a string between 1 and 255 characters.
                You can optionally include additional arguments that will be sent along with the event.
Here's an example of sending a "chat-message" event:
(//todos)>.dashboard.emit("chat-message", "Who's listening???");null
Remember, no one has joined the room yet. So, while the event was successfully emitted, no one received the "chat-message".
                Just like before, we'll demonstrate using Python to create a listener for the "dashboard" room. Remember, the concepts are transferable to other languages like C#, Go, PHP or JavaScript/Node.js with minor adjustments.
Before proceeding, let's create a dedicated user for the dashboard. We can leverage the
            same new_todo_user() procedure we established in Chapter 6.4:
(//todos)>@t(@t)>wse(); new_todo_user("dashboard", JOIN | RUN | QUERY);"14VdoACr4WydUi/PEi1MV6"
This grants the "dashboard" user:
JOIN: To subscribe to the room and receive events.QUERY: To retrieve the room ID (needed for joining).RUN: To execute a future procedure for grabbing the initial state.Save the following code as dashboard.py:
import asyncio from thingsdb.client import Client from thingsdb.room import Room, event class Dashboard(Room): @event("chat-message") def on_chat_message(self, message): print(message) async def main(): client = Client() client.set_default_scope('//todos') room = Dashboard("""//ti .dashboard.id();// This must return the room ID""")# Fetch the dashboard room ID dynamicallyawait client.connect('localhost')# Replace with your address/porttry:# Authenticate with the dashboards user's tokenawait client.authenticate('14VdoACr4WydUi/PEi1MV6') await room.join(client)# Join the dashboard roomawait asyncio.Future()# Run foreverfinally: client.close() await client.wait_closed() asyncio.run(main())
Open a terminal and run the script:
$python dashboard.py…
If successful, you'll see no immediate output. The listener is now actively waiting for events in the background.
With the listener running, let's send a test message from the //todos scope:
(//todos)>.dashboard.emit("chat-message", "Hello dear listeners");null
If everything is set up correctly, you should see the following output in the terminal
                running your dashboard.py script:
$ python dashboard.pyHello dear listeners…
This confirms that your listener successfully received and printed the message!
While sending chat messages is fun, our ultimate goal is to create a functional dashboard. Like many real-world applications, the dashboard needs to fetch an initial state, in this case, the current number of uncompleted tasks for each user.
First, we need a way to represent this information. Let's create a new wrap-only type named _DashboardUser:
(//todos)>new_type("_DashboardUser", true);"_DashboardUser"
Next, we define the structure of _DashboardUser:
(//todos)>set_type("_DashboardUser", { id: "#", name: "str", count: |this| list(this.todos).count(|todo| is_nil(todo.completion)), });null
The _DashboardUser type defines three properties: id to
            identify a user, name for retrieving user names and count for
            calculating uncompleted to-do's using a computed property.
Finally, we create a procedure named get_dashboard_state that retrieves and formats the initial data:
(//todos)>new_procedure("get_dashboard_state", || { .users.values().map_wrap("_DashboardUser"); });"get_dashboard_state"
Let's check if it works:
(//todos)>get_dashboard_state();[ {"id": 10, "count": 2, "name": "Alice"}, {"id": 11, "count": 0, "name": "Bob"} ]
This confirms that the procedure successfully retrieves the desired initial state.
Now that we have the get_dashboard_state procedure, let's integrate
                it into the dashboard application:
class Dashboard(Room):def on_init(self): # Called only once self.state = [] async def on_join(self): # Called when joining the room self.state = await self.client.run("get_dashboard_state") self.print_state() def print_state(self): print('-' * 20) for user in self.state: name, count = user["name"], user["count"] print(f"{name:<15}{count:>5}")
Initial state in on_init():
The on_init() method, inherited from Room, is
                    called once during room initialization. We use it to initialize an empty state.
Updating state on join in on_join():
The asynchronous on_join() method, called when joining the room,
                    retrieves the current state using get_dashboard_state and updates the state.
                    It then calls print_state to display the information.
                    Custom print_state() method:
This custom method formats and prints the user names and uncompleted to-do counts from the state.
Running dashboard.py again will now print the state:
$python dashboard.py-------------------- Alice 2 Bob 0
Now we need to handle events that indicate changes to the data, ensuring our dashboard reflects the latest information.
Identifying state-changing procedures and methods:
add_todo: Adds a new task to a user's list.add_user: Creates a new user.mark_as_done: Marks a task as completed.Here's the code to update the procedures and methods:
(//todos)>mod_procedure("add_todo", |name, body| { "Adds a to-do to the list."; todo = Todo{body:,}; .users[name.lower()].todos.add(todo); .dashboard.emit("add-todo", todo.user.id());// New linetodo.id(); }); mod_procedure("add_user", |name| { "Add a new to-do user."; uid = name.lower(); assert(!.users.has(uid)); user = .users[uid] = User{name:,}; .dashboard.emit("add-user", user.wrap("_DashboardUser"));// New lineuser.id(); }); mod_type("Todo", "mod", "mark_as_done", |this| { .dashboard.emit("mark-as-done", this.user.id());// New linethis.completion = datetime(); });null
Implementing the event handlers in dashboard.py:
class Dashboard(Room):@event("add-todo") def on_add_todo(self, user_id):# Update user's to-do count and display updated stateself.get_user(user_id)["count"] += 1 self.print_state() @event("add-user") def on_add_user(self, user):# Add user to state and display updated stateself.state.append(user) self.print_state() @event("mark-as-done") def on_mark_as_done(self, user_id):# Update user's to-do count and display updated stateself.get_user(user_id)["count"] -= 1 self.print_state() def get_user(self, user_id):# Find user object based on IDfor user in self.state: if user["id"] == user_id: return user# ... other methods
Restart your dashboard.py script and have some fun:
Example output:
$python dashboard.py-------------------- Alice 2 Bob 0 -------------------- Alice 2 Bob 1 -------------------- Alice 1 Bob 1 -------------------- Alice 1 Bob 1 Charlie 0 …
As you interact, the dashboard dynamically updates the list, displaying the latest to-do counts for each user in real-time. This demonstrates the power of event-driven communication between your application and ThingsDB!
We hope this chapter provided a solid foundation for using events and rooms in ThingsDB. While it does not cover everything, it equips you to start building interactive applications.
Expand your knowledge! For deeper insights into rooms and events, explore the client website specific to your chosen language. This website likely contains detailed information and examples beyond what is covered here. Do not miss the Connectors section of this book for quick access to these valuable resources.
Who will receive the message "Hello World" in the following statement?
room().emit("new-message", "Hello World");
Remember: Rooms must have an ID before they can be joined.
What access flag is required for a program to join a room in ThingsDB?
QUERYCHANGERUNJOINWhat method is available to get a room ID?
What is the most appropriate point to synchronize the state of a Room with the current state of the collection?
on_init(..) (During room initialization)on_join(..) (When joining the room)on_emit(..) (When emitting an event)Which of the following are valid event names?
"123""new-building""stop the program!""""ADD_USER"Consider the following Python code snippet belonging to a Room class that listens to the .math room:
    @event("add-two-numbers")
    def on_add_two_numbers(self, a, b):
        print(f"{a} + {b} = {a + b}")
The observed output is:
8 + 5 = 13
Can you write the ThingsDB code that could have triggered this event, resulting in the displayed response?
No one will receive the message "Hello World" because the statement lacks a room ID. Rooms require an ID for joining, and without one, a program cannot join the intended room.
The absolute requirement for joining a room in ThingsDB is the JOIN access flag. While the QUERY flag is not strictly mandatory, it offers valuable utility. It allows you to retrieve the room ID dynamically using a query.
The id() method is directly available on the room object and returns the ID as integer. While technically possible, extracting the ID from the string representation of the room object is less efficient and generally discouraged. The room object itself returns a string in the format room:ID.
Answer "b". The most appropriate point to synchronize the state with the current collection in a Room implementation is on_join(..). While the on_init(..) could be used for initial state retrieval, it would not account for potential changes after a connection loss. The on_join(..) handler is called every time a room is joined, making it ideal for ensuring the latest state is retrieved from the collection.
All event names are valid except for empty strings (answer "d"). Remember, event names need at least one character and cannot exceed 255 characters.
The following ThingsDB code could have triggered the "add-two-numbers" event, leading to the observed output:
.math.emit("add-two-numbers", 8, 5);
Ready to push your ThingsDB projects further? This chapter dives into the world of modules, unlocking a vast array of functionalities to extend your applications beyond the core features.
Imagine sending automated emails, interacting with external APIs, storing files in the cloud, or even connecting to other databases – modules make these and more possible!
But before we explore the exciting world of modules, let's take a quick detour and understand a key concept: futures in ThingsDB.
By understanding futures, you'll be well-equipped to harness the true power of modules and unlock the full potential of your ThingsDB projects.
Think of futures as placeholders for tasks running in the background, allowing your code to continue without getting stuck waiting.
We'll start by creating an "empty future", which doesn't have a specific task but still waits for a query to complete before executing asynchronously:
(//todos)>future(nil);[ null ]
This code creates a future without any specific task. It simply waits for the query to finish and
            then executes asynchronously. The response you see ([null]) is the
            result of the future task (which, in this case, is nothing).
We have seen how futures hold the place for background tasks. But what if we want to do something
                with the result of that task once it is finished? That is where the then()
                method comes to the rescue!
Imagine you delegate a task to a friend. When they finish, they share the result.
                The then() method acts like your friend, waiting for the future to finish and
                then running a closure you provide with the result.
Here is an example:
(//todos)>future(nil).then(|result| is_nil(result));true
As you can see, the query response now shows the outcome of the
                closure (true in this case).
The closure provided to the then() method operates in a separate context
                than your main code. It cannot directly access variables defined outside, like in this example:
(//todos)>x = 10; future(nil).then(|| x + 10);LookupError: variable `x` is undefined
So, how do we use existing variables within a future's closure? We simply pass them along! Wrap them
                up and send them together with the future using the future() function:
(//todos)>x = 10; future(nil, x).then(|_, x| x + 10);20
Specifying nil as an argument and then ignoring it could get a bit tedious. Well, ThingsDB offers a handy shortcut!
Instead of then(), simply provide the closure as the first argument
                to future(). This magical closure inherits variables from your main context,
                eliminating the need to pass them explicitly:
(//todos)>x = 5; future(|x| x + 10);// No more `nil`s!15
As you noticed, the task result is not explicitly mentioned, but since it is an empty future,
                we know it is nil anyway.
Do you want to pass the arguments explicitly to the closure? No problem! Wrap them in a list as
                the second argument to future():
(//todos)>future(|x| x + 10, [8]);// `x` is provided by the first item in the list18
By leveraging this simplified syntax, you can create and work with empty futures in a more concise and efficient way, making your asynchronous operations in ThingsDB cleaner and easier to manage.
You might wonder, "Why bother with empty futures?" The key lies in their ability to create separate contexts for closures, unlocking hidden potential.
Imagine this scenario in our to-do application: we need a procedure to retrieve or create a user and then grab their ID (or other information, but let's focus on ID for now).
One way to tackle this challenge is the following implementation:
(//todos)>new_procedure("get_or_create_user_id", |name| { user = .users.get(name.lower()); if (is_nil(user)) { wse();// Enforced Side effect for the add_user() callreturn add_user(name); }; user.id(); });"get_or_create_user_id"
While this works for both existing and new users, it has a flaw: it triggers side effects even when a user already exists. This can be inefficient, especially if the procedure is called frequently.
Here's how we can use an empty future to isolate side effects and improve efficiency:
(//todos)>mod_procedure("get_or_create_user_id", |name| { user = .users.get(name.lower()); if (is_nil(user)) { return future(|name| { wse();// Side effect now contained within the futureadd_user(name); }); }; user.id(); });null
Now, the main branch retrieves the existing user ID without causing a change. If a new user needs to be created, the future's closure handles it asynchronously, ensuring updates only happen when necessary.
Remember from Chapter 6.3 that we can verify if a procedure triggers
                side effects by checking the with_side_effects property. Let's confirm
                that our modified get_or_create_user_id procedure is truly free of them:
(//todos)>procedure_info("get_or_create_user_id").load().with_side_effects;false
The value false indicates no side effects are detected within the main logic of the procedure.
Before you store a future in your ThingsDB project, it is crucial to understand a key quirk: futures store their current state, not the eventual result. Just assigning a future to a variable works as expected, but attempting to store it in collections or object properties can lead to unexpected results.
Let's explore the scenario:
Imagine you create a future that calculates 2 + 2 and store it in a list:
(//todos)>[future(|| 2 + 2)];[ null ]
You might expect the list to contain [4]. However, the actual result
                is [null]. Why? Because you are not storing the future itself,
                but rather its initial state, which is nil before the calculation finishes.
Now that you are familiar with futures, let's explore modules, which can significantly expand ThingsDB's capabilities. Modules can unlock various features like sending emails, connecting to diverse databases, or making HTTP(S) requests.
Module Types:
For this book, we'll focus on pre-built binary modules, readily available and requiring no additional coding on your part.
            Module installation in ThingsDB happens smoothly within the /thingsdb scope
                using the new_module() function. Just give your module a friendly alias
                (like "demo"), then provide the corresponding GitHub repository URL where
                it resides.
                Switch to the /thingsdb scope and install the demo module:
(//todos)>@t(@t)>new_module('demo', 'github.com/thingsdb/module-go-demo');null
Once you have installed the module, use the module_info() function to
                confirm its success.
(@t)>module_info("demo");{ "conf": null, "created_at": 1707931759, "doc": "https://github.com/thingsdb/module-go-demo#readme", "exposes": { "echo": { "argmap": ["message"], "defaults": {"load": true}, "doc": "Echo message" } }, "file": "/usr/lib/thingsdb-modules/demo/bin/demo_linux_amd64.bin", "github_owner": "thingsdb", "github_ref": "default", "github_repo": "module-go-demo", "github_with_token": false, "name": "demo", "scope": null, "status": "running", "version": "0.1.0" }
                Now that the demo module is installed, let's explore how to use its capabilities
                by understanding the methods it is exposing.
"echo": {
    "argmap": ["message"],
    "defaults": {"load": true},
    "doc": "Echo message"
}
            Every module exposes specific methods. For demo, it is the echo() method.
                This method takes a single argument, a message, and echoes it back.
                The load attribute being set to true means the
                echoed message will be loaded directly into ThingsDB, making it readily accessible.
                Let's test the echo() method:
(@t)>demo.echo("Hello Module!");[ "Hello Module!" ]
As you see, the result is a list containing the echoed message. This is because calling a module method ultimately returns a future, similar to our previous future examples. However, in this case, the future holds the actual result (the echoed message) instead of being empty.
We can use the familiar then() method to capture and manipulate the echoed message:
(@t)>demo.echo("Hello Module!").then(|result| result.upper());"HELLO MODULE!"
Here, then() grabs the echoed message ("Hello Module!"),
                converts it to uppercase, and returns the result.
                This section demonstrates another powerful module: the HTTP(S) request module. It allows you to seamlessly interact with other web services and APIs.
                Install the requests module using this code:
(@t)>new_module("requests", "github.com/thingsdb/module-go-requests");null
Verify the installation:
(@t)>module_info("requests").load().status;"running"
The requests module boasts a powerful post_json() method.
                Watch how it sends a message to your NTFY app:
(@t)>requests.post("https://ntfy.sh/", json_dump({ topic: "ThingsDB_Book", message: "This book is fantastic!" })).then(|| "OK").else(|err| err.msg());"OK"
Check your NTFY app – you should see the "This book is fantastic!" message.
Just like then(), else() is available for all
                futures and plays a crucial role in error handling. It defines what happens when a request or task
                encounters an error.
While else() is not needed for simple futures that always resolve successfully,
                it becomes essential when dealing with external sources like NTFY. These external systems can potentially
                fail due to network issues, server errors, or other unforeseen circumstances.
                If both else() and then() are defined, only one
                will be called based on the outcome (success or error).
While the modules we have seen so far work out-of-the-box, others require configuration for specific tasks.
                Let's explore the thingsdb module, which allows communication across
                different ThingsDB scopes.
You might think, "We're already in ThingsDB, why connect to it again?". This module unlocks the ability to communicate between different scopes within the same ThingsDB node or even connect to separate ThingsDB clusters. Imagine you have data stored in a different scope that you need in your current scope, or you want to trigger actions across multiple clusters. This module makes it possible!
As always, installation comes first:
(@t)>new_module("thingsdb", "github.com/thingsdb/module-go-thingsdb");null
ThingsDB requires proper authentication. To grant the module access, let's create a dedicated user.
                We can leverage the same new_todo_user() procedure we established
                in Chapter 6.4:
(@t)>wse(); new_todo_user("module", RUN);"1w1WtiUZqSpCEX+B3p/Dtq"
With the token in hand, we'll configure the module using set_module_conf().
Here's how:
(@t)>set_module_conf("thingsdb", { token: "1w1WtiUZqSpCEX+B3p/Dtq",// Replace with your tokenhost: "localhost", });null
The function set_module_conf() operates asynchronously, meaning it initiates
                the configuration process in the background and returns nil immediately.
To confirm successful configuration, always use module_info():
(@t)>module_info("thingsdb");{ "conf": { "host": "localhost", "token": "1w1WtiUZqSpCEX+B3p/Dtq" }, "status": "running",… // – other information}
Look for a status of "running" and correct configuration in
                the conf section.
Troubleshooting:
status shows an error, carefully examine the message for guidance.DEBUG for more information in the node console (see Chapter 15.4 on how to enable debug logging).module_info() within a node scope. This unlocks the number of active tasks and restarts. Active tasks reveal current workload on that node, potentially indicating bottlenecks. Restarts provide insight into module stability, highlighting frequent restarts as potential red flags.Now that the ThingsDB module is configured, let's put its cross-scope communication power to the test!
We'll use the search_todos() procedure to find to-do's, even though we're currently in the /thingsdb scope:
(@t)>thingsdb.run("//todos", "search_todos", ["book"]).then(|res| res);[ { "body": "Read a book", "done": true, "id": 2, "severity": "Medium", "user": {"name": "Alice"} } ]
As you can see, we successfully retrieved a to-do containing "book" from the //todos scope! This demonstrates the magic of cross-scope communication enabled by the ThingsDB module.
By default, all installed modules are accessible by any scope. This is indicated
                by a nil value in the scope property
                of the module information.
To limit a module's accessibility to a specific scope, use the set_module_scope() function.
                Remember, each module can only have one assigned scope at a time, making it either globally accessible or
                scoped to a single entity.
If you need the same module with different configurations, or with access restrictions to different scopes, consider installing it multiple times with different names. This allows you to tailor each instance to its specific needs.
For example: Imagine connecting to different ThingsDB clusters. Install the thingsdb module twice with
                unique names, each configured to access a different cluster.
Modules are constantly evolving, so you might need to update them to benefit from new features and
                bug fixes. The refresh_module() function helps you out here. It stops the
                module, checks for available updates, performs the update if necessary, and restarts the module.
For granular control over module source updates, leverage the deploy_module() function.
                While sharing similarities with refresh_module(), it offers the crucial
                capability to specify a new source for the module. This empowers you to, for instance, "pin" the module to a
                specific version, ensuring a stable and predictable environment. This is particularly valuable when working
                with dependencies or critical modules where consistent behavior is paramount.
Can you simplify the following code while preserving its functionality and using a future?
future(nil, 6, 7).then(|_, a, b| a * b);
Can you answer this tricky question: Is it possible to directly return two futures in a single query result in ThingsDB?
You encounter an exposed method within a module, and its load property has a default value of true. What does this imply?
You are working with a ThingsDB module named foo. Its exposed method bar() has an intriguing argmap property set to ["*"]. Consider the following code snippet:
foo.bar(nil, 42).then(|A, B| nil).else(|C, D| nil);
Choose the most accurate statement about this call to the bar() method:
A and D will be nil regardless of success or failure.A will be nil and B will contain the method's response.B will be 42 and on failure, C will be 42.A will contain the response and on failure, C will hold the error.Important: The "*" in an argmap signifies a single, unnamed argument that can hold either nil or a thing
Uh oh! A module in your ThingsDB application isn't behaving as expected. What steps can you take to diagnose the issue and get it back on track?
Choose the most effective troubleshooting approach from the following:
module_info() function to gather information and check the node console logs.Scenario: You have a module pinned to version v0.2.1 using the @ notation. Now, a newer version (v0.3.0) with exciting features is available. How can you seamlessly update the module to tap into these enhancements?
ThingsDB offers a clever feature to create futures without an actual task. This allows you to directly combine argument unpacking with future creation, eliminating the need for an empty placeholder. Here is the code:
future(|a, b| a * b, [6, 7]);
This code accomplishes the identical multiplication as the original, but leverages a more concise and efficient syntax.
While futures offer flexibility, directly returning two futures in a single response is not possible. Remember, a future can only be assigned to a variable. Assigning it to a list, for example, would simply store nil instead of the actual future.
But wait! There is a workaround! While returning two direct futures is not possible, you can achieve similar behavior by chaining futures using the then() method. The first future's result can be passed to the second future, effectively creating a chained execution. One more workaround is to parse them as arguments to an empty future:
future(nil, future(|| 2+2), future(|| 3*3));  // response: [nil, 4, 9]
Answer "b" is correct. When set to true, it instructs ThingsDB to automatically
            deserialize the response from the method call, making it readily accessible for further processing or
            manipulation. However, if you set the load property to false
            (the default), the method returns the response as "mpdata". This option is particularly useful when you do not need
            to use the data within your ThingsDB application or when you plan to send it directly to a client.
            In these cases, deserializing and then serializing it again would be unnecessary and potentially slow down your
            application.
Answer "d" is the correct answer. The argmap property set to ["*"]
            signifies that the bar() method can accept a single
            argument (nil or a thing). In this case, you are providing nil as the argument.
            Additional arguments (like the 42 in the code snippet) won't be processed by the
            module itself. Instead, they'll be passed directly to the then() or else()
            callbacks making B or D equal to 42
            depending on success or failure. This allows you to parse data to your callback logic.
Option "c" offers the most effective path to diagnosis. Use module_info() to
            get its config and status, plus check the node
            console logs for clues. These steps unlock insights into the issue, paving the way for a fix.
            Remember, understanding the root cause is key!
When seeking precise control over module updates, leverage the deploy_module()
            function. This tool empowers you to define the new source for the update, replacing any pinned version
            tags with the desired reference (e.g., @v0.3.0 instead
            of @v0.2.1). Remember to prioritize testing updates in a non-production
            environment before deploying them to ensure compatibility and smooth operation.
            While refresh_module() can also update modules, it respects pinned versions.
Now that you have explored the power of ThingsDB, it is crucial to consider data protection. While ThingsDB is designed for minimal downtime by scaling across multiple nodes, accidents or unforeseen events can still occur, potentially leading to data loss. Here's where backups come in!
The good news is, ThingsDB offers scheduled backups without impacting ongoing operations. Unlike some solutions that lock access during backup creation, ThingsDB seamlessly handles this on multi-node setups.
Later in this chapter we will also look at single collection exports and how to import them when required. This is also useful for development as it allows you to quickly load another collection on your local development machine without disrupting other collections you might be working on.
So far, we have used the /thingsdb scope for managing user accounts and
            collections, and collection scopes for interacting with data. Now, for backups, we need a new scope:
            node scopes.
Unlike the previous scopes, backups are node-specific. While each node eventually stores the same data, you tell ThingsDB which node should create a backup.
            Before scheduling a backup, we need to identify the node responsible for creating it.
Access the node scope by typing /node (@n or /n for short) in the prompt:
(@t)>@ /node(/node)>
When you do not specify a node ID in the scope, ThingsDB automatically uses the node you are currently
            connected to. To check which node this is, use the node_info() function
            and extract the node_id property using the following command:
(/node)>node_info().load().node_id;0
The output 0 signifies you are currently connected to the node with ID 0.
The nodes_info() function displays all nodes within your ThingsDB setup,
            providing valuable information like their IDs, names, and statuses:
(/node)>nodes_info();[ { "committed_change_id": 1, "node_id": 0, "node_name": "node0", "port": 9220, "status": "READY", "stored_change_id": 1, "stream": null, "syntax_version": "v1", "zone": 0 } ]
In this set-up, we only have one node with ID 0, explaining the current connection.
            However, if you have a multi-node setup, specifying the node ID is crucial for targeted operations. To force a query on a specific node, simply include its ID within the scope like this:
(/node)>@ /node/0(/node/0)>
Now that you have selected the right scope, let's create your first ThingsDB backup!
To create a backup, use the new_backup() function. It requires at
            least one argument: the target filename. Backup filenames must end
            with ".tar.gz". This ensures consistency and helps you understand
            the format of the saved data.
ThingsDB offers handy template variables you can embed in your filename. These variables get automatically filled when the backup is created, making it easier to organize and identify your backups.
Table 14.2 - Summarizing backup template variable
| Variable | Description | Example | 
|---|---|---|
{DATE} | Current date (YYYYMMDD format) | 20240221 | 
{TIME} | Current time (HHMMSS format) | 092713 | 
{CHANGE_ID} | Last committed change ID | 123456 | 
Here's a code snippet that creates a backup named "/tmp/ti-backup-{DATE}-{TIME}.tar.gz".
Note: If you are using Docker Compose as described in this book,
            remember to adjust /tmp to /dump
            (which is volume-mounted to your local disk).
(/node/0)>new_backup("/tmp/ti-backup-{DATE}-{TIME}.tar.gz");0
The return value (0 in this case) is a unique backup ID assigned within that specific node.
Now that you have the backup ID, let's check its status using the backup_info() function:
(/node/0)>backup_info(0);// Use the backup ID (0 in our example){ "created_at": 1708507632, "file_template": "/tmp/ti-backup-{DATE}-{TIME}.tar.gz", "files": [ "/tmp/ti-backup-20240221-092713.tar.gz" ], "id": 0, "result_code": 0, "result_message": "success - 2024-02-21 09:27:13Z" }
In this example, the result_code is 0, indicating a
            successful backup. Additionally, the result_message confirms this
            with "success - 2024-02-21 09:27:13Z". In case of issues, the result message
            offers further information or troubleshooting guidance.
            Having created a backup, let's discuss restoring it. For full backups like the one we made earlier, a comprehensive restore is the only option. To ensure a smooth restoration process, ThingsDB performs some essential pre-checks:
            User Permissions:
FULL privileges on the /thingsdb scope.Data Presence:
collections_info() and, if necessary, remove them with del_collection(). (Note: empty collections may exist and are ignored)modules_info() to check and del_module() to remove any modules if needed./thingsdb scope using the tasks() function and remove them if necessary. Node Status:
nodes_info() to monitor node status.nodes_info() displays both committed_change_id and stored_change_id for each node. This check is not required for single-node setups.Initiate the restore process using the restore() function within
            the /thingsdb scope. This function requires the filename of the backup you
            want to restore. Additionally, you can provide an optional thing argument containing extra restore options
            for further customization.
Here's an example using restore options:
(/t)>restore("/tmp/ti-backup-20240221-092713.tar.gz", { take_access: true, restore_tasks: true, });0
In the provided example, the two key restore options are:
take_access: true
This grants full access to all restored scopes only to the user performing the restore. If you keep this option as the default false, access will be restored based on the original user permissions associated with each scope.
restore_tasks: true
This includes scheduled tasks in the restoration process. However, keep in mind that tasks might automatically trigger based on their schedules once restored. This could be undesirable if you need time to adjust the environment after the restore. The default false setting prevents task execution.
After completing the restore, even with take_access: true, re-authenticate by signing out and signing in again due to potential user ID changes.
            When restoring data from a backup created in a multi-node ThingsDB setup, you might encounter unintended behavior on single-node environments like your development machine. During the restore process, ThingsDB attempts to contact other nodes based on the information stored in the backup, which can be disruptive in this context.
To prevent this unnecessary search for non-existent nodes, simply restart your ThingsDB instance
                after the restore is complete using the --forget-nodes command-line
                argument. This instructs ThingsDB to disregard any stored node information and operate as a
                single-node setup, aligning with your development environment.
ThingsDB empowers you to schedule automatic backups, ensuring regular data protection without manual intervention. Let's explore how to set up scheduled backups:
The new_backup() function offers three additional arguments for scheduling:
start_ts
Define the start date and time for the initial backup. If omitted, the backup starts immediately.
repeat
Set the repetition interval in seconds. For daily backups, use 24 * 3600. Leaving this blank creates a one-time backup.
max_files
Determine the maximum number of backup files to retain. When this limit is reached, the oldest backup is automatically removed. The default is 7.
Let's schedule a daily backup at 22 PM Kyiv time, keeping up to 14 backup files:
(/t)>@ /node/0(/node/0)>new_backup( "/tmp/daily-{DATE}-{TIME}.tar.gz", datetime().to("Europe/Kyiv").replace({hour: 22, minute: 0, second: 0}), 24*3600,// Repeat every 24 hours14,// Keep 14 backup files);1
This starts a backup immediately as we start with a time in the past, the next one 24 hours later relative to the given start time.
Use backup_info() and the backup ID (1)
            to confirm the schedule:
(/node/0)>backup_info(1);{ "created_at": 1708624740, "file_template": "/tmp/daily-{DATE}-{TIME}.tar.gz", "files": [], "id": 1, "max_files": 14, "next_run": "2024-02-22 20:00:00Z", "repeat": 86400 }
This shows details of your backup schedule, including the next run at 22:00 PM Kyiv time
            (2024-02-22 20:00:00Z), repeat interval, and maximum file count.
ThingsDB offers a convenient function called backups_ok() to quickly
                check the overall health of your backup system. It returns a simple true
                or false value, making it easy to monitor if all backups are
                running as planned.
true: All scheduled backups are successful or have not yet started.false: At least one backup has failed.(/node/0)>backups_ok();true
This response indicates that all your scheduled backups are currently successful or have not started yet.
Looking to run ThingsDB on Google Cloud Platform (GCP)? We've got you covered! ThingsDB offers specialized Docker images for GCP, enabling you to write backups directly to a storage bucket.
While we do not delve into the complete workflow here, we want you to be aware of this powerful option. More cloud platforms might be supported in future releases, so stay tuned to the documentation for updates.
For GCP users, the ThingsDB GitHub repository provides a detailed guide for setting up backups on the GKE platform:
What you will find:
new_backup() function to write backups to your storage bucket.While backups are crucial for disaster recovery, sometimes you need to work with specific collections or
            data subsets. ThingsDB's export() function provides flexibility for
            these scenarios.
Collection Schemas
Run export() without arguments to retrieve the collection schema
            definition in a readable format. This includes enumerators, types, and procedures, but not the actual data.
Exporting Everything
Call export() with a "thing" argument containing a dump
            property set to true. This exports the entire collection, including data and
            tasks (if any), in MessagePack format. You can then use the import() function
            to restore this data.
ThingsDB Prompt simplifies the export/import process by letting you directly invoke export() and import() functions
            from the command line, eliminating the need for manual file handling.
Use export as an argument to write an export to a file. The default
            for things-prompt is to export everything:
$things-prompt -u admin -p pass -s //todos export /tmp/dump.mp
Use import as an argument to import the exported data into a new collection:
$things-prompt -u admin -p pass -s //dev import /tmp/dump.mp
This creates a new collection "dev" with data identical to the original "todos" collection.
            You can also export just the collection's definition (enumerators, types, procedures) for future reference or sharing. This exported schema is in a readable ThingsDB code format.
Include the --structure-only flag with the export command:
$things-prompt \ -u admin -p pass -s //todos export /tmp/dump.ti --structure-only
This creates a pain text file /tmp/dump.ti containing the collection's code,
                including enum definitions like:
// Enums
set_enum('Severity', {
        Medium: 1,
        Low: 0,
        High: 2,
        str: |this| `{this.name()} ({this.value()})`,
});
// … more lines
            As before, use import to import the exported schema into a (new) collection.
$things-prompt -u admin -p pass -s //schema import /tmp/dump.ti
This creates a new collection "schema" with the same enumerators, types and procedures as the original, but without any data.
                In conclusion, exports and backups are both valuable tools, but serve distinct purposes:
Good luck with the quiz, and see you in the next chapter on adding nodes to your ThingsDB set-up!
Which approach is more suitable for safeguarding your ThingsDB data?: backups or exports?
For disaster recovery, what is the recommended scope for creating a backup using the new_backup() function?
//stuff or //todos/n/thingsdb/n/0 or /n/1Disaster strikes! You accidentally forget your password and lose access to your ThingsDB instance. Thankfully, you have a recent backup. What is the best way to regain access to your data?
restore() and manually grant yourself access to each individual scope.restore() with the take_access: true property, regaining access to all restored scopes automatically.How can you verify that your ThingsDB backups are functioning as intended?
Consider the behavior of tasks during imports and restores: By default, they are not included. Why do you think this happens, and what are the potential implications?
Answer "a" is correct. Backups are your data's safety net. They create complete copies at specific points in time, ensuring you can recover from disasters like hardware failures or accidental deletion. These backups run silently in the background on designated nodes and can be scheduled for automatic execution, safeguarding your data without disrupting your ThingsDB operations.
Exports on the other hand allow you to extract specific collections or structures for development purposes. This makes them ideal for creating testing environments, sharing data subsets, or migrating data between instances.
The correct answer is "d". While both /n and /n/0 are technically valid scopes for creating a backup, using an explicit node scope like /n/0 is recommended as it explicitly specifies the node on which the backup will be scheduled, eliminating ambiguity and potential confusion.
The correct answer is "c". Restore the backup using restore() with the take_access: true property. This efficiently restores your data and automatically grants you access to all restored scopes, ensuring a smooth recovery process.
(Once the restore is completed, always re-authenticate due to possible user ID changes!!)
The backups_ok() function provides a convenient boolean answer on backup success,
            but it does not give you the "why" behind failures. For that, you need the backups_info() function,
            which goes beyond a simple true / false by providing a result message, often pinpointing the exact issue or offering clues for troubleshooting.
Including tasks in imports or restores can lead to unexpected behavior and potential data manipulation if not carefully considered. If, for example, a task is scheduled for a specific date in the past, restoring it might trigger immediate execution, potentially causing issues with outdated data or unintended consequences. Therefore, answer "a" is correct.
Ready to unleash the full potential of your ThingsDB setup? This chapter dives into two essential topics:
First, we'll guide you through the process of integrating additional nodes. Next, we'll equip you with valuable troubleshooting tools, from identifying bottlenecks to analyzing data flow, ensuring your ThingsDB runs smoothly and efficiently.
Not interested in exploring multiple nodes for development? Feel free to skip the next sections and continue at 15.3 - Node Counters.
In this section, we shall guide you through two options for adding nodes:
            Ready to test multiple ThingsDB nodes on a single machine? Let's explore how to achieve this when you have built ThingsDB from source.
Each node requires unique TCP ports and data storage paths. You can configure them through environment variables or a configuration file.
Table 15.1.1 summarizes the essential variables for setting up multiple nodes. For a full list of configuration options, refer to the documentation: https://docs.thingsdb.io/v1/getting-started/configuration/
Table 15.1.1 - Relevant environment variables for multiple nodes
| Variable | Description | 
|---|---|
THINGSDB_LISTEN_CLIENT_PORT | Listen to this TCP port for client socket connections. | 
THINGSDB_LISTEN_NODE_PORT | Listen to this TCP port for node connections. | 
THINGSDB_HTTP_API_PORT | (Optional) Listen to this TCP port for HTTP API requests. | 
THINGSDB_HTTP_STATUS_PORT | (Optional) Listen to this TCP port for HTTP status requests. | 
THINGSDB_WS_PORT | (Optional) Listen to this TCP port for WebSocket requests. | 
THINGSDB_MODULES_PATH | Path where ThingsDB modules are stored. | 
THINGSDB_STORAGE_PATH | Location to store ThingsDB data. | 
Create a separate directory: (we use "node1" and consider the first one as "node0")
$mkdir ~/node1
Start the node with unique port numbers and data paths:
$THINGSDB_LISTEN_CLIENT_PORT=9201 \ THINGSDB_LISTEN_NODE_PORT=9221 \ THINGSDB_HTTP_API_PORT=9211 \ THINGSDB_HTTP_STATUS_PORT=8081 \ THINGSDB_WS_PORT=9271 \ THINGSDB_MODULES_PATH=~/node1/modules \ THINGSDB_STORAGE_PATH=~/node1/data \ thingsdb --secret pass… Waiting for an invite from a node to join ThingsDB... You can use the following query to add this node: new_node('pass', 'mylaptop', 9221);
With the node waiting for an invite, skip the next section and jump to 15.2 - Invite the New Node.
In your docker-compose.yml file, find the "services" section and uncomment (or add) the following configuration for a second node:
services:node1: << : *ti hostname: node1 container_name: node1 command: "--secret pass" ports: - 8081:8080 volumes: - ./node1/data:/data/ - ./node1/modules:/modules/ - ./node1/dump:/dump/
Navigate to the directory containing your docker-compose.yml file and run:
$docker compose up -d[+] Running 2/2 ✔ Container node1 Started ✔ Container node0 Running
Verify the new node is running by checking the container logs:
$docker logs node1… Waiting for an invite from a node to join ThingsDB... You can use the following query to add this node: new_node('pass', 'node1', 9220);
Now that your new node is waiting for an invitation (as mentioned in the previous section), it is time to officially welcome it to your ThingsDB cluster.
ThingsDB conveniently provides the invitation command directly in the logs. It typically looks like this:
new_node('pass', 'node1', 9220);
        The new_node() function takes three arguments:
--secret pass). It ensures only authorized nodes can join.node1) or IP address if your DNS isn't working perfectly.9220) (do not confuse it with the client port).Switch to the /thingsdb scope in your client and paste
            the new_node(..) command as shown in the logs output.
(@t)>new_node('pass', 'node1', 9220);// Replace the arguments if needed1
The command will return the ID of the new node.
Allow approximately 20 seconds for the nodes to synchronize. This time may vary depending on your system load. During this process, the first node might be temporarily unavailable as it handles the joining process.
            Once the wait is over, switch to a /node scope and run the nodes_info()
            function to see information about all nodes.
(@t)>@n(@n)>nodes_info();…
You should see two nodes with status "READY". If one of the nodes is currently
            in "AWAY" mode, it means ThingsDB has chosen it for a specific task, temporarily
            taking it out of the general cluster operations. This does not indicate an issue, and the node will
            return to "READY" once its task is completed.
To scale your ThingsDB cluster, simply repeat the steps outlined in sections 15.1 and 15.2 for each new node. Remember to use unique data paths and ports to avoid conflicts.
ThingsDB tracks various counters to provide insights into a node's well-being. These counters start at zero
            on node startup and can be manually reset with the reset_counters() function.
Start exploring counters by navigating to a node scope.
(@t)>@ /node/0(/node/0)>
Run the counters() function:
(/node/0)>counters();{ "average_change_duration": 0.0013940499503816795, "average_query_duration": 0.0003048990412233053, "changes_committed": 5262, "changes_failed": 0, "changes_killed": 0, "changes_skipped": 0, "changes_unaligned": 1, "changes_with_gap": 0, "garbage_collected": 1, "largest_result_size": 400999, "longest_change_duration": 0.002760923, "longest_query_duration": 0.010911413, "queries_from_cache": 235, "queries_success": 355260, "queries_with_error": 1499, "quorum_lost": 1, "started_at": 1709132982, "tasks_success": 0, "tasks_with_error": 0, "wasted_cache": 4 }
While it may seem like a lot of information, let's break down the key metrics:
average_change_duration, average_query_duration: Indicate average processing times for changes and queries (in seconds).queries_success: Number of successful queries handled since the last reset.changes_failed, changes_killed, changes_skipped, changes_with_gap: Must remain at zero as they indicate potential data corruption.changes_unaligned, quorum_lost: Monitor these values. While slight increases are possible, significant growth suggests node collisions during change ID claiming.garbage_collected: Number of items cleaned up by garbage collection.queries_from_cache, wasted_cache: Indicate cache utilization and potential optimization opportunities.Remember:
started_at: Reflects the last counter reset time or node startup time if no reset occurred.queries_with_error, tasks_with_error)
                are not inherently problematic. Some intentional actions might trigger them (e.g. lookup_error when
                something is not found etc.).THINGSDB_THRESHOLD_QUERY_CACHE). Queries stay cached
                for 15 minutes (adjustable via THINGSDB_CACHE_EXPIRATION_TIME).
            Analyzing node counters offers valuable insights into your ThingsDB cluster's health and performance. Use this information to identify potential issues, optimize resource usage, and ensure your ThingsDB setup runs smoothly.
ThingsDB offers granular control over various aspects of node behavior through configuration options. Let's explore how to access and manage these settings.
Similar to the query cache in the previous section, some options can be adjusted using environment variables.
            It is crucial to maintain consistency across all nodes. For example,
            if THINGSDB_RESULT_SIZE_LIMIT differs between nodes, the response size for
            queries might vary depending on the responding node.
The node_info() function, introduced in the previous chapter, comes in handy
            for reviewing configuration parameters and ensuring they match across your nodes. It also displays
            the ThingsDB version and its dependent library versions. Aim for consistency in these versions,
            except during controlled upgrades (performed one node at a time).
node_info() also exposes the current log level for a node. You can set the
            initial level using the command-line argument --log-level {debug,info,warning,error,critical} or
            adjust it dynamically. Let's check the current level:
(/node/0)>node_info().load().log_level;"WARNING"
A default setting should display "WARNING".
Need more detailed insights into your node's activities? ThingsDB allows you to dynamically adjust the log level using the set_log_level() function.
(/node/0)>set_log_level(DEBUG);// DEBUG, INFO, WARNING, ERROR, CRITICALnull
            Check the terminal where the node with ID 0 is running (or use "docker logs node0"" if using Docker Compose).
Example output:
…
[D 2024-02-23 21:18:56] not going in away mode (no reason for going into away mode)
[D 2024-02-23 21:18:59] not going in away mode (no reason for going into away mode)
        The D at the beginning of each line indicates a DEBUG log message.
Remember:
DEBUG and INFO) provide extensive details but can overwhelm your logs. Use them for troubleshooting or specific insights.WARNING, ERROR, CRITICAL) offer concise information about potential issues.WARNING for normal operation.New ThingsDB versions bring exciting enhancements! Luckily, they're backwards compatible, meaning you can still access data from the very first release (v0.7.5).
Upgrading one at a time:
For a smooth upgrade, follow a rolling update approach: update one node at a time while ensuring full synchronization before moving on to the next. This prevents downtime during the process.
Monitoring node readiness:
To easily determine when a node is ready for upgrade, enable HTTP status ports for each node. You can check the current status with:
(/node/0)>node_info().load().http_status_port;8080
This shows the port number (e.g., 8080) or "disabled" if not configured.
To enable it, set the THINGSDB_HTTP_STATUS_PORT environment
            variable to your desired port (e.g., 8080) and restart the node.
The enabled port offers three URLs:
/ready
                    Responds with "200 OK" if the node is in READY, AWAY, or AWAY_SOON status, indicating it is ready for the next upgrade. Any other status (e.g., SYNCHRONIZING) triggers a 503 Service Unavailable response.
/status
                    Shows the current node status.
/health
                    Always responds with "200 OK", regardless of the node's state, indicating the node is responding.
This URL plays a critical role in the cluster coordination process during node startup. It acts as a synchronization checkpoint, ensuring a node is fully operational before subsequent nodes initiate their own restarts.
Use the curl command (available on Windows, Mac, and most Linux systems)
                to check the /ready status from the command line:
$curl http://localhost:8080/readyOK
A successful response of OK confirms that the node has completed its
                synchronization process and is now fully operational. This ensures a stable starting point for
                other nodes and allows them to proceed with their own restart or initialization safely.
In the realm of dynamic environments like Kubernetes, where containerized applications are subject to frequent restarts and scaling operations, maintaining application health and availability becomes a top priority. ThingsDB, with its powerful HTTP status port, and Kubernetes' built-in liveness and readiness probes, form a combination to streamline upgrades and guarantee application responsiveness throughout the process.
While this chapter primarily explores node-related functionalities, ThingsDB also offers valuable tools for data debugging.
In ThingsDB, everything is treated as an object. The refs() function
                helps you determine the number of references an object has within the database. This information
                can be crucial for understanding how data is stored and manipulated.
For example, check the references for nil:
(/t)>refs(nil);23
While 23 references for nil might seem unexpected, this reflects
                internal system references and the nature of immutable objects. Unlike mutable data,
                immutable objects like nil, true,
                false, strings, and numbers can be shared across scopes without
                creating copies, potentially contributing to the reference count.
This example demonstrates how refs() can help verify list copying behavior:
(/t)>A = []; B = A; refs(A);3
Here, we see three references:
A.B.refs() function.When interpreting refs() results, remember to account for the additional
                reference introduced by the function itself.
(/t)>A = []; B = [A]; refs(A);2
This output (2 references) confirms that adding a list to another list creates a copy.
                The original list A only has two references:
A.refs() function call.The list within B is a separate copy and does not hold a reference to
                the original A.
The search() method, available on all "thing" instances,
                offers a valuable tool for debugging purposes. It enables you to search for a specific "thing"
                within their properties.
The search() method accepts two arguments:
deep: Controls the maximum depth of the search (defaults to 1).limit: Restricts the number of returned results (defaults to 1).Imagine you have a Todo object with ID 2 in the //todos scope but
                are unsure of its location within the collection. Here's how to use search() to
                locate it:
(/t)>@ //todos(//todos)>.search(Todo(2), {deep: 3, limit: 1});[ { "key": "todos", "key_type": "set", "parent": {"#": 10}, "parent_type": "User" } ]
This output indicates that the first occurrence of the Todo with ID 2 is
                found within the todos property of a User object
                with ID 10.
While search() is valuable for debugging, its potential performance
                impact makes it unsuitable for most production queries. Exercise caution when increasing the
                deep argument, as higher values can significantly slow down the search process.
Optimize your ThingsDB queries and identify potential bottlenecks with
                the timeit() function. This handy tool measures the
                execution time of code blocks, providing valuable insights for performance tuning.
To use it, simply enclose the code you want to evaluate within the timeit()
                function. Once executed, timeit() returns a new object containing
                two key properties:
data: This property stores the result of the executed code itself.time: This property records the execution time in seconds, providing valuable insights into the performance of your code.Let's explore how to use timeit() to compare the performance of two
                approaches for calculating the sum of a large list.
Using reduce():
(/t)>r = range(50000); timeit({ r.reduce(|t, x| t += x, 0); });{ "data": 1249975000, "time": 0.009375523 }
Using the built-in sum():
(/t)>r = range(50000); timeit({ r.sum(); });{ "data": 1249975000, "time": 0.000410818 }
The results clearly demonstrate that the native sum() method is
                significantly faster than the reduce() approach in this case.
                The function timeit() empowers you to make decisions about query
                optimization, enhancing the overall performance of your ThingsDB applications.
When adding a new node to a ThingsDB cluster, under what circumstances and for what reason might you need to wait before proceeding with further actions?
Which command-line argument is only required once when starting a new node to enable it to join an existing ThingsDB cluster?
In a multi-node ThingsDB setup, which of the following practices are considered essential for ensuring optimal performance and stability?
THINGSDB_RESULT_SIZE_LIMIT).counters() method on each node to assess cluster health.Which methods allow you to modify the log level of a ThingsDB node, enabling you to control the verbosity of its output?
--log-level command-line argument.set_log_level() function within the node scope.You're planning to implement liveness and readiness probes for ThingsDB nodes, but
            the node_info().load().http_status_port; command
            returns "disabled", signaling a crucial feature is not yet active.
            What specific steps must be taken to successfully enable the HTTP status port for these nodes,
            allowing for effective health checks?
Predict the reference count and explain the reasoning behind your answer for the following code snippet:
t = {};
refs(t);  // ???
Consider a multi-node setup. You set a property .x in one query and then immediately try to read its value in a subsequent query. However, you do not get the expected result. Why might this occur?
Waiting, for about 20 seconds, depending on the size and performance, is only essential for the second node as it synchronizes from the first. Subsequent nodes can leverage any existing node, allowing the cluster to remain responsive to queries during the process.
Joining a new ThingsDB node to an existing cluster requires the --secret <SECRET> argument only once during initial node startup. This secret tells the node to wait for an invite and serves as a security measure as it prevents unauthorized nodes from joining by requiring the same secret within the new_node() function.
The answer is "e", all of the above. By adhering to these best practices, you can create a robust and reliable multi-node ThingsDB environment that delivers consistent performance and efficient data management.
Answer "c". ThingsDB provides two methods to modify the log level. The --log-level command-line argument sets the initial log level when starting the node and the set_log_level() function in the node scope dynamically adjusts the log level during runtime.
To enable the HTTP status port in ThingsDB, set the THINGSDB_HTTP_STATUS_PORT
            environment variable and restart the node. (e.g. THINGSDB_HTTP_STATUS_PORT=8080).
            While less common, ThingsDB also allows you to configure the HTTP status port within a configuration file.
            Refer to the official documentation for more information: https://docs.thingsdb.io/v1/getting-started/configuration/
The reference count is 2. Both t and the refs() function hold a reference, leading to a total of 2.
Your second query might hit a node that has not yet received the update to property .x, leading to the unexpected result.
There are two main solutions to address this:
.x.wse(): If combining read and write is not an option, you can use the wse() function. This enforces a change, effectively forcing subsequent queries to wait for the change to propagate before responding. This guarantees eventual consistency but can introduce a slight performance overhead.In this final chapter, we delve into the realm of ThingsDB's HTTP API, a valuable tool that empowers you to interact with your data beyond the ThingsDB client.
        Why a HTTP API?
The HTTP API offers several compelling advantages:
By default, the HTTP API is disabled for security reasons. To activate it, you need to configure the API port using the THINGSDB_HTTP_API_PORT environment variable:
docker-compose.yml file.Once configured, you can verify if the API is enabled and determine its listening port using the following command within the node scope:
(/node/0)>node_info().load().http_api_port;9210
Remember, the API needs to be enabled individually on each node. You can either check each node or target the specific node you intend to interact with.
With the API up and running, the next sections will equip you with the knowledge to harness its full potential. We'll explore various API endpoints, authentication mechanisms, and practical use cases, empowering you to unlock new possibilities for managing and interacting with your ThingsDB data.
Authorization is crucial for safeguarding your ThingsDB data. This section delves into basic authentication using a username and password.
Let's leverage curl to execute our first API call, utilizing the default credentials admin and pass:
$curl --location --request POST 'http://localhost:9210//todos' \ --header 'Content-Type: application/json' \ --user admin:pass \ --data-raw '{ "type": "query", "code": "a * b;", "vars": {"a": 6, "b": 7} }'42
Breaking down the steps:
http://localhost:9210//todos
                This specifies the target endpoint for our API call. The //todos part is the scope. You can also use the full scope name /collection/todos in case you do not like the two slashes.
POST
                This indicates that we are sending data to the server.
application/json
                This informs the server that we are sending JSON data.
--user admin:pass
                This provides the username and password for basic authentication.
--data-raw
                This specifies the raw JSON data containing the query information.
The request body containing the query details:
"type": "query": Specifies the request type as a query."code": "a * b;": Defines the query code to be executed."vars": {"a": 6, "b": 7}: Provides variables used within the query code.The previous example highlights the crucial importance of implementing secure authentication practices in production environments. Here are two recommended approaches:
Change default credentials
Replace the default username and password for the administrator account with strong, unique credentials. This prevents unauthorized access using well-known default values.
Token-based authentication
Create a token for the admin account and remove its password entirely:
(/t)>token = new_token("admin"); set_password("admin", nil);// Remove the passwordtoken;// Return the new token"7vXo2vgtnyaWsQ1LKw9CkI"
Carefully safeguard the generated token. Losing the token means losing access to the admin account, as there is no password to fall back on.
Before we continue exploring API calls, let's create a new user account and grant it some
                basic permissions for exploration. While we could utilize the API itself for this process,
                we'll demonstrate using the things-prompt:
(/t)>// Use the /thingsdb scope// Create the user "web"user = new_user("web");// Grant access to the specified scopesgrant("/node", user, QUERY);grant("/thingsdb", user, QUERY);grant("//todos", user, QUERY | RUN);// Generate a new token for user "web"new_token(user);"qOeH6Mny1jLgc6WRGujk5x"
With the generated token, we can now perform queries through the API:
$curl --location --request POST 'http://localhost:9210/node/0' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer qOeH6Mny1jLgc6WRGujk5x' \ --data-raw '{ "type": "query", "code": "node_info().load().version;" }'"1.6.0"
As you can see, the response "1.6.0" confirms the successful
                execution of the query through the API using the generated token and specified user
                permissions. Notice the scope (/node/0) specified in the URL,
                effectively targeting the node with ID 0. We also replaced
                the --user argument which we previously used for username and
                password authentication, with an Authorization header containing
                the Bearer prefix and the generated token.
The ThingsDB API extends beyond queries and allows you to execute procedures as well.
Invoking procedures:
"type" field is set to "run" to indicate procedure execution."name" field within the data body, specifying the name of the procedure you wish to call. This name corresponds to the procedure defined within the scope provided in the URL."args" field. This field can be formatted in two ways:
                Let's revisit the previously created search_todos procedure within the //todos scope. Here's how to execute it with positional arguments using curl:
$curl --location --request POST 'http://localhost:9210//todos' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer qOeH6Mny1jLgc6WRGujk5x' \ --data-raw '{ "type": "run", "name": "search_todos", "args": ["book"] }'[{"id":2,"body":"Read a book","user":{"name":"Alice"},"severity":"Medium","done":true}]
Here's an alternative way to call the same procedure using key-value arguments:
$curl --location --request POST 'http://localhost:9210//todos' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer qOeH6Mny1jLgc6WRGujk5x' \ --data-raw '{ "type": "run", "name": "search_todos", "args": {"needle": "teeth"} }'[{"id":3,"body":"Brush your teeth","user":{"name":"Alice"},"severity":"Medium","done":false}]
By understanding these concepts, you can effectively invoke procedures through the ThingsDB API to execute various functionalities within your applications.
While JSON is the most common data format used with the ThingsDB API, it has limitations when handling binary data. Attempting to send binary data directly within a JSON request will result in an error, as demonstrated in the following example:
$curl --location --request POST 'http://localhost:9210/thingsdb' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer qOeH6Mny1jLgc6WRGujk5x' \ --data-raw '{ "type": "query", "code": "bytes(\"Not supported by JSON\");" }'type `bytes` is not JSON serializable (-61)
One approach to solve this issue involves base64 encoding the binary
            data before including it in a JSON request. However, this adds an extra step of encoding and decoding, potentially impacting performance and code readability. The following example demonstrates this approach using the base64_encode() function:
$curl --location --request POST 'http://localhost:9210/thingsdb' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer qOeH6Mny1jLgc6WRGujk5x' \ --data-raw '{ "type": "query", "code": "base64_encode(bytes(\"Conversion works!\"));" }'"Q29udmVyc2lvbiB3b3JrcyE="
ThingsDB offers MessagePack as a more efficient alternative. It is specifically designed for binary data exchange and provides a more compact and efficient way to transmit information compared to JSON.
While directly utilizing MessagePack with curl might involve additional tools, here's a
            simple Python example showcasing its usage:
import requests import msgpack# Prepare data in MessagePack formatdata = msgpack.packb({ "type": "query", "code": "bytes('Works without conversion!');" })# Send request with appropriate headersresponse = requests.post('http://localhost:9210//todos', data, headers={ "Content-Type": "application/msgpack", "Authorization": "Bearer qOeH6Mny1jLgc6WRGujk5x" })# Unpack response and print the resultresult = msgpack.unpackb(response.content) print(result)# Prints: b'Works without conversion!'
For most situations, JSON remains the preferred choice due to its widespread adoption and ease of use. However, for performance-critical scenarios with substantial binary data exchange, consider MessagePack. Being ThingsDB's internal format, data is already optimized for MessagePack, minimizing the need for conversions compared to using JSON.
Which of the following security practices are NOT recommended for securing a ThingsDB production environment?
ThingsDB is working on my computer but I cannot connect to the HTTP API. The error message states "Failed to connect to localhost port 9210". Which of the following is most likely the reason?
Which of the following URLs are valid to perform a query in the "foo" collection?
http://thingsdb/foohttp://thingsdb/collection/foohttp://thingsdb//foohttp://thingsdb/foo/collectionThe procedure get_score exists in the //game scope and accepts a user_id as argument. Fix the
            following API request to retrieve the score for a user with ID 123:
GET http://thingsdb//game
{
  "type": "procedure",
  "name": "get_score",
  "args": [{"user_id": 123}]
}
Which of the following Media Types are supported by the ThingsDB HTTP API for data exchange?
Option "d", "leave the default admin account credentials unchanged" is the least secure practice among the choices. Using the default credentials increases the risk of unauthorized access, as they are widely known and easily guessable.
Answer "c". The most likely reason is that the API is not enabled on port 9210.
Use the node_info().load().http_api_port; query in the node scope to confirm the currently configured port for the HTTP API.
If the API is disabled or the desired port is not 9210, start ThingsDB with the environment
            variable THINGSDB_HTTP_API_PORT=9210. This will enable the API on port 9210.
Both "b" (../collection/foo) and "c" (..//foo) are valid options, as ThingsDB allows shortening the 'collection' term in the URL.
Fix the request:
POST."run" (indicating a procedure call).[123]).{"user_id": 123}).Here's the corrected request:
POST http://thingsdb//game
{
  "type": "run",
  "name": "get_score",
  "args": {"user_id": 123}    // or [123]
}
Both "a" and "d". The supported Media Types for the HTTP API are:
Both JSON and MessagePack are efficient formats for transmitting data over the API. While JSON is the most commonly used option due to its widespread adoption, MessagePack offers advantages in terms of compactness and efficiency, especially when dealing with binary data.
This exploration of "Data as Code" has brought you on a journey through the heart of ThingsDB. You've learned how this innovative platform allows you to seamlessly combine data and code, empowering you to craft powerful, flexible, secure and scalable applications.
As you venture beyond this book, remember that ThingsDB is an actively evolving platform. Stay connected with ThingsDB's community and evolving features! Explore the official documentation, engage in discussions on the forum (https://github.com/orgs/thingsdb/discussions), and discover the latest best practices to keep your ThingsDB knowledge sharp.
We are confident that you, armed with the knowledge from this book and the power of ThingsDB, will embark on a journey of data-driven innovation, shaping the future of information management and application development.
This appendix helps you jump into Chapters 7-13 of this book, which build on a to-do application using the "todos" collection. Whether you want to start a specific chapter or just explore the application with pre-populated data, follow these steps:
Switch to the /thingsdb Scope (using @t)
        and install the "requests" module (if not already done):
(@t)>new_module("requests", "github.com/thingsdb/module-go-requests");null
Create a new collection (replace "todos" if you prefer another name):
(@t)>new_collection("todos");"todos"
Switch to the newly created collection scope:
(@t)>@ //todos(//todos)>
Choose your starting chapter (7, 8,
        9, 10, 11,
        12, or 13):
(@t)>// Replace chapter9 with desired chapter number (7-13):requests.get("https://docs.thingsdb.io/v1/book/chapter9.mp").then(|res| { data = res.load().body; import(data); });null
Ready to Go!
With these steps, you've imported the chapter data and can start exploring the "todos" application from your chosen point.
ThingsDB Website
ThingsDB Source Code
ThingsDB Documentation
ThingsDB Discussion Forum
ThingsGUI Application
ThingsPrompt Toolkit
Python Connector
Go Connector
C# Connector
PHP Connector
JavaScript/Node.js Connector
List of all connectors
Demo module
Requests module
ThingsDB module
List of other modules maintained by the ThingsDB team
Google Cloud Platform Deployment (Kubernetes, GKE)
Docker Compose file
Python Template file
Python Dashboard file
Jeroen van der Heijden has been passionate about programming since his early years, and has been actively contributing to the open-source world since 2012. He is the designer behind ThingsDB. His expertise extends beyond ThingsDB, as he is also the maintainer of other projects like SiriDB (a high-performance time-series database) and the "leri" language parsing libraries.
Currently, Jeroen's expertise drives innovation at Cesbit, where he leads the development of InfraSonar (www.infrasonar.com), a infrastructure monitoring solution that relies heavily on both ThingsDB and SiriDB.
Beyond ThingsDB, he enjoys coding in C/C++, Python and other languages. When he's not coding, he spends time with his family or seeks thrills on the open road or trails with his race or mountain bike.