[Back to LOC CLI Guidebook](https://hackmd.io/4LIsNIuWQnuR70JMeuBAEA?view) [Setup LOC CLI Environment](https://hackmd.io/igLh4azUT2aI8Fv-q-0e-g) [Getting Started - JavaScript](https://hackmd.io/IiHvmAtjTTGFfakaH0dWuw?view) [Getting Started - TypeScript](https://hackmd.io/kz93Th7vTCCbO3GFxp3r-A) [LOC CLI Commands](https://hackmd.io/R4mrz2t1QSyTCHH73_2itg) [Local Simple Runtime Execution](https://hackmd.io/JhAMB49rS4CrpNdhHed7YA?view) # Local Database Testing with Simple Runtime In Local Simple Runtime Execution, since a LOC data process is running locally, it can access other local services on your machine for testing purposes. However, we can add a database service in the Docker Compose definition as well (which makes it accessible from ```127.0.0.1```). This is also a good way to do test with [database agents](https://hackmd.io/Uj-tC7l9Q82VyGr8R5PL2Q?view#Database). In this tutorial, we will demonstrate the following operations: 1. Add a DB server to LOC local environment 2. Add a script to automatically initialize the database on start 3. Access the database in a data process There are currently 3 supported databases: - [MySQL](#Add-a-Local-MySQL-Service) - [PostgreSQL](PostgresSQL) - [MS SQL Server](MS-SQL-Server) :::info ### Do I Have to Use Docker Compose for Databases? Not really, you can still install a database service in the conventional way (```localhost:<port>``` can be accessed via ```host.docker.internal:<port>```). For people who don't want to have a bunch of DB services running on all times, using containers is a lot faster and convenient. ::: ## Add a Local MySQL Service ### Modify ```saffron-evenstore.yaml``` Add the following lines at the end of your ```saffron-evenstore.yaml``` (see [Local Simple Runtime Execution](https://hackmd.io/JhAMB49rS4CrpNdhHed7YA?view) for more information): ```yaml services: ... mysql: image: mysql restart: "always" container_name: mysql environment: - MYSQL_ROOT_USER=root - MYSQL_ROOT_PASSWORD=1234 volumes: - ./sql/mysql-setup.sql:/docker-entrypoint-initdb.d/setup.sql ports: - "3306:3306" networks: - saffron ``` This will add a MySQL server container: - Container name: ```mysql``` - Port: ```3306``` - Default user: ```root``` - Password: ```1234``` ### Add ```/sql/mysql-setup.sql``` Since the database would be empty after startup, you may want to add something first. Now create a dir ```sql``` and add a file```mysql-setup.sql``` in it. This will be run when the database container starts up at the first time. The following script will do the following things: - create a db ```mydb``` - create a table ```policy``` under ```mydb``` - insert three instances of data ```sql CREATE DATABASE mydb; USE mydb; CREATE TABLE policy (AcuNo VARCHAR(20) NOT NULL, Amount INT); INSERT INTO policy (AcuNo, Amount) VALUES ('Acu-048', 4239); INSERT INTO policy (AcuNo, Amount) VALUES ('Acu-049', 2022); INSERT INTO policy (AcuNo, Amount) VALUES ('Acu-050', 9527); ``` The table is a simple data structure that records accounting data of insurance policies: | Column | Type | Description | | ------ | ------ | ---------------------------------- | | AcuNo | string | Account number of insurance policy | | Amount | number | Amound (premium) of the policy | You can replace the content with your own SQL script. ## Startup Database Open a new terminal under the LOC CLI workspace dir and run the following command: ```bash docker-compose -f saffron-evenstore.yaml up -d ``` This is *exactly* the same as what we did in [Local Simple Runtime Execution](https://hackmd.io/JhAMB49rS4CrpNdhHed7YA?view) - except that now you have a new container ```mysql``` running along with others and share the same virtual network environment. ![](https://hackmd.io/_uploads/HyTlduNY5.png) ### (Optional) Access MySQL Client You can access the client interface of the MySQL container. Open a new terminal and enter ```bash docker exec -it mysql mysql -u root -p ``` MySQL will prompt you to enter the root password (```1234``` in our case). Then you will see ``` Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 11 Server version: 8.0.29 MySQL Community Server - GPL Copyright (c) 2000, 2022, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> ``` You can test SQL commands here. For example, you can check if the table and data in ```mysql-setup.sql``` have been added successfully: ``` mysql> USE mydb; mysql> SELECT * FROM policy; +---------+--------+ | AcuNo | Amount | +---------+--------+ | Acu-048 | 4239 | | Acu-049 | 2022 | | Acu-050 | 9527 | +---------+--------+ 3 rows in set (0.00 sec) ``` ## Add MySQL Connection Variables in Profile For safety reasons, we'll put the connection information in the profile ([local.yaml](https://hackmd.io/JhAMB49rS4CrpNdhHed7YA?view#Create-Local-Profile) in our example): ```yaml # docker mode (host network) simpleRuntime: image: public.ecr.aws/m0s8j1u6/saffron/simple-runtime:release-0.5.1 dockerNetwork: host eventstoreEndpoint: http://127.0.0.1:8087 etcdEndpoints: http://127.0.0.1:2379 # mysql connection info mysql_host: 127.0.0.1 mysql_port: 3306 mysql_username: root mysql_password: "1234" mysql_database: mydb ``` Remember that the port has to be a *number* and the password has to be a *string*. We also set the host as ```127.0.0.1``` so our data process can access the MySQL server without problem. :::info If you have a MySQL service installed locally outside the LOC local environment, change ```127.0.0.1``` to ```host.docker.internal``` instead. ::: Now update your profile: ```bash loc profile set -f local.yaml -p local ``` If you haven't set the local profile as default, run ```bash loc profile default local ``` ## Query the Database in Data Process Here we have one single generic logic, which will connect the database ```mydb```, query any rows from the ```policy``` table that has the account number ```Acu-048```: ```javascript export async function run(ctx) { // import database-related classes const database = Saffron.Database; // connection info (which are defined in your profile) const connectionInfo = { host: process.env.mysql_host, // host.docker.internal port: process.env.mysql_port, // 3306 database: process.env.mysql_database, // root username: process.env.mysql_username, // "1234" password: process.env.mysql_password, // mydb }; let db = null; try { // connect to a MySQL database db = await ctx.agents.database.connect({ databaseDriver: "MySQL", connection: new database.MySqlParameters(connectionInfo), }); // SQL query const resp = await db.query( "SELECT * FROM policy WHERE AcuNo = ?", // prepared statement ['Acu-048'] // parameters (replace the ? in prepared statement) ); // read the returned rows const rows = resp?.rows; // iterate through the rows and log them rows.forEach(row => { ctx.agents.logging.info(`Account number: ${row.AcuNo} => $${row.Amount}`); }); } catch (error) { // log error if something went wrong ctx.agents.logging.error(error.message); } finally { // close the connection at the end whether or not there were errors await db?.disconnect(); } } ``` If you execute this in a data process in local runtime, you should see a log output like this: ``` INFO saffron_process::agent::logger: [execution_id=...,id=...]@d3482d10-11c8-487d-9135-11c03afc2b9a-0 Account number: Acu-048 => $4239 ``` This proves that we've successfully queried the MySQL database. :::info ### Plain Query vs. Prepared Statement Of course, you can simply write the query as ```javascript const resp = await db.query( "SELECT * FROM policy WHERE AcuNo = " + "'Acu-048'", [] ); ``` or if the parameter is loaded from somewhere ```javascript const accno = body?.accno; // read from request body ... const resp = await db.query( `SELECT * FROM policy WHERE AcuNo = '${accno}'`, [] ); ``` However, this would be dangerous if the parameter itself *contains* SQL commands (++[**SQL injection**](https://en.wikipedia.org/wiki/SQL_injection))++. ++[Prepared statements](https://en.wikipedia.org/wiki/Prepared_statement)++ separates parameters with the raw SQL so any injected SQL won't be executed. This is a safer and widely-accepted good practice. The parameter syntax may differ depending on the database. ::: ## Insert, Update and Delete Row(s) ```javascript // connect to db ... // add a new policy with account number Acu-051 // but the payment is too low! await db.execute( "INSERT INTO policy (AcuNo, Amount) VALUES (?, ?);", ['Acu-051', 99] ); ... // update the payment to correct amount await db.execute( "UPDATE policy SET Amount = ? WHERE AcuNo = ?;", [5000, 'Acu-051'] ); ... // the policy has been written off, delete it await db.execute( "DELETE FROM policy WHERE AcuNo = ?;", ['Acu-051'] ); ... // disconnect db ``` You can check the table at anytime in MySQL client: ``` mysql> SELECT * FROM policy; ``` ### Remove MySQL from Local Environment You can stop the container in Docker Desktop anytime. If you want to remove MySQL from LOC local environment: 1. Delete or comment out (in VS Code select and press ```Ctrl + \```) the MySQL section in ```saffron-evenstore.yaml``` 2. Delete local environment containers in Docker Desktop 3. Run ```docker-compose -f saffron-evenstore.yaml up``` again ## Other Supported Databases ### PostgresSQL 1. Modify ```saffron-evenstore.yaml``` ```yaml service: ... postgres: image: postgres restart: "always" container_name: postgres environment: - POSTGRES_PASSWORD=1234 volumes: - ./sql/postgres-setup.sql:/docker-entrypoint-initdb.d/setup.sql ports: - "5432:5432" networks: - saffron ``` - Container name: ```postgres``` - Port: ```5432``` - Default user: ```postgres``` - Password: ```1234``` 2. Add ```/sql/postgres-setup.sql``` ```sql CREATE DATABASE mydb; \c mydb CREATE TABLE policy (acuno VARCHAR(20) NOT NULL, amount INT); INSERT INTO policy (acuno, amount) VALUES ('Acu-048', 4239); INSERT INTO policy (acuno, amount) VALUES ('Acu-049', 2022); INSERT INTO policy (acuno, amount) VALUES ('Acu-050', 9527); ``` :::info Column names in PostgresSQL **are not case sensitive**. Which means even if you set it as ```AcuNo``` in the SQL script, it will still be lower cased ```acuno``` in PostgresSQL *and* in LOC db object. ::: 3. Startup Database ```bash docker-compose -f saffron-evenstore.yaml up -d ``` 4. (Optional) Connect to PostgresSQL client: ```bash docker exec -it postgres psql -U postgres ``` 5. Setup ```local.yaml``` profile ```yaml ... # postgres pgsql_host: 127.0.0.1 pgsql_port: 5432 pgsql_username: postgres pgsql_password: "1234" pgsql_database: mydb ``` Then update it: ```bash loc profile set -f local.yaml -p local ``` 6. Query data ```javascript const connectionInfo = { host: process.env.pgsql_host, port: process.env.pgsql_port, database: process.env.pgsql_database, username: process.env.pgsql_username, password: process.env.pgsql_password, }; db = await ctx.agents.database.connect({ databaseDriver: "Postgres", connection: new database.PostgresParameters(connectionInfo), }); ... const resp = await db.query( // !!! beware that PostgresDB columns are all lower case! "SELECT * FROM policy WHERE acuno = $1", // prepared statement ['Acu-048'] // parameters (replace the $1 in prepared statement) ); const rows = resp?.rows; rows.forEach(row => { ctx.agents.logging.info(`Account number: ${row.acuno} => $${row.amount}`); }); ``` ### MS SQL Server MS SQL Server is slightly complicated to setup, but the principle is still the same. 1. Modify ```saffron-evenstore.yaml``` ```yaml service: ... mssql: build: context: . dockerfile: ./sql/mssql-Dockerfile restart: "always" container_name: mssql ports: - "1433:1433" networks: - saffron ``` 2. Add ```/sql/mssql-Dockerfile``` Since the MSSQL container does not have a Docker entrypoint, we'll have to build a custom image with the entrypoint script in it: ```dockerfile= # using SQL Server 2019 FROM mcr.microsoft.com/mssql/server:2019-latest USER root RUN mkdir -p /usr/config WORKDIR /usr/config ENV ACCEPT_EULA Y ENV MSSQL_PID Express # running as Express version ENV SA_PASSWORD MsSql1234 # root user password COPY ./sql/mssql-entrypoint.sh /usr/config/entrypoint.sh COPY ./sql/mssql-init.sh /usr/config/init.sh COPY ./sql/mssql-setup.sql /usr/config/setup.sql EXPOSE 1433 RUN chmod +x /usr/config/entrypoint.sh RUN chmod +x /usr/config/init.sh CMD ./entrypoint.sh ``` The ```saffron-evenstore.yaml``` will use this file to build a MSSQL image and run a container based on it: - Container name: ```postgres``` - Port: ```5432``` - Default user: ```postgres``` - Password: ```1234``` 3. Add ```sql/mssql-setup.sql``` ```sql CREATE DATABASE mydb; GO USE mydb; GO CREATE TABLE policy (AcuNo VARCHAR(20) NOT NULL, Amount INT); GO INSERT INTO policy (AcuNo, Amount) VALUES ('Acu-048', 4239); INSERT INTO policy (AcuNo, Amount) VALUES ('Acu-049', 2022); INSERT INTO policy (AcuNo, Amount) VALUES ('Acu-050', 9527); GO ``` :::info In MS SQL Server, ```GO``` is required to confirm one or multiple SQL commands. See: ++[SQL Server Utilities Statements - GO](https://docs.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go?view=sql-server-ver16)++ ::: 4. Add ```/sql/mssql-entrypoint.sh``` ```shell= /usr/config/init.sh & /opt/mssql/bin/sqlservr ``` 5. Add ```/sql/mssql-init.sh``` ```shell= sleep 20s /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P MsSql1234 -d master -i setup.sql ``` This script will wait 20 seconds to make sure the DB service is up and running. Then it will run ```sql/mssql-setup.sql``` to initialize the data. Change the time if you encounter problems. :::info If you modified anything in the .sh scripts (including the password), you have to delete **both** image and container before re-deploying them; otherwise Docker won't update them. ::: 6. Startup Database ```bash docker-compose -f saffron-evenstore.yaml up -d ``` 4. (Optional) Connect to MSSQL client: ```bash docker exec -it mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P MsSql1234 ``` 5. Setup ```local.yaml``` profile ```yaml ... # mssql mssql_host: 127.0.0.1 mssql_port: 1433 mssql_username: sa mssql_password: "MsSql1234" mssql_database: mydb ``` Then update it: ```bash loc profile set -f local.yaml -p local ``` 6. Query data ```javascript const connectionInfo = { host: process.env.mssql_host, port: process.env.mssql_port, database: process.env.mssql_database, username: process.env.mssql_username, password: process.env.mssql_password, trustCert: true, // optional }; db = await ctx.agents.database.connect({ databaseDriver: "MSSQL", connection: new database.MssqlParameters(connectionInfo), }); ... const resp = await db.query( "SELECT * FROM policy WHERE acuno = @P1", // prepared statement ['Acu-048'] // parameters (replace the @P1 in prepared statement) ); const rows = resp?.rows; rows.forEach(row => { ctx.agents.logging.info(`Account number: ${row.AcuNo} => $${row.Amount}`); }); ``` ###### tags: `LOC CLI`