MySQL REST Service - SDK Reference

#
MySQL REST Service - SDK Reference

This document explains how to work with the MRS Software Development Kit and discusses the Client API.

Please also see

- MySQL REST Service - Developer’s Guide - This book explains how to install and configure the MySQL REST Service (MRS) as well as how to access the data through REST calls.
- MySQL REST Service - Quickstart Guide - This book provides a hands-on approach to learning how to use the MySQL REST service.
- MySQL REST Service - Core REST APIs - This book provides examples of using the MySQL REST Service queries and other operations against tables and views after you have REST-enabled them.
- MySQL REST Service - SQL Reference - This book discusses the MySQL REST Service SQL Extension.

# 1 Introduction to the MySQL REST Service Client SDK

The MySQL REST Service offers a Software Development Kit (SDK) that makes it easier to write client applications and interact with the REST service.

The SDK features a Client API that is specifically generated for each REST Service. This makes it possible to provide the best possible support for each REST project.

The SDK is generated for a specific development language. Right now, TypeScript and Python are supported. Support for other languages is planned. Each language-specific SDK has its own individual and independent version number. Versioning follows the rules specified by Semantic Versioning 2.0.0.

Most of the examples in this guide are written in TypeScript. If you want more details about the SDK for a different language, check the API reference docs.

## 1.1 SDK Cheat Sheet

SDK Cheat Sheet
Scope | TypeScript | Python | Description
--- | --- | --- | ---
All | getMetadata | get_metadata | Returns the metadata of a REST service, schema, view, function or procedure.
Service | getAuthApps | get_auth_apps | Used to authenticate with the REST service.
| authenticate | authenticate | Used to authenticate with the REST service.
| deauthenticate | deauthenticate | Used to close an authenticated session to the REST service.
View | create | create | Creates a new document on a given REST view endpoint.
| createMany | create_many | Creates a list of document on a given REST view endpoint.
| find | find | Reads the first page of documents of a search request and returns an iterator.
| findFirst | find_first | Reads the first matching document of a search request.
| findFirstOrThrow | find_first | Reads the first matching document of a search request and throws an error if not found.
| findUnique | find_unique | Reads the first matching document of a primary key lookup.
| findUniqueOrThrow | find_unique_or_throw | Reads the first matching document of a primary key lookup and throws if not found.
| delete | delete | Deletes a given document from a REST view endpoint.
| deleteMany | delete_many | Deletes several documents from a REST view endpoint.
| update | update | Updates a given document on a REST view endpoint.
| updateMany | update_many | Updates several documents on a REST view endpoint.
Document | update | update | Updates a REST document that was fetched before.
| delete | delete | Deletes a REST document that was fetched before.
Function | call | call | Calls a REST function.
| start | start | Calls an async REST function and returns a task.
Procedure | call | call | Calls a REST procedure.
| start | start | Calls an async REST procedure and returns a task.
Task | watch | watch | Watches a Task for progress and result.
| kill | kill | Terminates an async REST function or REST procedure call.

## 1.2 Generation of SDK Files

Once a REST service has been defined, the corresponding SDK can be generated in the required development language.

Several different methods can be used to perform the actual generation process.

- Generating the SDK files from MySQL Shell for VS Code
  - When using VS Code or VSCodium and the MySQL Shell for VS Code extension, the SDK for a given REST service can be generated directly from the UI.
  - While using the MySQL Shell for VS Code extension, the SDK will be generated on the fly when using a DB Notebook to enabled instant prototyping of the SDK API calls.
- Generating the SDK Files from the Command Line
  - To integrate the SDK generation into an existing development process, it is possible to use the MySQL Shell on the command line to generate the SDK files.

An constructor/initializer for the client-side REST service instance is generated based on the conventions established for the selected programming language. It allows to optionally specify the base URL of that REST service, has deployed in the MySQL Router instance used by the MRS installation. This would override the base URL specified when the SDK is generated in the first place, using the MySQL Shell.

### 1.2.1 Generating the SDK Files from MySQL Shell for VS Code

To generate the SDK files for a development project, right click on the MRS Service and select `Dump to Disk > Dump REST Client SDK Files ...`. This will allow you to select a destination folder inside your development project the files will be placed in.

The following files will be placed in the selected folder.

### 1.2.2 On the Fly Generation of TypeScript SDK in VS Code

The MySQL Shell for VS Code extension allows interactive execution of TypeScript code inside a DB Notebook. To make working with the MySQL REST Service easier, the TypeScript SDK for the current REST Service is made available directly within the DB Notebooks.

Whenever a REST DB Object has being edited, the TypeScript SDK is updated to allow instant prototyping of REST queries using the Client API.

This allows for adjusting and fine tuning the REST views and routines till they exactly meet the developer’s requirements and to prototype Client API calls for a development project.

### 1.2.3 Generating the SDK Files from the Command Line

To generate the SDK files on the command line, the MySQL Shell needs to be downloaded and installed.

> When using the MySQL Shell for VS Code extension, the MySQL Shell executable is made available at `~/.mysqlsh-gui/mysqlsh` and a dedicated installation of the MySQL Shell is not required.

The following template shows how to call the `mrs.dump.sdk_service_files` plugin function to perform the SDK generation.

    mysqlsh dba@localhost --py -e 'mrs.dump.sdk_service_files(directory="/path/to/project/sdk", options={"sdk_language": "TypeScript", "service_url":"https://example.com/myService"})'

The full list of parameters include the following.

    \? mrs.dump.sdk_service_files
    NAME
          sdk_service_files - Dumps the SDK service files for a REST Service

    SYNTAX
          mrs.dump.sdk_service_files([kwargs])

    WHERE
          kwargs: Dictionary - Options to determine what should be generated.

    DESCRIPTION
          Returns:

              True on success

          The kwargs parameter accepts the following options:

          - directory: String - The directory to store the .mrs.sdk folder with the
            files.
          - options: Dictionary - Several options how the SDK should be created.
          - session: Object - The database session to use.

          The options option accepts the following options:

          - service_id: String - The ID of the service the SDK should be generated
            for. If not specified, the default service is used.
          - db_connection_uri: String - The dbConnectionUri that was used to export
            the SDK files.
          - sdk_language: String - The SDK language to generate.
          - add_app_base_class: String - The additional AppBaseClass file name.
          - service_url: String - The url of the service.
          - version: Integer - The version of the generated files.
          - generationDate: String - The generation date of the SDK files.
          - header: String - The header to use for the SDK files.

### 1.2.4 Important Notes

The identifiers used to name each corresponding REST resource (services, schemas and/or objects) in the SDK are based on their corresponding request path segment.

These are generated using the most common convention for each language - TypeScript and Python - which means that the identifier generated for a request path such as `/myRequestPath` would be equivalent to the one generated for `/my_request_path`. To avoid a naming conflict in this case, the code generator keeps track of potential conflicts and appends a suffix to duplicate identifiers which corresponds to an increasing number based on the current total of duplicates. Following the sorting rules by omission in MySQL, under these circumstances, the snake_case version takes precedence over the camelCase version.

Request Path | TypeScript Class | Python Class | TypeScript Property | Python Property
--- | --- | --- | --- | ---
`/my_request_path` | `MyRequestPath` | `MyRequestPath` | `myRequestPath` | `my_request_path`
`/myRequestPath` | `MyRequestPath1` | `MyRequestPath1` | `myRequestPath1` | `my_request_path1`

Naming conflicts can also happen with the static identifiers used the native SDK functions available at each REST resource level (service, schema and/or object), examples can be as follows (depending on the level):

- `authenticate`
- `deauthenticate`
- `getMetadata`/`get_metadata`
- `getAuthApps`/`get_auth_apps`

In this case, the non static identifiers used for schema or object names will contain the corresponding suffix.

Additionally, request paths with a leading numeric character are also perfectly valid, but without special handling, the resulting identifiers would lead to syntax errors in both languages. In this case, following the common convention, the generated identifier contains an extra leading `_`.

# 2 Working with REST Services

The initializer returns an objects that implements the interface described in the API reference docs.

For a REST service available under the root path `/myService`, the corresponding client-side object can be created, on TypeScript, as follows:

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

or, using a custom base URL:

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService("https://localhost:8443/myService");

Similarly, on Python, the client-side object can be created as follows:

    from sdk.my_service import *

    my_service = MyService()

or, using a custom base URL:

    from sdk.my_service import *

    my_service = MyService(base_url="https://localhost:8443/myService")
    # or just
    my_service = MyService("https://localhost:8443/myService")

## 2.1 Authentication

When a REST object requires authentication, applications using the SDK should authenticate in the scope of the corresponding REST service beforehand. A client can be authenticated using an existing authentication app, providing a valid username and password (and optionally, a vendor id).

If a vendor id is not specified, the SDK automatically looks up the appropriate vendor id for the corresponding authentication app (which results in an extra round-trip to the MRS backend).

Currently, the MRS SDK (both for TypeScript and Python) only supports MRS Native and MySQL Internal Authentication apps (more details here).

### 2.1.1 MRS Native Authentication

With an authentication app created as follows:

    CREATE REST AUTH APP baz ON SERVICE /myService VENDOR MRS;

and a REST user created as follows:

    CREATE REST USER "foo"@"baz" IDENTIFIED BY "bar";

TypeScript

    myService.authenticate({ username: "foo", password: "bar", app: "baz" })

Python

    my_service.authenticate(username="foo", password="bar", app="baz")

### 2.1.2 MySQL Internal Authentication

In the same way, with an authentication app created as follows:

    CREATE REST AUTH APP qux ON SERVICE /myService VENDOR MYSQL;

and, this time, an actual MySQL server account created as follows:

    CREATE USER foo IDENTIFIED BY "bar";

the API is used in the exact same way.

TypeScript

    myService.authenticate({ username: "foo", password: "bar", app: "qux" })

Python

    my_service.authenticate(username="foo", password="bar", app="baz")

After the authentication succeeds, every valid SDK command that executes on top of a REST object that requires authentication, should also succeed.

### 2.1.3 Authentication Errors

In the case where a vendor id is not specified when calling the command, the client performs a vendor lookup in the backend using the name of the authentication app. If the authentication app does not exist, the command yields an error.

    try {
        await myService.authenticate({ username: "foo", password: "bar", app: "<non_existing>" })
    } catch (err) {
        console.log(err.message) // "Authentication failed. The authentication app does not exist."
    }

In the case where a vendor id is specified when calling the command, the client does not perform any additional vendor lookup, which means that it assumes the command was provided with the name of an authentication app of that same vendor and simplify attempts to authenticate using the appropriate authentication mechanism. Ultimately the authentication will fail and the command will return an error.

    const result = await myService.authenticate({
        username: "foo",
        password: "bar",
        app: "<app_from_different_vendor>",
        vendor: "<vendor_id>"
    })

    console.log(result.errorMessage) // Authentication failed. The authentication app is of a different vendor.

Additionally, the command will, as expected, also yield an error when the password does not match the given username.

### 2.1.4 Deauthentication

Once a user is authenticated, it can logout from a given service, by calling the `deauthenticate` command as follows:

    await myService.deauthenticate()

If no user is authenticated, calling the command yields an error as follows:

    try {
        await myService.deauthenticate()
    } catch (err) {
        console.log(err.message) // No user is currently authenticated.
    }

# 3 Working with REST Views

## 3.1 Create a New Document

To insert a new document on a REST view, the `create` API method is used.

### 3.1.1 Example of Inserting a New Document

Given the REST view `/actor` defined as follows, this example shows how to insert a new document.

    CREATE REST SERVICE IF NOT EXISTS /myService;

    CREATE REST SCHEMA IF NOT EXISTS /sakila ON SERVICE /myService FROM sakila;

    CREATE OR REPLACE REST VIEW /actor
        ON SERVICE /myService SCHEMA /sakila
        AS sakila.actor CLASS MyServiceSakilaActor @INSERT @UPDATE @DELETE {
            actorId: actor_id @SORTABLE @KEY,
            firstName: first_name,
            lastName: last_name,
            lastUpdate: last_update
        }
        AUTHENTICATION REQUIRED;

> Inserting a new document in the `actor` table does not require either the `actorId` field or the `lastUpdate` field because the former is an auto-generated primary key (`AUTO_INCREMENT`) whereas the latter maps to a column with a default value `CURRENT_TIMESTAMP()`.

TypeScript

    myService.sakila.actor.create({ data: { firstName: "FOO", lastName: "BAR" } })

Python

    my_service.sakila.actor.create(data={"first_name": "FOO", "last_name": "BAR"})

## 3.2 Read Documents

To fetch documents from a REST view the family of `find` API commands is used. Each of these commands covers a specific use case when looking for documents.

Family of `find` API Commands
API Command | Description
--- | ---
find() | Fetches a page of the list of documents that were found.
findFirst() | Fetches the first document that was found.
findFirstOrThrow() | Same as findFirst() but throws when there was no document found.
findUnique() | Fetches the first document that matches a unique key lookup.
findUniqueOrThrow() | Same as findUnique() but throws when there was no document found.

> Please not that exact spelling of the API commands depends on the actual SDK language used, as its specific naming conventions (e.g. snake_case for Python) are honored.

### 3.2.1 Querying Data in Multiple Pages

When a query for a REST View produces multiple documents, those are sent to the client organized in a set of pages that can be requested on demand. Each page contains, by default, 25 documents at most. The page size can be customized at the REST object level or by using the `take` option in the `find()` command. The command works by fetching the first page of documents, and provides additional infrastructure to keep consuming more matching documents whilst they exist.

For example, retrieving the first 50 documents with the default page size can be done as follows:

    let countries = await myService.sakila.country.find();
    print(countries)
    if (countries.hasMore) {
        countries = await countries.next();
        print(countries)
    }
    [
      {
        "country": "Afghanistan",
        "countryId": 1,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      },
      // ...
      {
        "country": "Congo, The Democratic Republic of the",
        "countryId": 25,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      },
    ]
    [
      {
        "country": "Czech Republic",
        "countryId": 26,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      },
      // ...
      {
        "country": "Japan",
        "countryId": 50,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      }
    ]

Retrieving all documents under a given filter, whilst skipping an initial number of them and lowering the page size can be done as follows:

    let countries = await myService.sakila.country.find({ where: { country: { $like: "C%" } }, take: 3, skip: 2 });
    print(countries)
    while (countries.hasMore) {
        countries = await countries.next();
        print(countries)
    }
    [
      {
        "country": "Canada",
        "countryId": 20,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      },
      {
        "country": "Chad",
        "countryId": 21,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      },
      {
        "country": "Chile",
        "countryId": 22,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      },
    ]
    // ...
    [
      {
        "country": "Czech Republic",
        "countryId": 26,
        "lastUpdate": "2006-02-15 04:44:00.000000",
      }
    ]

### 3.2.2 Querying Data Across Relational Tables

MySQL supports foreign keys, which permit cross-referencing related data across tables, and foreign key constraints to help keep the related data consistent.

A foreign key relationship involves a parent table that holds the initial column values, and a child table with column values that reference the parent column values. A foreign key constraint is defined on the child table. Foreign keys enable establishing one-to-one, one-to-many or many-to-many relationships between rows in those tables.

With the MySQL REST Service, these relationships can be expanded to include related data from different tables embedded in the same result set with the REST data mapping view feature available for each MRS database object. The client can then select which columns should be expanded using a specific HTTP query syntax to specify and navigate along the nesting path of columns on other tables that are referenced by a root column in the main (or parent) table.

A key feature of the MRS SDK is the ability to query these relations between two database objects and include or exclude specific columns from the query response.

This feature is available using the `select` option in the following API commands:

- `findFirst()`
- `find()`
- `findUnique()`

By default, all the object fields (expanded or not) and their values are returned in the query response. Specific fields can be excluded from the query response using a plain object format in which the properties are the names of the fields to exclude and each value is `false`.

With a setup using the Sakila Sample Database where the schema is available under a REST service called `myService` and the relationship between the city and country tables (one-to-one) is expanded via the REST data mapping view feature, the `lastUpdate` and `country.lastUpdate` fields can be excluded as follows:

    myService.sakila.city.findFirst({ select: { lastUpdate: false, country: { lastUpdate: false } } })
    {
      "city": "A Coruña (La Coruña)",
      "cityId": 1,
      "country": {
        "country": "Spain",
        "countryId": 87
      },
      "countryId": 87
    }

In the same way, if the relationship between the `actor` and `film` tables (many-to-many) is expanded, the following command excludes the identifiers on each nested object:

    myService.sakila.actor.findFirst({ select: { filmActor: { actorId: false, film: { filmId: false, languageId: false, originalLanguageId: false } } } })
    {
      {
      "actorId": 58,
      "lastName": "AKROYD",
      "filmActor": [
        {
          "film": {
            "title": "BACKLASH UNDEFEATED",
            "length": 118,
            "rating": "PG-13",
            "lastUpdate": "2006-02-15 05:03:42.000000",
            "rentalRate": 4.99,
            "description": "A Stunning Character Study of a Mad Scientist And a Mad Cow who must Kill a Car in A Monastery",
            "releaseYear": 2006,
            "rentalDuration": 3,
            "replacementCost": 24.99,
            "specialFeatures": "Trailers,Behind the Scenes"
          },
          "filmId": 48,
          "lastUpdate": "2006-02-15 05:05:03.000000"
        },
        // ...
      ],
      "firstName": "CHRISTIAN",
      "lastUpdate": "2006-02-15 04:34:33.000000"
    }

On the other hand, fields can be cherry-picked and included in the query response by using either the same object format and setting the value to `true`, or alternatively, using a list of field names to include.

In the same way, this is possible for one-to-one relationships:

    myService.sakila.city.findFirst({ select: { city: true, country: { country: true } } })
    {
      "city": "A Coruña (La Coruña)",
      "country": {
        "country": "Spain",
      }
    }

And also for many-to-many relationships:

    myService.sakila.actor.findFirst({ select: ['filmActor.film.title'] })
    {
      "filmActor": [
        {
          "film": {
            "title": "BACKLASH UNDEFEATED"
          }
        },
        {
          "film": {
            "title": "BETRAYED REAR"
          }
        }
        // ...
      ]
    }

## 3.3 Updating A Document

The SDK offers two different methods of how to update an existing document on a REST view.

1. Using the REST view `update` method.
   - REST view class exposes an `update` API method that can be called with the new document data. In this case, all fields, including the primary key fields need to be specified explicitly.
2. Using the Document API.
   - When a MRS document has been fetched before using the `find` API methods, the updates can be applied directly to the fields of that document. After all changes have been performed, the `update` method of the document object can be called. Please see the Document API section for more details.

### 3.3.1 Updating a Document Using the REST View update Method

Updating a document on the REST view requires all fields to be specified if they are not nullable.

In the following example, neither `firstName` nor `lastName` are nullable and have to be specified. On the other hand, the `description` column in the `film_text` table is nullable.

TypeScript

    myService.sakila.actor.update({ data: { id: 1, firstName: "PENELOPE", lastName: "CRUZ" } }) // Property 'lastUpdate' is missing in type '{ actorId: number; lastName: string; firstName: string; }' but required in type 'IUpdateMyServiceSakilaActor'.
    myService.sakila.filmText.update({ data: { film_id: 1, title: "FOO" } })

Python

    my_service.sakila.actor.update(data={"id": 1, "first_name": "PENELOPE", "last_name": "CRUZ"}) # Missing key "last_update" for TypedDict "IUpdateMyServiceSakilaActor"
    my_service.sakila.film_text.update(data={"film_id": 1, "title": "FOO"})

### 3.3.2 Updating a Document using the Document API

When fetching documents from REST view endpoints, the SDK offers a convenient API to work with those objects in a object-oriented way, by using `update` and `delete` methods that can be directly called on the document.

The `update` and `delete` methods are only available if the corresponding REST View enables the “UPDATE” and/or “DELETE” CRUD operations, respectively and specifies the appropriate identifier fields (mapping to underlying database primary keys).

> In the TypeScript SDK, the identifier fields that are part of a REST Document are read-only. This is currently not the case on the Python SDK.

TypeScript

    let actor = await my_service.sakila.actor.find_first()
    if (actor) {
        console.log(actor.actorId) // 1
        console.log(actor.lastName) // "GUINESS"
        actor.lastName = "NOGUINESS"
        await actor.update()
    }

    actor = await my_service.sakila.actor.find_first()
    if (actor) {
        console.log(actor.lastName) // "NOGUINESS"
        await actor.delete()
    }

    actor = await my_service.sakila.actor.find_first()
    if (actor) {
        console.log(actor.actorId) // 2
    }

Python

    actor = await my_service.sakila.actor.find_first()
    if actor:
        print(actor.actor_id) # 1
        print(actor.last_name) # "GUINESS"
        actor.last_name = "NOGUINESS"
        await actor.update()

    actor = await my_service.sakila.actor.find_first()
    if actor:
        print(actor.last_name) # "NOGUINESS"
        await actor.delete()

    actor = await my_service.sakila.actor.find_first()
    if actor:
        print(actor.actor_id) # 2

### 3.3.3 Language Specific Implementation Details

All MRS SDK commands that return back to the application one or more instances of REST documents perform some internal plumbing to simplify the client-side data structure, by ensuring that SDK-specific details such as protocol resource metadata (which includes things like ETags and GTIDs) or HATEOAS-specific properties (such as links and pagination control fields) are not exposed but are still able to be tracked at runtime. This is important because, even though those details are not supposed to be handled by an application, they can still determine specific behaviors when the application executes an SDK command.

For instance, when updating a REST document, the corresponding ETag must be sent to the MySQL Router, in order to detect mid-air collisions and make sure that changes happened in the document, after it was retrieved by the application in the first place, are not overridden. In the same way, a command executed by the application that can write data (`INSERT` or `UPDATE`) will spawn a server-side transaction that can generate a GTID which must also be sent to the MySQL Router if the application requires read consistency in a setup consisting of multiple server instances.

Hiding and locking these details involves either wrapping the actual data responses sent by the MySQL Router or applying specific access control constraints on top of the details available on those responses. In TypeScript, this is done by wrapping a client-side instance in a `Proxy` object. In Python, this is done by wrapping it in a `dataclass`.

This results in something as follows:

    const actor = await myService.sakila.actor.findFirst()

    try {
        delete actor._metadata
    } catch (err) {
        console.log(err.message) // The "_metadata" property cannot be deleted.
    }

    try {
        actor._metadata = { foo: "bar" }
    } catch (err) {
        console.log(err.message) // The "_metadata" property cannot be changed.
    }

Additionally, these wrappers allow to augment the object representation of a REST Document with a small contextual API which contains utility commands (`update()` and `delete()`, names are self-describing) that operate directly in the scope of each particular document.

### 3.3.4 Contextual fields and parameters

There are different requirements at play when inserting a new document or when updating an existing document in a Table or View using the MySQL REST Service. For starters, since there is currently no support for partial updates, this means that every time an application wants to update a row, it needs to provide a complete representation of the row as it will become. That representation can still determine potential columns which will be “unset” notwithstanding, at least for those cases where the columns do not impose a constraint that prevents such an action (this is in line with what happens on ORDS). On the other hand, even though there is no specific limitation for inserting new rows, an application should still be aware of the underlying column constraints to reduce the friction by requiring the minimum possible set of fields or alternatively, provide better user feedback (e.g. using the type checker) when it comes to missing fields required to perform the operation.

From the MRS SDK standpoint this means that the type definitions used to insert and update rows must be capable of making the distinction between required and optional fields. In practice, a field should always be required unless there is some specific circumstance that allows it to be optional such as the fact that it maps to an auto-generated primary key column, a foreign key column, a nullable column or a column with a default value. Whilst inserting a value, all of these circumstances are valid and should be accounted for, whereas whilst updating a value, due to the limitations described above, it only makes sense for a field to be optional when it maps to a nullable column or column with row ownership.

## 3.4 Deleting A Document

Similar to updating a document, deleting a document can be done using either the REST view `delete` method or the Document API `delete` method called directly on the object.

Please see above to learn how to delete a document via the Document API.

## 3.5 Read Your Writes Consistency

With multiple MySQL server instances running as an InnoDB Cluster/ClusterSet, data read from one instance might be dependent on data written on a different instance, which might not have been yet replicated to the server where the data is being read from. This is a classical concern on distributed systems which alludes to the consistency of the data and the problem has been formalized as a concept called Read Your Writes.

To solve this issue, and ensure an application is always able to read its own writes, MySQL uses a Global Transaction ID (GTID), whose definition, according to the official documentation, is:

A global transaction identifier (GTID) is a unique identifier created and associated with each transaction committed on the server of origin (the source). This identifier is unique not only to the server on which it originated, but is unique across all servers in a given replication topology.

It is, in essence, and in layman’s terms, an identifier that is provided to a client for each “write” operation, which the client can then provide back to the MySQL server cluster which can use it to ensure any subsequent read accounts for all the data written up until the operation that generated that GTID. This usually carries a cost, and for that reason, is a behavior that needs to be explicitly enabled by the end user depending on what kind of topology an application is using.

In the MySQL REST Service, it is possible to ensure an application is able to read its own writes consistently in a cluster of MySQL instances only when retrieving resources or deleting resources. Using the TypeScript SDK, this can be done with the `readOwnWrites` option available for the following commands:

- `find()`
- `findFirst()`
- `findFirstOrThrow()`
- `findUnique()`
- `findUniqueOrThrow()`
- `delete()`
- `deleteMany()`

    myService.sakila.actor.findFirst({ readOwnWrites: true })

This option is only relevant when the application is running on top of a MySQL instance cluster where the GTID infrastructure is specifically configured and enabled, otherwise the option will be ignored.

# 4 Working with REST Routines

In its most basic form, a REST Function or Procedure can be executed with the MRS SDK using the `call()` command. The command must receive, as input, the SET of `IN` and/or `INOUT` parameters (and corresponding values) allowed by the database routine.

The examples assume a setup using the Sakila Sample Database where the schema and the corresponding tables and routines are available under a REST service called `myService`.

Consider the following REST Function based on the `inventory_in_stock` function available in the Sakila Sample Database.

    CREATE OR REPLACE REST FUNCTION /inventoryInStock
        ON SERVICE /myService SCHEMA /sakila
        AS sakila.inventory_in_stock
        PARAMETERS MyServiceSakilaInventoryInStockParams {
            pInventoryId: p_inventory_id @IN
        }
        RESULT MyServiceSakilaInventoryInStockResult {
            result: result @DATATYPE("bit(1)")
        }
        AUTHENTICATION NOT REQUIRED;

TypeScript

In the TypeScript SDK, the command accepts, as its first parameter, an object containing the set of `IN` and/or `INOUT` parameters (and corresponding values).

    myService.sakila.inventoryInStock.call({ pInventoryId: 1 });
    // true

Python

In the Python SDK, the command accepts the same set of parameters and values as keyword arguments:

    my_service.sakila.inventory_in_stock.call(p_inventory_id=1)
    # true

For Functions or Procedures, all fields (or input parameters in this case) should be considered optional because they cannot have `NOT NULL` constraints, which always makes them nullable by nature. Thus, an optional parameter is just a parameter where the value can be `NULL`.

Calling a function or procedure does not require any field to be specified because input parameters are nullable by nature (there is no syntax to add `NOT NULL` constraints). It is expected that functions and procedures handle `NULL` values at runtime accordingly. For example, with a MySQL `FUNCTION` as follows:

    DELIMITER //
    CREATE FUNCTION my_db.my_func (x INT, y INT)
    RETURNS BIGINT DETERMINISTIC
    BEGIN
      DECLARE sum_result BIGINT DEFAULT 0;
      IF y is NULL THEN
        SET sum_result = x;
      ELSE
        SET sum_result = x + y;
      END IF;
      RETURN sum_result;
    END //
    DELIMITER ;

where the corresponding REST object is created as follows:

    CREATE OR REPLACE REST FUNCTION /myFunc ON SERVICE /myService SCHEMA /myDb AS my_db.my_func
      PARAMETERS IMyServiceMyDbMyFuncParams {
        x: x @IN,
        y: y @IN
      }

TypeScript

    myService.myDb.myFunc.call() // null
    myService.myDb.myFunc.call({ x: 3 }) // 3
    myService.myDb.myFunc.call({ x: 3, y: 2 }) // 5

Python

    my_service.my_db.myFunc() # None
    my_service.my_db.myFunc(x=3) # 3
    my_service.my_db.myFunc(x=3, y=2) # 5

Procedures can produce output either via result sets ou `OUT`/`INOUT` parameters. For example, with a MySQL `PROCEDURE` as follows:

    DELIMITER //
    CREATE PROCEDURE my_db.my_proc (IN x INT, IN y INT, OUT z INT)
    BEGIN
      DECLARE sum_result BIGINT DEFAULT 0;
      IF y is NULL THEN
        SET sum_result = x;
      ELSE
        SET sum_result = x + y;
      END IF;
      SELECT sum_result INTO z;
      SELECT sum_result AS sum_result;
    END //
    DELIMITER ;

where the corresponding REST object is created as follows:

    CREATE OR REPLACE REST PROCEDURE /myProc ON SERVICE /myService SCHEMA /myDb AS my_db.my_proc
      PARAMETERS IMyServiceMyDbMyProcParams {
        x: x @IN,
        y: y @IN,
        x: z @OUT
      }
      RESULT MyServiceMyDbMyProcResult {
        sum_result: sum_result @DATATYPE("INT")
      }

TypeScript

    myService.myDb.myProc.call() // { resultSets: [{ type: "MyServiceMyDbMySumResult", items: [{ sum_result: null }] }], outParameters: { z: null } }
    myService.myDb.myProc.call({ x: 3 }) // { resultSets: [{ type: "MyServiceMyDbMySumResult", items: [{ sum_result: 3 }] }], outParameters: { z: 3 } }
    myService.myDb.myProc.call({ x: 3, y: 2 }) // { resultSets: [{ type: "MyServiceMyDbMySumResult", items: [{ sum_result: 5 }] }], outParameters: { z: 5 } }

Python

    my_service.my_db.myProc() # IMrsProcedureResponse(result_sets=[MrsProcedureResultSet(type='MyServiceMyDbMySumResult', items=[{'sum_result': None}])], out_parameters={'z': None}
    my_service.my_db.myProc(x=3) # IMrsProcedureResponse(result_sets=[MrsProcedureResultSet(type='MyServiceMyDbMySumResult', items=[{'sum_result': 3}])], out_parameters={'z': 3}
    my_service.my_db.myProc(x=3, y=2) # IMrsProcedureResponse(result_sets=[MrsProcedureResultSet(type='MyServiceMyDbMySumResult', items=[{'sum_result': 5}])], out_parameters={'z': 5}

## 4.1 Async Task Support

Long-running REST Functions/Procedures can use the MySQL Async Task framework to spawn a monitoring task which can be asynchronously checked for updates by the client, in order to avoid directly executing the routine and hit any existing HTTP request or MySQL Router handling timeouts.

Using the MRS TypeScript SDK, applications can either manually monitor tasks spawned for a given REST routine, or simply execute the routine without having to worry about those issues.

In this case, for REST routine with an associated Async Task, the SDK will produce the same compatible `call()` command, with support for an additional object that allows to specify a set of execution options, namely:

- `refreshRate` specifies the interval (ms) between each status update check
- `progress` specifies an asynchronous callback that is executed with the details of each status update report
- `timeout` specifies the maximum time (ms) to wait for the execution to complete if this threshold is reached, the ongoing task is killed

As an example, consider the REST Function depicted above has an associated Async Task. Executing the task, whilst obtaining the status update reports generated by that task can be done, in TypeScript, as follows:

    myService.sakila.inventoryInStock.call({ pInventoryId: 1 }, { progress: (r) => console.log(r) });

Additionally, the SDK produces a `start()` command, which starts the task and allows to manually watch for status updates and/or kill the task (cancelling the execution of the actual routine). The command accepts, as its first an only argument, the same set of `IN` and/or `INOUT` parameters (and corresponding values) and returns back a `Task` object which provides the API for task-level actions (check the reference API docs for more details).

Starting the task and cancelling it if takes longer than a specified amount of time to finish can be done, in TypeScript, as follows:

    const task = myService.sakila.inventoryInStock.start({ pInventoryId: 1 }, { timeout: 10000 });

    for await (const report of task.watch()) {
      if (report.status === "TIMEOUT") {
        await task.kill();
      } else if (report.status === "CANCELLED") {
        // this block is executed after the task is killed
        console.log(report.message);
      }
    }

Getting the actual result produced by the REST routine can be done as follows:

    const task = myService.sakila.inventoryInStock.start({ pInventoryId: 1 });

    for await (const report of task.watch()) {
      if (report.status === "COMPLETED") {
        console.log(report.data.result); // true
      }
    }

# 5 TypeScript Client API Reference

The MRS TypeScript SDK requires TypeScript 5.3.3+.

## 5.1 getMetadata

`getMetadata` is used to retrieve application-specific metadata attached to an MRS resource (REST Service, Schema and/or Object).

### 5.1.1 Return Type (getMetadata)

A JSON object containing the application-specific metadata attached to the resource.

### 5.1.2 Example (getMetadata)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    await myService.getMetadata();
    await myService.mrsNotes.getMetadata();
    await myService.mrsNotes.note.getMetadata();

## 5.2 Service.getAuthApps

Use `getAuthApps` to get a list of available REST authentication apps for the given REST service. A REST service may be linked to several REST auth apps and therefore it is necessary to choose the right one for authentication.

The name of the REST auth apps needs to be passed to the `Service.authenticate` method when performing the authentication process.

## 5.3 Service.authenticate

Use `authenticate` to authenticate in a given REST service using a given authentication app.

### 5.3.1 Options (authenticate)

REST Service Options for Authentication
Name | Type | Required | Description
--- | --- | --- | ---
username | string | Yes | Username in the scope of the authentication app.
password | string | No | Password in the scope of the authentication app.
authApp | string | Yes | Name of the authentication app.

### 5.3.2 Return Type (authenticate)

Nothing (void).

### 5.3.3 Reference (authenticate)

    async function authenticate (IAuthenticateOptions): Promise<IMrsLoginResult> {
        // ...
    }

    interface IAuthenticateOptions {
        username: string
        password: string
        app: string
        vendor?: string
    }

    interface IMrsLoginResult {
        authApp?: string
        jwt?: string
        errorCode?: number
        errorMessage?: string
    }

### 5.3.4 Example (authenticate)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    await myService.authenticate({ username: 'foo', password: 'bar', app: 'baz' });
    await myService.authenticate({ username: 'foo', password: 'bar', app: 'baz', vendor: "0x30000000000000000000000000000000" });

## 5.4 Service.deauthenticate

`deauthenticate` is used for logging out a user from a given REST service.

### 5.4.1 Return Type (deauthenticate)

Nothing (void).

#### 5.4.1.1 Reference (deauthenticate)

    async function deauthenticate (): Promise<void> {
        // ...
    }

### 5.4.2 Example (deauthenticate)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    await myService.deauthenticate();

## 5.5 View.create

`create` is used to add a REST Document to a given REST View. The document is represented as a plain TypeScript/JavaScript object or, alternatively, as an instance of a particular class that encapsulates the data required to create a new document. To insert multiple documents, see `createMany`[#createmany].

### 5.5.1 Options (create)

REST View Options (create)
Name | Type | Required | Description
--- | --- | --- | ---
data | object | Yes | Object containing the mapping between column names and values for the REST Document to be created.

### 5.5.2 Return Type (create)

A JSON object representing the created REST Documents.

### 5.5.3 Reference (create)

    async function create (args: ICreateOptions<Type>): Promise<Type> {
        // ...
    }

    interface ICreateOptions<Type> {
        data: Type
    }

### 5.5.4 Example (create)

    import type { IMyServiceMrsNotesNote } from '/path/to/sdk/myService';
    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // using a plain object
    myService.mrsNotes.note.create({ data: { title: 'foo' } });

    // using a custom class instance
    class Note implements IMyServiceMrsNotesNote {
        // ...
    }

    const note = new Note();
    note.title = 'foo';

    myService.mrsNotes.note.create({ data: note });

## 5.6 View.createMany

`createMany` adds one or more REST Documents to a given REST View. The documents are represented as plain TypeScript/JavaScript objects, or alternatively, as instances of a particular class that encapsulates the data required to create them.

### 5.6.1 Options (createMany)

REST View Options (createMany)
Name | Type | Required | Description
--- | --- | --- | ---
data | object | Yes | Array of objects containing the mapping between column names and values for the REST Documents to be created.

### 5.6.2 Return Type (createMany)

An array of JSON objects representing the created REST Documents.

### 5.6.3 Reference (createMany)

    async function createMany (args: ICreateOptions<Type[]>): Promise<Type[]> {
        // ...
    }

    interface ICreateOptions<Type> {
        data: Type
    }

### 5.6.4 Example (createMany)

    import type { IMyServiceMrsNotesNote } from '/path/to/sdk/myService';
    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // using a plain object
    myService.mrsNotes.note.createMany({ data: [{ title: 'foo' }, { title: 'bar' }] });

    // using a custom class
    class Note implements IMyServiceMrsNotesNote {
        // ...
    }

    const note1 = new Note();
    note1.title = 'foo';

    const note2 = new Note({ /* */ });
    note1.title = 'bar';

    myService.mrsNotes.note.createMany({ data: [note1, note2] });

## 5.7 View.find

`find` is used to query the subset of REST Documents (that optionally match a given filter) in the first page.

### 5.7.1 Options (find)

REST View Options (find)
Name | Type | Required | Description
--- | --- | --- | ---
cursor | object | No | Retrieve documents using unique and sequential fields as cursor.
orderBy | object | No | Determines the sort order of specific fields.
select | object | No | Specifies which properties to include in the returned object.
skip | number | No | How many documents to skip before returning one of the matches.
where | object | No | Filtering conditions that apply to specific fields.
take | number | No | The maximum size of the page.
readOwnWrites | boolean | No | Ensures read consistency for a cluster of servers.

### 5.7.2 Return Type (find)

An array of JSON objects representing the first page of REST Documents matching the filter. If there are more matching REST Documents, the array contains an additional `hasMore` truthy property and a `next()` async function that automatically retrieves the subsequent page of REST Documents.

### 5.7.3 Reference (find)

    async function find ({ cursor, orderBy, select, skip, take, where }: IFindManyOptions<Item, Filterable, Cursors>): Promise<PaginatedList<Item>> {
        // ...
    }

    interface IFindManyOptions<Item, Filterable, Iterable> {
        cursor?: Cursor<Iterable>;
        orderBy?: ColumnOrder<Filterable>;
        select?: BooleanFieldMapSelect<Item> | FieldNameSelect<Item>;
        skip?: number;
        take?: number;
        where?: DataFilter<Filterable>;
        readOwnWrites?: boolean;
    }

    export interface IExhaustedList<T> extends Array<T> {
        hasMore: false,
    }

    export interface INotExhaustedList<T> extends Array<T> {
        hasMore: true,
        next(): Promise<PaginatedList<T>>,
    }

    export type PaginatedList<T> = IExhaustedList<T> | INotExhaustedList<T>;

### 5.7.4 Example (find)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // get all notes of the first page
    await myService.mrsNotes.note.find();
    // get the first 3 notes
    await myService.mrsNotes.note.find({ take: 3 });
    // get notes of then first page where the id is greater than 10
    await myService.mrsNotes.note.find({ where: { id: { $gt: 10 } } });

    // iterate over the pages
    let notes = await myService.mrsNotes.note.find();
    if (notes.hasMore) {
        // automatically get the next page (if there is one)
        notes = await notes.next();
    }

## 5.8 View.findFirst

`findFirst` is used to query the first REST Document (in no specific order) that matches a given optional filter.

### 5.8.1 Options (findFirst)

REST View Options (findFirst)
Name | Type | Required | Description
--- | --- | --- | ---
where | object | No | Filtering conditions that apply to specific fields.
select | object | No | Specifies which properties to include in the returned object.
skip | number | No | Specifies how many documents to skip before returning one of the matches.
readOwnWrites | boolean | No | Ensures read consistency for a cluster of servers.

### 5.8.2 Return Type (findFirst)

A JSON object representing the first REST Document that matches the filter or `undefined` when the document was not found.

### 5.8.3 Reference (findFirst)

    async function findFirst (args?: IFindOptions<Selectable, Filterable>): Promise<Selectable | undefined> {
        // ...
    }

    export interface IFindOptions<Selectable, Filterable> {
        orderBy?: ColumnOrder<Filterable>;
        select?: BooleanFieldMapSelect<Selectable> | FieldNameSelect<Selectable>;
        skip?: number;
        where?: DataFilter<Filterable>;
        readOwnWrites?: boolean;
    }

### 5.8.4 Example (findFirst)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // get the first note, without any filter
    await myService.mrsNotes.note.findFirst();
    // get the last note, without any filter
    await myService.mrsNotes.note.findFirst({ orderBy: { id: "DESC" } });
    // get the second note, without any filter
    await myService.mrsNotes.note.findFirst({ skip: 1 });
    // get the title and shared fields of the second note
    await myService.mrsNotes.note.findFirst({ select: { title: true, shared: true }, skip: 1 });
    // get the title and shared fields of the first note
    await myService.mrsNotes.note.findFirst({ select: ["title", "shared"] });
    // get the first shared note
    await myService.mrsNotes.note.findFirst({ where: { shared: true } });
    // get the first note whose title includes the string "foo"
    await myService.mrsNotes.note.findFirst({ where: { title: { $like: "%foo%" } } });

## 5.9 View.findUnique

`findUnique` is used to query a single, uniquely identified REST Document by:

- Primary key column(s)
- Unique column(s)

If no document was found matching the given `where` condition, `undefined` is returned. To have an exception thrown in this case, see findUniqueOrThrow.

### 5.9.1 Options (findUnique)

REST View Options (findUnique)
Name | Type | Required | Description
--- | --- | --- | ---
where | object | Yes | Wraps all unique columns so that individual documents can be selected.
select | object | No | Specifies which properties to include in the returned object.
readOwnWrites | boolean | No | Ensures read consistency for a cluster of servers.

### 5.9.2 Return Type (findUnique)

A JSON object representing the REST Document that matches the filter or `undefined` when the document was not found.

### 5.9.3 Reference (findUnique)

    async function findUnique (args?: IFindUniqueOptions<Selectable, Filterable>): Promise<Selectable | undefined> {
        // ...
    }

    interface IFindUniqueOptions<Selectable, Filterable> {
        select?: BooleanFieldMapSelect<Selectable> | FieldNameSelect<Selectable>;
        where?: DataFilter<Filterable>;
        readOwnWrites?: boolean;
    }

### 5.9.4 Example (findUnique)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // Get the note with id 4.
    // using implicit equality
    await myService.mrsNotes.note.findUnique({ where: { id: 4 } });
    // or using explicit equality
    await myService.mrsNotes.note.findUnique({ where: { id: { $eq: 4 } } });

## 5.10 View.findUniqueOrThrow

`findUniqueOrThrow` retrieves a single REST Document in the same way as findUnique. However, if the query does not find a document, it throws a `NotFoundError`.

`findUniqueOrThrow` differs from `findUnique` as follows:

- Its return type is non-nullable. For example, myService.mrsNotes.note.findUnique() can return a note or undefined, but myService.mrsNotes.note.findUniqueOrThrow() always returns a note.

## 5.11 View.delete

`delete` is used to delete the first REST Document that matches a given required filter.

### 5.11.1 Options (delete)

REST View Options (delete)
Name | Type | Required | Description
--- | --- | --- | ---
where | object | Yes | Filtering conditions that apply to specific fields.
readOwnWrites | boolean | No | Ensures read consistency for a cluster of servers.

### 5.11.2 Return Type (delete)

`true` if the document was deleted successfully or `false` otherwise.

### 5.11.3 Reference (delete)

    async function delete (args: IDeleteOptions<IMyServiceMrsNotesUserParams>): Promise<IMrsDeleteResult> {
        // ...
    }

    interface IDeleteOptions<Filterable> {
        where?: DataFilter<Filterable>;
        readOwnWrites?: boolean;
    }

    interface IMrsDeleteResult {
        itemsDeleted: 1;
    }

### 5.11.4 Example (delete)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // delete the first note whose title includes the string "foo"
    await myService.mrsNotes.note.delete({ where: { title: { $like: "%foo%" } } });

## 5.12 View.deleteMany

`delete` is used to delete all REST Documents that match a given filter.

### 5.12.1 Options (deleteMany)

REST View Options (deleteMany)
Name | Type | Required | Description
--- | --- | --- | ---
where | object | No | Filtering conditions that apply to specific fields.
readOwnWrites | boolean | No | Ensures read consistency for a cluster of servers.

### 5.12.2 Return Type (deleteMany)

The number of REST Documents that were deleted.

### 5.12.3 Reference (deleteMany)

    async function deleteMany (args: IDeleteOptions<IMyServiceMrsNotesUserParams>): Promise<number> {
        // ...
    }

    interface IDeleteOptions<Filterable> {
        where?: DataFilter<Filterable>;
        readOwnWrites: boolean;
    }

#### 5.12.3.1 Example (deleteMany)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // delete all notes whose title includes the string "foo"
    await myService.mrsNotes.note.deleteMany({ where: { title: { $like: "%foo%" } } });
    // delete all shared notes
    await myService.mrsNotes.note.deleteMany({ where: { shared: true } });

## 5.13 View.update

`update` is used to update a REST Document with a given identifier or primary key.

### 5.13.1 Options (update)

REST View Options (update)
Name | Type | Required | Description
--- | --- | --- | ---
data | object | Yes | Set of fields and corresponding values to update.

### 5.13.2 Return Type (update)

A JSON object representing the up-to-date REST Document.

### 5.13.3 Reference (update)

    async function update (args: IUpdateOptions<UpdatableFields>): Promise<Data> {
        // ...
    }

    type IUpdateOptions<Type> = ICreateOptions<Type>;

### 5.13.4 Example (update)

    import type { IMyServiceMrsNotesNote } from '/path/to/sdk/myService';
    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // update the note with id is 1 using a plain object
    await myService.mrsNotes.note.update({ data: { id: 1, title: 'bar' } });

    // using a custom class instance
    class Note implements IMyServiceMrsNotesNote {
        // ...
    }

    const note = new Note();
    note.id = 1
    note.shared = false;

    // update the note with id 1
    await myService.mrsNotes.note.update({ data: note });

## 5.14 View.updateMany

`updateMany` is used to update all REST Documents with matching identifiers or primary keys.

### 5.14.1 Options (updateMany)

REST View Options (updateMany)
Name | Type | Required | Description
--- | --- | --- | ---
data | object | Yes | Set of fields and corresponding values to update.

### 5.14.2 Return Type (updateMany)

An array of JSON objects representing the up-to-date REST Documents.

### 5.14.3 Reference (updateMany)

    async function updateMany (args: IUpdateOptions<UpdatableFields[]>): Promise<Data[]> {
        // ...
    }

    type IUpdateOptions<Type> = ICreateOptions<Type>;

### 5.14.4 Example (updateMany)

    import type { IMyServiceMrsNotesNote } from '/path/to/sdk/myService';
    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // update the notes with id 1 and 2 using a plain object
    await myService.mrsNotes.note.update({ data: [{ id: 1, title: 'bar' }, { id: 2, title: 'bar' }] });

    // using a custom class instance
    class Note implements IMyServiceMrsNotesNote {
        // ...
    }

    const note1 = new Note();
    note.id = 1;
    note.shared = false;

    const note2 = new Note();
    note.id = 2;
    note.shared = false;

    // update the notes with id 1 and 2
    await myService.mrsNotes.note.update({ data: [note1, note2] });

## 5.15 Document.update

`update` is used to update a given REST document by committing the set of updates performed locally on the corresponding instance in the application.

> This function is only available if the REST View enables the “UPDATE” CRUD operation and specifies one or more identifier fields.

### 5.15.1 Reference (update)

    async function update(): Promise<IMyServiceSakilaActor> {
        // ...
    }

    interface IMyServiceSakilaActor {
        readonly actorId?: number;
        firstName?: string;
        lastName?: string;
        lastUpdate?: string;
    }

### 5.15.2 Example (update)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    const actor = await myService.sakila.actor.findFirst();
    if (actor) {
        actor.lastName = "FOO";
        const modifiedActor = await actor.update();
        console.log(modifiedActor.lastName); // FOO
    }

## 5.16 Document.delete

`delete` is used to delete a given REST document represented by a corresponding instance in the application.

> This function is only available if the REST View enables the “DELETE” CRUD operation and specifies one or more identifier fields.

### 5.16.1 Return Type (delete)

`true` if the document was deleted successfully or `false` otherwise.

### 5.16.2 Reference (delete)

    async function delete(): Promise<boolean> {
        // ...
    }

### 5.16.3 Example (delete)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    const actor = await myService.sakila.actor.findFirst();
    if (actor) {
        if (await actor.delete()) {
            console.log(`Actor ${actor.actorId} was deleted.`)
        } else {
            console.log(`Actor ${actor.actorId} was not deleted.`)
        }
    }

## 5.17 Function.call

`call` is used to execute a REST routine (`FUNCTION` or `PROCEDURE`). The first parameter of the command is an `object` containing the set of `IN`/`INOUT` parameters (and corresponding values) as specified by the database routine. The second parameter is an `object` with execution options which is only available if the REST routine has an associated Async Task.

### 5.17.1 Options (call)

REST Function/Procedure Options (call)
Name | Type | Required | Description
--- | --- | --- | ---
refreshRate | number (>=500) | No | Time (ms) to wait (default 2000) for retrieving the next progress report. (available only if the routine has an associated Async Task)
progress | async function | No | Callback to be executed using the details of each progress report while the routine does not finish.
timeout | number | No | Maximum time to wait for the execution to complete. If this threshold is reached, the ongoing task is killed. By default, no timeout is enforced. (available only if the routine has an associated Async Task)

### 5.17.2 Return Type (call)

In the case of a `FUNCTION`, the value returned by that function. In the case of a `PROCEDURE`, a JSON object containing the result produced by the procedure (including `OUT`/`INOUT` parameters and result sets).

Note: Procedures with an associated Async Task are not capable of not producing result sets, only `OUT`/`INOUT` parameter values.

### 5.17.3 Reference (call)

    async function call (noteUpdateParams?: IMyServiceMrsNotesNoteUpdateParams, options?: IMrsTaskRunOptions<object, IMrsProcedureResult<IMyServiceMrsNotesNoteUpdateParamsOut, IMyServiceMrsNotesNoteUpdateResultSet>>): Promise<IMrsProcedureResult<IMyServiceMrsNotesNoteUpdateParamsOut, IMyServiceMrsNotesNoteUpdateResultSet>> {
        // ...
    }

    interface IMyServiceMrsNotesNoteUpdateParams {
        tags?: JsonValue;
        lockedDown?: boolean;
        noteId?: number;
        title?: string;
        content?: string;
        pinned?: boolean;
        userId?: string;
    }

    interface IMrsTaskStartOptions {
        refreshRate?: number;
        timeout?: number;
    }

    interface IMrsTaskRunOptions<MrsTaskStatusUpdate, MrsTaskResult> extends IMrsTaskStartOptions {
        progress?(report: IMrsRunningTaskReport<MrsTaskStatusUpdate, MrsTaskResult>): Promise<void>;
    }

    interface IMrsRunningTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        data: MrsTaskStatusUpdate;
        status: "RUNNING";
        message: string;
        progress: number;
    }

    type IMyServiceMrsNotesNoteUpdateParamsOut = never;

    type IMyServiceMrsNotesNoteUpdateResultSet = JsonObject;

    interface IMrsProcedureResult<OutParams, ResultSet> {
        outParameters?: OutParams;
        resultSets: ResultSet[];
    }

#### 5.17.3.1 Example (call)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // update the title of a note with a given id
    await myService.mrsNotes.noteUpdate.call({ noteId: note.id, title: "hello world" });

    // execute a function for each progress status update
    const progress = (report) => {
        console.log(report.progress)
    };

    await myService.mrsNotes.noteUpdate.call({ noteId: note.id, title: "hello world" }, { progress });

## 5.18 Function.start

`start` is used to start a REST routine (`FUNCTION` or `PROCEDURE`) with an associated Async Task. The first parameter of the command is an `object` containing the set of `IN`/`INOUT` parameters (and corresponding values) as specified by the database routine. The second and last parameter of the command is an `object` with a set of routine execution constraint options.

### 5.18.1 Options (start)

Name | Type | Required | Description
--- | --- | --- | ---
refreshRate | number (>=500) | No | Time (ms) to wait (default 2000) for retrieving the next progress report (available only if the routine has an associated Async Task).
timeout | number | No | Time (ms) to wait for the routine to produce a result.

### 5.18.2 Return Type (start)

A Task instance.

### 5.18.3 Reference (start)

    async function start(params?: IMyServiceMrsNotesNoteUpdateParams, options?: IMrsTaskStartOptions): Promise<MrsTask<object, IMrsProcedureResult<IMyServiceMrsNotesNoteUpdateParamsOut, IMyServiceMrsNotesNoteUpdateResultSet>>> {
        // ...
    }

    interface IMyServiceMrsNotesNoteUpdateParams {
        tags?: JsonValue;
        lockedDown?: boolean;
        noteId?: number;
        title?: string;
        content?: string;
        pinned?: boolean;
        userId?: string;
    }

    type IMyServiceMrsNotesNoteUpdateParamsOut = never;

    type IMyServiceMrsNotesNoteUpdateResultSet = JsonObject;

    interface IMrsProcedureResult<OutParams, ResultSet> {
        outParameters?: OutParams;
        resultSets: ResultSet[];
    }

    interface IMrsTaskStartOptions {
        refreshRate?: number;
        timeout?: number;
    }

#### 5.18.3.1 Example (start)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // update the title of a note with a given id
    let task = await myService.mrsNotes.noteUpdate.start({ noteId: note.id, title: "hello world" });
    // check for status updates every 1 second
    task = await myService.mrsNotes.noteUpdate.start({ noteId: note.id, title: "hello world" }, { refreshRate: 1000 });
    // cancel the execution after 5 seconds
    task = await myService.mrsNotes.noteUpdate.start({ noteId: note.id, title: "hello world" }, { timeout: 5000 });

## 5.19 Procedure.call

`call` is used to execute a REST routine (`FUNCTION` or `PROCEDURE`). Please see Function.call for more details.

## 5.20 Procedure.start

`start` is used to start a REST routine (`FUNCTION` or `PROCEDURE`) with an associated Async Task. Please see Function.start for more details.

## 5.21 Task.watch

`watch` is used to monitor the status of a REST routine (`FUNCTION` or `PROCEDURE`) with an associated Async Task.

### 5.21.1 Return Type (watch)

An AsyncGenerator instance which produces status update reports with details about the execution context of the REST routine.

### 5.21.2 Reference (watch)

    async function watch(): AsyncGenerator<
        IMrsTaskReport<object, IMrsProcedureResult<IMyServiceMrsNotesNoteUpdateParamsOut, IMyServiceMrsNotesNoteUpdateResultSet>>> {
        // ...
    }

    type IMyServiceMrsNotesNoteUpdateParamsOut = never;

    type IMyServiceMrsNotesNoteUpdateResultSet = JsonObject;

    interface IMrsProcedureResult<OutParams, ResultSet> {
        outParameters?: OutParams;
        resultSets: ResultSet[]
    }

    interface IMrsScheduledTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        status: "SCHEDULED";
        message: string;
    }

    interface IMrsRunningTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        data: MrsTaskStatusUpdate;
        status: "RUNNING";
        message: string;
        progress: number;
    }

    interface IMrsCompletedTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        data: MrsTaskResult;
        status: "COMPLETED";
        message: string;
    }

    interface IMrsCancelledTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        status: "CANCELLED";
        message: string;
    }

    interface IMrsErrorTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        status: "ERROR";
        message: string;
    }

    interface IMrsTimedOutTaskReport<MrsTaskStatusUpdate, MrsTaskResult> {
        status: "TIMEOUT";
        message: string;
    }

    type IMrsTaskReport<MrsTaskStatusUpdate, MrsTaskResult> =
        IMrsRunningTaskReport<MrsTaskStatusUpdate, MrsTaskResult>
        | IMrsCompletedTaskReport<MrsTaskStatusUpdate, MrsTaskResult>
        | IMrsCancelledTaskReport<MrsTaskStatusUpdate, MrsTaskResult>
        | IMrsErrorTaskReport<MrsTaskStatusUpdate, MrsTaskResult>
        | IMrsTimedOutTaskReport<MrsTaskStatusUpdate, MrsTaskResult>;

### 5.21.3 Example (watch)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // update the title of a note with a given id
    const task = await myService.mrsNotes.noteUpdate.start({ noteId: note.id, title: "hello world" });

    // assuming it is a long-running operation, watch for status updates
    for await (const report of task.watch()) {
        if (report.status === "RUNNING") {
            console.log(report.progress);
        } else if (report.status === "ERROR") {
            console.log(report.message);
        }
    }

## 5.22 Task.kill

`kill` is used to kill the underlying Async Task of a REST routine (`FUNCTION` or `PROCEDURE`) and cancel its execution.

### 5.22.1 Reference (kill)

    async function kill(): Promise<void> {
        // ...
    }

### 5.22.2 Example (kill)

    import { MyService } from './myService.mrs.sdk/myService';

    const myService = new MyService();

    // update the title of a note with a given id, kill the task if it takes more than 10 seconds to finish
    const task = await myService.mrsNotes.noteUpdate.start({ noteId: note.id, title: "hello world" }, { timeout: 10000 });

    // assuming it is a long-running operation, kill the task if it takes more than 10 seconds to finish
    for await (const report of task.watch()) {
        if (report.status === "TIMEOUT") {
            await task.kill();
        } else if (report.status === "CANCELLED") {
            console.log(report.message);
        }
    }

# 6 Python Client API Reference

The MRS Python SDK requires Python 3.12+.

## 6.1 REST Resources

The MRS Python SDK exposes a type-safe programmable interface for the MySQL REST Service that unifies, under a set of contextual commands, the available HTTP operations to access and use database objects (schemas, tables, views, functions, etc.) exposed as REST resources.

The following `MRS Resources` exist as part of the MRS Python SDK:

- REST Service
  - REST Schema
    - REST View, Function and Procedure
      - REST Document

MRS resources, as shown above, are grouped under service namespaces.

The following commands can be executed in the scope of a client-side representation of any MRS resource (except REST Documents).

- get_metadata

### 6.1.1 get_metadata

The MRS has a dedicated JSON field where users can store application specific metadata at different levels: service, schema and objects of the schema (such as Views or Functions).

`get_metadata()` is a command that enables users to access the underlying MRS metadata information from client REST objects.

#### 6.1.1.1 Options (get_metadata)

`None`.

#### 6.1.1.2 Return Type (get_metadata)

The metadata information is returned as a JSON-like object (`dict`). If there is no metadata specified for a given MRS resource, an empty JSON object (ultimately, a Python `dict`) is returned.

#### 6.1.1.3 Example (get_metadata)

    from sdk.python import MyService

    my_service = MyService()

    print(await my_service.get_metadata())  # {"title": "My Service"}

    print(await my_service.sakila.get_metadata())  # {"title": "Sakila Sample Database"}

## 6.2 REST Services

In the Python SDK, schemas are grouped under service namespaces.

The following resources can be accessed from a service namespace:

- REST Schemas

The following options are supported when creating a service:

- base_url
- verify_tls_cert

The following commands can be accessed from a service object:

- Properties
  - tls_context
- Methods
  - get_auth_apps
  - authenticate
  - deauthenticate

### 6.2.1 base_url

`base_url` is a service constructor option that allows you to customize the service URL.

    from sdk.python import MyService

    domain = "my_domain"
    port = 8443

    my_service = MyService(base_url=f"https://{domain}:{port}/myService")

By default, `base_url` corresponds to the URL specified when the Python SDK was exported/dumped.

### 6.2.2 verify_tls_cert

`verify_tls_cert` is a service constructor option that allows you to customize the configuration of the TLS/SSL context that is created alongside the service. This option can be used to disable TLS/SSL CA certificate (cert) verification or specify what cert(s) should be loaded during the verification.

TLS/SSL cert(s) verification is enabled by default, in this regard, if `verify_tls_cert` is never set, or set as `True` explicitly, TLS/SSL cert verification is enabled and a set of default “certification authority” (CA) certificates from default locations are loaded (see TLS/SSL default certs).

    # default behavior

    from sdk.python import MyService

    my_service = MyService()
    # print(my_service.tls_context.verify_mode)
    # ------------------------------
    # True

To customize what CA certificates should be loaded, instead of relying on the default behavior, `verify_tls_cert` must be specified as a path-like string (the string can be the path to a CA certificate file, or it can be the path to a folder containing several CA certificates). The file(s) referenced by the specified path-like string are loaded during TLS/SSL cert verification. Certificates should be in PEM format, following an OpenSSL specific layout.

    # customize what CA certificates should be loaded

    from sdk.python import MyService

    my_service = MyService(verify_tls_cert="/path/to/certfile")
    # print(my_service.tls_context.verify_mode)
    # ------------------------------
    # True

Finally, to disable TLS/SSL cert(s) verification, `verify tls cert` must be specified as `False`.

    # disable TLS/SSL cert(s) verification

    from sdk.python import MyService

    my_service = MyService(verify_tls_cert=False)
    # print(my_service.tls_context.verify_mode)
    # ------------------------------
    # False

### 6.2.3 tls_context

`tls_context` is a service-level property that gets the TLS/SSL context configured for the service, which is used when executing HTTPS requests. The TLS/SSL context configuration depends on how `verify_tls_cert` is set when the service is created, see verify_tls_cert.

#### 6.2.3.1 Return Type (tls_context)

An `ssl.SSLContext` instance.

#### 6.2.3.2 Example (tls_context)

    from sdk.python import MyService

    my_service = MyService()

    tls_context = my_service.tls_context

    # print(tls_context.verify_mode)
    # ------------------------------
    # True

### 6.2.4 Service.get_auth_apps

`get_auth_apps()` is a service-level command that enables users to get a list containing the authentication apps and vendor IDs registered for the given service.

#### 6.2.4.1 Options (get_auth_apps)

This command expects no input from the calling application.

#### 6.2.4.2 Return Type (get_auth_apps)

A list of dictionaries. Each element in the list is a 2-key dictionary, where keys are `name` and `vendor_id`.

#### 6.2.4.3 Example (get_auth_apps)

    from sdk.python import MyService

    my_service = MyService()

    auth_apps = await my_service.get_auth_apps()

    # print(auth_apps)
    # ----------------
    # [
    #     {"name": "MRS", "vendor_id": "0x30000000000000000000000000000000"},
    #     {"name": "MySQL", "vendor_id": "0x31000000000000000000000000000000"}
    # ]

### 6.2.5 Service.authenticate

`authenticate` is a service-level command that authenticates a user so he/she can work with restricted MySQL REST Services.

#### 6.2.5.1 Options (authenticate)

REST Service Options for Authentication
Argument Name | Data Type | Required | Default | Notes
--- | --- | --- | --- | ---
app | `str` | Yes | N/A | Name of the authentication application (as specified by the admin).
username | `str` | Yes | N/A | User name
password | `str` | No | `""` | If not provided, the empty string is assumed as the password
vendor_id | `str` | No | `None` | ID of the underlying authentication mechanism. Specifying the vendor ID avoids an additional round-trip to the server

The following authentication app vendors are supported:

- MRS
- MySQL Internal

#### 6.2.5.2 Return Type (authenticate)

This command returns nothing.

#### 6.2.5.3 Raises (authenticate)

`AuthenticationError` if something goes wrong during the authentication workflow.

#### 6.2.5.4 Example (authenticate)

    from sdk.python import MyService

    my_service = MyService()

    # `authenticate` will account for authentication
     await my_service.authenticate(
        app="MySQL",
        username="Lucas",
        password="S3cr3t",
        vendor_id="0x31000000000000000000000000000000"
    )

    # Service is ready and tied database objects can be utilized
    # E.g., calling a function
    res = await my_service.sakila.hello_func.call(name="Rui")
    # print(res) -> Hello, Rui!

When `vendor_id` is not specified, a vendor ID lookup is performed. The vendor ID that matches the given `app` is picked and used down the road. If not a match takes place, an `AuthenticationError` exception is raised.

In the case the vendor ID is not specified, and a nonexisting app is provided, an `AuthenticationError` exception is raised.

Also, in the case the vendor ID is specified alongside a nonexisting app, there will not be a lookup. This means that if, by accident, or not, there is no authentication app from the specified `vendor_id` with the given `app`, an `AuthenticationError` exception is returned to the application.

### 6.2.6 Service.deauthenticate

`deauthenticate` is a service-level command that logs you out from authenticated MySQL REST Services.

#### 6.2.6.1 Options (deauthenticate)

This command expects no input from the calling application.

#### 6.2.6.2 Return Type (deauthenticate)

This command returns nothing.

#### 6.2.6.3 Raises (deauthenticate)

`DeauthenticationError` if no user is currently authenticated.

#### 6.2.6.4 Example (deauthenticate)

    from sdk.python import MyService

    my_service = MyService()

    # Log in - `authenticate` will account for authentication
     await my_service.authenticate(
        app="MySQL",
        username="Lucas",
        password="S3cr3t"
    )

    # Call a function
    res = await my_service.sakila.hello_func.call(name="Oscar")
    # print(res) -> Hello, Oscar!

    # Log out
    await my_service.deauthenticate()

    # Calling the function again - you should get an HTTP 401 (Unauthorized) error
    res = await my_service.sakila.hello_func.call(name="Rui")

    # Log out again - you should get an `ServiceNotAuthenticatedError` exception
    await my_service.deauthenticate()

## 6.3 REST Schemas

In the Python SDK, database objects such as tables and functions are grouped under namespaces that correspond to their schema. Applications can access and use those database objects via the API exposed by each one.

The following REST resources can be accessed from the corresponding schema namespace:

- REST Views
- REST Documents
- REST Functions
- REST Procedures

## 6.4 REST Views

### 6.4.1 View.create

`create` is used to insert a record (a REST document) into the database. The REST document is represented as a typed dictionary object whose fields, or keys, should comply with the interface exposed by the type definition `INew${obj_class_name}` where `${obj_class_name}` is a variable containing a string which is a fully-qualified name composed by the names of the REST Service, REST Schema and REST View themselves.

> To insert multiple documents, see create_many.

#### 6.4.1.1 Options (create)

REST Views Options (create)
Name | Type | Required | Description
--- | --- | --- | ---
data | TypedDict | Yes | Object containing the mapping between column names and values for the record to be inserted

#### 6.4.1.2 Return Type (create)

A REST document data class object representing the record that was inserted. For more details about REST documents, check the REST Documents section.

#### 6.4.1.3 Example (create)

    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    actor: Actor = await my_service.sakila.actor.create(
        data={
            "first_name": "FOO",
            "last_name": "BAR",
        }
    )

    print(actor)
    # IMyServiceSakilaActor(actor_id=35000, first_name='FOO', last_name='BAR', last_update='2024-06-04 10:14:33.000000')

The `actor_id` and `last_update` columns from the `sakila` table on the sample sakila database are automatically generated on each insert, which means they can be omitted.

### 6.4.2 View.create_many

`create_many` is used to insert one or more records (REST documents) into the database. A record is represented as a typed dictionary object whose fields, or keys, should comply with the interface exposed by the type definition `INew${obj_class_name}` where `${obj_class_name}` is a variable which value depends on the service, schema and table names themselves.

> To insert a single record, see create.

#### 6.4.2.1 Options (create_many)

REST Views Options (create_many)
Name | Type | Required | Description
--- | --- | --- | ---
data | Sequence of `TypedDict` - it can be any Python object supporting the iteration protocol, such as lists and tuples | Yes | List of objects containing the mapping between column names and values for the records to be inserted

#### 6.4.2.2 Return Type (create_many)

A list of REST document data class objects representing each record that was inserted. For more details about REST documents, check the REST Documents section.

#### 6.4.2.3 Example (create_many)

    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    actors: list[Actor] = await my_service.sakila.actor.create_many(
        data=[
            {
                "first_name": "FOO",
                "last_name": "BAR",
            },
            {
                "first_name": "OOF",
                "last_name": "RAB",
            }
        ]
    )

    print(actors)
    # [
    #     IMyServiceSakilaActor(
    #         actor_id=35000,
    #         first_name='FOO',
    #         last_name='BAR',
    #         last_update='2024-06-04 10:14:33.000000'
    #     ),
    #     IMyServiceSakilaActor(
    #         actor_id=36000,
    #         first_name='OOF',
    #         last_name='RAB',
    #         last_update='2024-08-04 10:14:33.000000'
    #     )
    # ]

The `actor_id` and `last_update` columns from the `sakila` table on the sample sakila database are automatically generated on each insert, which means they can be omitted.

### 6.4.3 View.find_first

`find_first` is used to query the first REST document (in no specific order) that matches a given optional filter. It returns `None` if no document is found.

> To raise an exception if there are no matches, use find_first_or_throw instead.

> To find multiple REST documents, see find.

#### 6.4.3.1 Options (find_first)

REST Views Options (find_first)
Name | Type | Required | Description
--- | --- | --- | ---
select | dict or list | No | Specifies which properties to include or exclude on the returned document - works as a field filter
where | dict | No | Applies filtering conditions based on specific fields - works as a document filter
skip | int | No | Specifies how many documents to skip before returning one of the matches
order_by | dict | No | Lets you customize the order (`ASC` or `DESC`) in which the documents are returned based on specific fields
cursor | dict | No | Specifies the position of the first item to include in the result set. A cursor bookmarks a location in a result set and must be a column containing unique and sequential values.
read_own_writes | bool | No | Ensures read consistency for a cluster of servers - `False` is used by default

> Cursor-based pagination takes precedence over offset-based pagination, which means that if a cursor is defined, the value of the offset property (`skip`) will be ignored.

#### 6.4.3.2 Return Type (find_first)

If there is a match, a REST document data class object meeting the filter conditions, otherwise `None` is returned. For more details about REST documents, check the REST Documents section.

#### 6.4.3.3 Example (find_first)

Consider the following generic usage snippet of `find_first`:

    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    actor: Optional[Actor] = await self.my_service.sakila.actor.find_first(
        select={"last_name": False},
        where={"first_name": {"like": "%%ED%%"}},
        .
        .
    )

In the following subsections, a small usage example is included for each option.

##### 6.4.3.3.1 Select (find_first)

    # Only include the fields specified (`list` use case)
    select=["last_update"]

    # Only include the fields specified (`dict` use case)
    select={"last_name": True}

    # Include all fields but the specified
    select={"last_name": False}

##### 6.4.3.3.2 Where (find_first)

    # Equality - these two are equivalent
    where={"actor_id": 3}
    where={"actor_id": {"equals": 3}}

    # Difference
    where={"last_name": {"ne": "Pacheco"}}

    # Greater than
    where={"actor_id": {"gt": 3}}

    # Greater than or equal
    where={"actor_id": {"gte": 3}}

    # Lower than
    where={"actor_id": {"lt": 3}}

    # Lower than or equal
    where={"actor_id": {"lte": 3}}

    # Leads to a match when the field is not NULL.
    # In this case, we would get documents where the
    # field `last_update` is not NULL.
    where={"last_updated": {"not": None}}

    # Pattern
    where={"first_name": {"like": "%%ED%%"}}

    # Union of conditions
    where={
        "AND": [
            {"first_name": "PENELOPE"},
            {"actor_id": {"gte": 3}}
        ]
    }

    # Intersection of conditions
    where={
        "OR": [
            {"first_name": "MICHAEL"},
            {"last_name": {"like": "%%AB%%"}}
        ]
    }

##### 6.4.3.3.3 Skip (find_first)

Offset-based pagination is non-inclusive, meaning that if the position marking the offset is a match, it will not be included in the result set.

Suppose the actor table contains a bunch of records as shown in Cursor Example. Then, the `skip` option can be used as follows:

    actor: Optional[Actor] = await self.my_service.sakila.actor.find_first(
        where={"last_name": {"like": "%%HA%%"}}, skip=2
    )

    if actor is None:
        warnings.warn(f"Actor not found")
    else:
        print(actor.actor_id)

The printed actor ID would be `32`.

##### 6.4.3.3.4 Order By (find_first)

    # Descending order
    order_by={"first_name": "DESC"}

    # Ascending order
    order_by={"actor_id": "ASC"}

##### 6.4.3.3.5 Cursor (find_first)

The position bookmarked by the cursor is non-inclusive, meaning that if the bookmarked position is a match, it will not be included in the result set.

Suppose the actor table contains a bunch of records. In the following snippet, only those records where `last_name` matches the pattern `%%HA%%` are shown:

    {
        "actorId": 1,
        "lastName": ...,
        "firstName": ...,
        "lastUpdate": ...,
    },
    ...,
    {
        "actorId": 3,
        "lastName": "CHASE",
        "firstName": "ED",
        "lastUpdate": "2006-02-15 04:34:33.000000",
    },
    ...,
    {
        "actorId": 8,
        "lastName": "JOHANSSON",
        "firstName": "MATTHEW",
        "lastUpdate": "2006-02-15 04:34:33.000000",
    },
    ...,
    {
        "actorId": 32,
        "lastName": "HACKMAN",
        "firstName": "TIM",
        "lastUpdate": "2006-02-15 04:34:33.000000",
    }

The cursor option can be used as follows:

    actor: Optional[Actor] = await self.my_service.sakila.actor.find_first(
        where={"last_name": {"like": "%%HA%%"}},
        cursor={"actor_id": 3},  # cursor is exclusive
    )

    if actor is None:
        warnings.warn(f"Actor not found")
    else:
        print(actor.actor_id)

The printed actor ID would be `8`.

##### 6.4.3.3.6 Read Own Writes (find_first)

    # ON
    read_own_writes=True

    # OFF
    read_own_writes=False

### 6.4.4 View.find_first_or_throw

`find_first_or_throw` is used to retrieve the first REST document that matches a given optional filter in the same way as find_first does. However, if the query does not find a document, it raises a `MrsDocumentNotFoundError` exception.

> To not raise an exception and get `None` if there are no matches, use find_first instead.

> To find multiple REST documents, see find.

#### 6.4.4.1 Options (find_first_or_throw)

`find_first_or_throw` and `find_first` implement the very same options. For more details about these, see Options (find_first).

#### 6.4.4.2 Return Type (find_first_or_throw)

If there is a match, a REST document data class object meeting the filter conditions is returned, otherwise an exception `MrsDocumentNotFoundError` is raised. For more details about REST documents, check the REST Documents section.

#### 6.4.4.3 Example (find_first_or_throw)

Usage is similar to `find_first`, however, now you should account for a possible exception:

    import warnings
    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService, MrsDocumentNotFoundError

    my_service = MyService()

    try:
        actor: Actor = await self.my_service.sakila.actor.find_first_or_throw(
            select={"last_name": False},
            where={"first_name": {"like": "%%ED%%"}},
        )
    except MrsDocumentNotFoundError:
        warnings.warn("Ups, no matches found")

See Example (find_first) for additional usage options.

### 6.4.5 View.find_unique

`find_unique` is used to query a single, uniquely identified REST document by:

- Primary key column(s)
- Unique column(s)

It returns `None` if no document is found.

> To raise an exception if there are no matches, use find_unique_or_throw instead.

#### 6.4.5.1 Options (find_unique)

REST Views Options (find_unique)
Name | Type | Required | Description
--- | --- | --- | ---
select | dict or list | No | Specifies which properties to include or exclude on the returned document - works as a field filter
where | dict | Yes | Applies filtering conditions based on specific fields (must be unique) - works as a document filter
read_own_writes | bool | No | Ensures read consistency for a cluster of servers - `False` is used by default

#### 6.4.5.2 Return Type (find_unique)

If there is a match, a REST document data class object meeting the filter conditions, otherwise `None` is returned. For more details about REST documents, check the REST Documents section.

#### 6.4.5.3 Example (find_unique)

    import warnings
    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    aid = 3
    actor: Actor = await self.my_service.sakila.actor.find_unique(
        where={"actor_id": aid}, select=["last_update"], read_own_writes=False
    )
    if actor is None:
        warnings.warn(f"Actor with id={aid} not found")

    assert actor.actor_id == aid

See Example (find_first) for additional usage options.

### 6.4.6 View.find_unique_or_throw

`find_unique_or_throw` is used to query a single, uniquely identified REST document by:

- Primary key column(s)
- Unique column(s)

If no document was found matching the given filter conditions, `MrsDocumentNotFoundError` is raised.

> To not raise an exception and get `None` if there are no matches, use find_unique instead.

#### 6.4.6.1 Options (find_unique_or_throw)

`find_unique_or_throw` and `find_unique` implement the very same options. For more details about these, see Options (find_unique).

#### 6.4.6.2 Return Type (find_unique_or_throw)

If there is a match, a REST document data class object meeting the filter conditions, otherwise `MrsDocumentNotFoundError` is raised. For more details about REST documents, check the REST Documents section.

#### 6.4.6.3 Example (find_unique_or_throw)

    import warnings
    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService, MrsDocumentNotFoundError

    my_service = MyService()

    aid = 3
    try:
        actor: Actor = await self.my_service.sakila.actor.find_unique_or_throw(
            where={"actor_id": aid}, select=["last_update"], read_own_writes=False
        )
    except MrsDocumentNotFoundError:
        warnings.warn("Ups, no matches found")

See Example (find_first) for additional usage options.

### 6.4.7 View.find

`find` is used to query a subset of REST documents in one or more pages, and optionally, those that match a given filter.

#### 6.4.7.1 Options (find)

REST Views Options (find)
Name | Type | Required | Description
--- | --- | --- | ---
select | dict or list | No | Specifies which properties to include or exclude on the returned document - works as a field filter
where | dict | No | Applies filtering conditions based on specific fields - works as a document filter
skip | int | No | Specifies how many documents to skip before returning one of the matches
order_by | dict | No | Lets you customize the order (`ASC` or `DESC`) in which the documents are returned based on specific fields
cursor | dict | No | Specifies the position of the first item to include in the result set. A cursor bookmarks a location in a result set and must be a column containing unique and sequential values.
read_own_writes | bool | No | Ensures read consistency for a cluster of servers - `False` is used by default
take | int | No | The maximum size of the page.
in order to avoid n + 1 requests, the internal iterator stops after the MySQL Router says there are no more items. Default value is `True` (enabled). |  |  |

> Cursor-based pagination takes precedence over offset-based pagination, which means that if a cursor is defined, the value of the offset property (`skip`) will be ignored.

#### 6.4.7.2 Return Type (find)

A list of objects representing the first page of REST Documents matching the filter. If there are more matching REST Documents, the array contains an additional `has_more` truthy property and a `next()` async function that automatically retrieves the subsequent page of REST Documents.

#### 6.4.7.3 Example (find)

    from sdk.python.my_service import MyService

    my_service = MyService()

    actors = await self.my_service.sakila.countries.find()
    print(actors)
    # [
    #   IMyServiceSakilaCountry(
    #       country_id=1,
    #       last_update=datetime.datetime(2006, 2, 15, 4, 44),
    #       country='Afghanistan',
    #   ),
    #   IMyServiceSakilaCountry(
    #       country_id=2,
    #       last_update=datetime.datetime(2006, 2, 15, 4, 44),
    #       country='Algeria',
    #   ),
    #   ...
    #   IMyServiceSakilaCountry(
    #       country_id=25,
    #       last_update=datetime.datetime(2006, 2, 15, 4, 44),
    #       country='Congo, The Democratic Republic of the',
    #   )
    # ]

    if actors.has_more is True:
        actors = await actors.next()
        print(actors)
        # [
        #   IMyServiceSakilaCountry(
        #       country_id=26,
        #       last_update=datetime.datetime(2006, 2, 15, 4, 44),
        #       country='Czech Republic',
        #   ),
        #   IMyServiceSakilaCountry(
        #       country_id=27,
        #       last_update=datetime.datetime(2006, 2, 15, 4, 44),
        #       country='Dominican Republic',
        #   ),
        #   ...
        #   IMyServiceSakilaCountry(
        #       country_id=50,
        #       last_update=datetime.datetime(2006, 2, 15, 4, 44),
        #       country='Japan',
        #   )

> Due the way attributes work in Python, for the MyPy type checker to be able to correctly narrow the types returned by `next()`, `has_more` must be explicitly checked against `True`.

See Example (find_first) for additional usage options.

### 6.4.8 View.delete

`delete` is used to delete a single, uniquely identified REST document by:

- Primary key column(s)
- Unique column(s)

> To delete multiple documents, see delete_many.

#### 6.4.8.1 Options (delete)

REST Views Options (delete)
Name | Type | Required | Description
--- | --- | --- | ---
where | dict | Yes | Applies filtering conditions based on specific fields (must be unique) - works as a document filter
read_own_writes | bool | No | Ensures read consistency for a cluster of servers - `False` is used by default

#### 6.4.8.2 Return Type (delete)

`True` if the document was deleted successfully or `False` otherwise.

#### 6.4.8.3 Example (delete)

    import warnings
    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    aid = 3
    ans: bool = await self.my_service.sakila.actor.delete(
        where={"actor_id": aid}
    )
    if ans is False:
        warnings.warn(f"Actor not deleted - actor_id={aid} not found")
    else:
        print(f"Actor document with ID={aid} was deleted")

### 6.4.9 View.delete_many

`delete_many` is used to delete all REST documents that match a given filter. To delete a single document, see delete.

#### 6.4.9.1 Options (delete_many)

REST Views Options (delete_many)
Name | Type | Required | Description
--- | --- | --- | ---
where | dict | Yes | Applies filtering conditions based on specific fields - works as a document filter
read_own_writes | bool | No | Ensures read consistency for a cluster of servers - `False` is used by default

#### 6.4.9.2 Return Type (delete_many)

An integer indicating the number of deleted documents.

#### 6.4.9.3 Example (delete_many)

Suppose the actor table contains a bunch of records as shown in Cursor Example.

    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    num_items_removed = await self.my_service.sakila.actor.delete_many(
        where={"last_name": {"like": "%%HA%%"}}
    )
    print(num_items_removed)
    # 3

### 6.4.10 View.update

`update` is used to update a REST document with a given identifier or primary key.

> To update multiple documents, see update_many.

#### 6.4.10.1 Options (update)

REST Views Options (update)
Name | Type | Required | Description
--- | --- | --- | ---
data | TypedDict | Yes | Set of fields and corresponding values to update. The identifier or primary key must be included

#### 6.4.10.2 Return Type (update)

A REST document data class object representing the up-to-date record. For more details about REST documents, check the REST Documents section.

#### 6.4.10.3 Example (update)

    from datetime import datetime
    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    # The `actor_id` and `last_update` columns from the `sakila` table on the
    # sample [sakila database](https://dev.mysql.com/doc/sakila/en/) are
    # automatically generated on each insert, which means they can be omitted.
    actor: Actor = await self.my_service.sakila.actor.create(
        data={"first_name": "Foo", "last_name": "Bar", "actor_id": 345}
    )

    actor_updated: Actor = await self.my_service.sakila.actor.update(
        data={
                "actor_id": cast(int, actor.actor_id),
                "first_name": "Rodolfo",
                "last_name": "Smith",
                "last_update": str(datetime.now()),
            }
        )

    assert actor_updated.first_name == "Rodolfo"
    assert actor_updated.last_name == "Smith"
    assert actor.actor_id == actor_updated.actor_id

### 6.4.11 View.update_many

`update_many` is used to update all REST documents with matching identifiers or primary keys.

> To update a single document, see update.

#### 6.4.11.1 Options (update_many)

REST Views Options (update_many)
Name | Type | Required | Description
--- | --- | --- | ---
data | list of `TypedDict` | Yes | A list of set of fields and corresponding values to update. The identifier or primary key must be included for each “set of fields” (document)

#### 6.4.11.2 Return Type (update_many)

A list of REST document data class objects representing the up-to-date records. For more details about REST documents, check the REST Documents section.

#### 6.4.11.3 Example (update_many)

    from datetime import datetime
    from sdk.python.my_service import IMyServiceSakilaActor as Actor, MyService

    my_service = MyService()

    # The `actor_id` and `last_update` columns from the `sakila` table on the
    # sample [sakila database](https://dev.mysql.com/doc/sakila/en/) are
    # automatically generated on each insert, which means they can be omitted.
    actors: Actor = await self.my_service.sakila.actor.create(
        data=[
            {"first_name": "Foo", "last_name": "Bar", "actor_id": 345},
            {"first_name": "Bruh", "last_name": "Baz", "actor_id": 346},
        ]
    )

    actors_updated: Actor = await self.my_service.sakila.actor.update(
        data=[
            {
                "actor_id": cast(int, actors[0].actor_id),
                "first_name": "Rodolfo",
                "last_name": "Smith",
                "last_update": str(datetime.now()),
            },
            {
                "actor_id": cast(int, actors[1].actor_id),
                "first_name": "Ma",
                "last_name": "Yeung",
                "last_update": str(datetime.now()),
            },

        ]
        )

    assert actors_updated[0].first_name == "Rodolfo"
    assert actors_updated[0].last_name == "Smith"

    assert actors_updated[1].first_name == "Ma"
    assert actors_updated[1].last_name == "Yeung"

## 6.5 REST Documents

A REST document behaves like a Python data class instance, and implements an extended interface which includes the `update` and `delete` methods.

> Python data classes defining REST documents are public elements of the Service module, however, we advise you to not produce (instantiate) REST documents directly. Instead, we recommend doing so indirectly; by calling specific Python SDK commands such as `find*()` or `create*()`. See REST Views to know more about these commands.

### 6.5.1 Document.update

`update` updates the REST document represented by the data class instance.

#### 6.5.1.1 Options (update - document)

No options are implemented because the data required to complete the operation is assumed to be already included in the data class instance itself.

#### 6.5.1.2 Return Type (update - document)

`None`.

#### 6.5.1.3 Example (update - document)

    import asyncio
    from datetime import datetime
    from typing import Optional
    from sdk.python.my_service import (
        IMyServiceSakilaActor as Actor,
        MyService,
        MrsDocumentNotFoundError,
    )

    async def get_actor_document_by_id(service: MyService, doc_id: int) -> Actor:
        try:
            actor: Actor = await service.sakila.actor.find_first_or_throw(
                    where={"actor_id": doc_id}
                )
        except MrsDocumentNotFoundError:
            raise MrsDocumentNotFoundError(msg=f"No actor document exists matching actor_id={doc_id}")
        return actor

    async def main() -> None:
        # Create service
        my_service = MyService()

        # Get a document
        doc_id = 3
        actor = await get_actor_document_by_id(service=my_service, doc_id=doc_id)
        print("Before:", actor)

        # Modify the data class instance representing a REST document
        actor.first_name = "DESIRE"
        actor.last_name = "LEE"
        actor.last_update = str(datetime.now())

        # Commit an update
        await actor.update()

        # Peak the REST document to see if it was updated accordingly
        actor_after = await get_actor_document_by_id(service=my_service, doc_id=doc_id)
        print("After:", actor_after)

        # Before: IMyServiceSakilaActor(last_name='CHASE', last_update='2023-04-13 15:11:22.000000', first_name='ED', actor_id=3)
        # After: IMyServiceSakilaActor(last_name='LEE', last_update='2025-01-09 13:07:50.000000', first_name='DESIRE', actor_id=3)

    if __name__ == "__main__":
        asyncio.run(main())

### 6.5.2 Document.delete

`delete` deletes the resource represented by the data class instance.

#### 6.5.2.1 Options (delete - document)

No options are implemented because the data required to complete the operation is assumed to be already included in the data class instance itself.

#### 6.5.2.2 Return Type (delete - document)

`True` if the document was deleted successfully or `False` otherwise.

#### 6.5.2.3 Example (delete - document)

    import asyncio
    from typing import Optional, cast
    from sdk.python.my_service import (
        IMyServiceSakilaActor as Actor,
        MyService,
    )

    async def main():
        # Create service
        my_service = MyService()

        # Create a document
        #
        # The `actor_id` and `last_update` columns from the `sakila`
        # table on the sample [sakila database](https://dev.mysql.com/doc/sakila/en/)
        # are automatically generated on each insert, which means they can be omitted.
        actor = await my_service.sakila.actor.create(
            {"first_name": "GRACO", "last_name": "WALKER"}
        )
        print(actor)

        # Commit a delete
        await actor.delete()

        actor_after: Optional[Actor] = await my_service.sakila.actor.find_first(
            where={"actor_id": cast(int, actor.actor_id)}
        )
        print("deleted?", actor_after is None)

        # IMyServiceSakilaActor(last_name='WALKER', last_update='2025-01-09 13:31:16.000000', first_name='GRACO', actor_id=37171)
        # deleted? True

    if __name__ == "__main__":
        asyncio.run(main())

## 6.6 REST Routines

### 6.6.1 Function.call

`call` is used to execute a REST routine (`FUNCTION` or `PROCEDURE`). In the case of a `FUNCTION`, the set of parameters (and corresponding values) as specified by the database routine are provided as corresponding keyword arguments. If the REST routine has an associated Async Task, the first parameter is, instead, a positional argument that uses a Python `dict` to specify additional task-specific execution options.

> For the sake of avoiding conflict with keyword argument names, an arbitrary number of positional arguments are enabled. The recommended treat is to pass only one options dictionary, as the first positional argument. If you, intentionally or not, pass more than one positional argument, the option value from the later dictionary in the sequence takes precedence.

#### 6.6.1.1 Options (call)

REST Function/Procedure Options (call)
Option Name | Data Type | Required | Default | Notes
--- | --- | --- | --- | ---
refresh_rate | `float` | No | `2.0` | Rate at which the underlying implementation checks for status updates of the execution. Value in seconds. An exception is raised if `refresh_rate` is lower than 0.5 seconds. (available only if the routine has an associated Async Task)
progress | `Callable[[IMrsRunningTaskReport], Awaitable[None]]` | No | `None` | Callback function that gets executed (with the details provided by the status update) while the status of the execution remains in `RUNNING` state. By default, no progress is carried on.
timeout | `float` | No | `None` | Maximum time to wait for the execution to complete. If this threshold is reached, the ongoing task is killed and `MrsTaskTimeOutError` exception is raised. By default, no timeout is enforced. (available only if the routine has an associated Async Task)

#### 6.6.1.2 Return Type (call)

The Python data type returned by `<func_name>(...)` depends on the data type returned by the MySQL function.

> For instance, the Python data type `int` must be expected for MySQL functions declared to return `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT` and `BIGINT`.

#### 6.6.1.3 Exceptions (call)

For REST routines with an associated asynchronous task, `call` can raise exceptions as follows:

Exception | Notes
--- | ---
`MrsTaskExecutionError` | When the status update reports back an `ERROR` event.
`MrsTaskExecutionCancelledError` | When the status update reports back a `CANCELLED` event.
`MrsTaskTimeOutError` | When the specified `timeout` threshold is reached.

#### 6.6.1.4 Example (call)

    from sdk.python import MyService

    my_service = MyService()

    res = await my_service.sakila.hello_func.call(name="Rui")
    # print(res) -> Hello, Rui!

    res = await my_service.sakila.sum_func.call(a=3, b=2)
    # print(res) -> 5

    res = await my_service.sakila.my_birthday_func.call()
    # print(res) -> 2024-07-18 00:00:00

where `hello_func`, `sum_func` and `my_birthday_func` are:

    -- one input
    mysql> CREATE FUNCTION hello_func (name CHAR(20))
    > RETURNS CHAR(50) DETERMINISTIC
    > RETURN CONCAT('Hello, ', name, '!');

    -- many input
    mysql> CREATE FUNCTION sum_func (a INT, b INT)
    > RETURNS INT DETERMINISTIC
    > RETURN a + b;

    -- no input
    mysql> CREATE FUNCTION my_birthday_func ()
    > RETURNS DATETIME DETERMINISTIC
    > RETURN CURDATE();

### 6.6.2 Function.start

`start()` is used to start a REST routine (`FUNCTION` or `PROCEDURE`), with an associated Async Task, without waiting for it to finish.

#### 6.6.2.1 Options (start)

`start()` accepts the same set of options as `call()`, see Function.call for more details.

#### 6.6.2.2 Return Type (start)

A Task instance.

#### 6.6.2.3 Example (start)

    from sdk.python import MyService

    my_service = MyService()

    task = await my_service.sakila.delayed_hello_func.start({"refresh_rate": 5.0}, name="Rui")

where `delayed_hello_func` is:

    mysql> DELIMITER $$
    mysql> CREATE FUNCTION delayed_hello_func (name CHAR(20))
    > RETURNS CHAR(50) DETERMINISTIC
    > SQL SECURITY INVOKER
    > BEGIN
    >   DO SLEEP(5);
    >   RETURN CONCAT('Hello, ', name, '!');
    > END $$

### 6.6.3 Procedure.call

`call` is used to execute a REST routine (`FUNCTION` or `PROCEDURE`). In the case of a `PROCEDURE`, the set of `IN`/`INOUT` parameters (and corresponding values) as specified by the database routine are provided as corresponding keyword arguments. If the REST routine has an associated Async Task, the first parameter is, instead, a positional argument that uses a Python `dict` to specify additional task-specific execution options.

> For the sake of avoiding conflict with keyword argument names, an arbitrary number of positional arguments are enabled. The recommended treat is to pass only one options dictionary, as the first positional argument. If you, intentionally or not, pass more than one positional argument, the option value from the later dictionary in the sequence takes precedence

#### 6.6.3.1 Options (call)

Input parameters aren’t mandatory, meaning you are free to not provide them.

In case of being provided, input parameters can also be assigned a null value when calling the procedure, in other words, you can set any parameters to `None`.

As for additional options, see Function.call for more details.

#### 6.6.3.2 Return Type (call)

A data class object representing a REST result set. This object includes the following attributes:

- `out_parameters`: Dictionary with fields for each `OUT`/`INOUT` parameter declared as part of the MySQL procedure that produces an actual value. If a parameter is not used to return a value, the field will not be present in the dictionary.

- `result_sets`: List of result set types generated when executing one or more SELECT statements as part of the procedure body. Each result set type can include one or more items.

#### 6.6.3.3 Example (call)

Consider the following dummy procedures:

    -- Assuming the database `mrs_tests` exists
    -- You can use the MySQL Client console to run this script

    DELIMITER //

    CREATE PROCEDURE mrs_tests.mirror_proc (INOUT channel CHAR(4))
    BEGIN
        SELECT REVERSE(channel) INTO channel;
    END//

    CREATE PROCEDURE mrs_tests.twice_proc (IN number INT, OUT number_twice INT)
    BEGIN
        SELECT number*2 INTO number_twice;
    END//

    CREATE PROCEDURE mrs_tests.sample_proc(
        IN arg1 CHAR(5), INOUT arg2 CHAR(5), OUT arg3 FLOAT
    )
    BEGIN
        SELECT "foo" as name, 42 as age;
        SELECT "bar" as something;
    END//

    DELIMITER ;

Use command `call()` to call a REST procedure in the Python SDK.

    from sdk.python import MyService

    my_service = MyService()

    procedure_result = await my_service.mrs_tests.mirror_proc.call(channel="roma")
    print(procedure_result)
    # IMrsProcedureResponse(
    #     result_sets=[],
    #     out_parameters={"channel": "amor"}
    # )

    procedure_result = await my_service.mrs_tests.twice_proc.call(number=13)
    print(procedure_result)
    # IMrsProcedureResponse(
    #     result_sets=[],
    #     out_parameters={"number_twice": 26}
    # )

    # Note how `arg1` is not provided, and `arg2` is set to null.
    procedure_result = await my_service.mrs_tests.sample_proc.call(arg2=None)
    print(procedure_result)
    # IMrsProcedureResponse(
    #     result_sets=[
    #         MrsProcedureResultSet(
    #             type="items0",
    #             items=[{"name": "foo", "age": 42}],
    #         ),
    #         MrsProcedureResultSet(
    #             type="items1",
    #             items=[{"something": "bar"}],
    #         ),
    #     ],
    #     out_parameters={'arg2': None, 'arg3': None}
    # )

The first two procedures do not generate result sets, however the third one does. By omission, result sets are untyped meaning generic type names are used for the result sets.

If you want a typed result set, meaning you wish to specify a type, you can do so at the MRS procedure level via the MySQL Shell:

    CREATE OR REPLACE REST PROCEDURE /sampleProc
        ON SERVICE /myService SCHEMA /mrsTests
        AS mrs_tests.sample_proc
        RESULT IMyServiceMrsTestsSampleProcResultSet1 {
            name: name @DATATYPE("CHAR(3)"),
            age: age @DATATYPE("TINYINT")
        }
        RESULT IMyServiceMrsTestsSampleProcResultSet2 {
            something: something @DATATYPE("CHAR(3)")
        };

Calling the REST procedure again from the Python SDK leads to:

    from sdk.python import MyService

    my_service = MyService()

    # Note how `arg1` is not provided, and `arg2` is set to null.
    procedure_result = await my_service.mrs_tests.sample_proc.call(arg2=None)
    # print(procedure_result.result_sets)
    # [
    #     MrsProcedureResultSet(
    #         type="IMyServiceMrsTestsSampleProcResultSet1",
    #         items=[{"name": "foo", "age": 42}],
    #     ),
    #     MrsProcedureResultSet(
    #         type="IMyServiceMrsTestsSampleProcResultSet2",
    #         items=[{"something": "bar"}],
    #     ),
    # ],

### 6.6.4 Procedure.start

See Function.start for more details.

#### 6.6.4.1 Options (start)

`start()` accepts the same set of options as `call()`, see Procedure.call for more details.

#### 6.6.4.2 Return Type (start)

A Task instance.

#### 6.6.4.3 Example (start)

    from sdk.python import MyService

    my_service = MyService()

    task = await my_service.sakila.delayed_hello_proc.start({"refresh_rate": 5.0}, name="Rui")

where `delayed_hello_proc` is:

    mysql> DELIMITER $$
    mysql> CREATE FUNCTION delayed_hello_proc (name CHAR(20), out salute CHAR(40))
    > RETURNS CHAR(50) DETERMINISTIC
    > SQL SECURITY INVOKER
    > BEGIN
    >   DO SLEEP(5);
    >   SELECT CONCAT('Hello, ', name, '!') INTO salute;
    > END $$

## 6.7 Async Tasks

Asynchronous Tasks are an MRS construct used to manage the life-cycle of a long-running procedure which clients can poll to monitor for status updates. From the client-standpoint, a Task can produce the following type of events (status updates):

- `SCHEDULED` starting the routine schedules a new task
- `RUNNING` progress status updates whilst the procedure is running
- `COMPLETE` result produced by the routine after it finishes
- `TIMEOUT` if the routine does not produce a result before a given timeout
- `ERROR` runtime error whilst executing the routine
- `CANCELLED` when the associated asynchronous task is killed before the routine finishes

### 6.7.1 Task.watch

`watch` is used to monitor the status of a REST routine (`FUNCTION` or `PROCEDURE`) with an associated Async Task.

#### 6.7.1.1 Return Type (watch)

An Asynchronous Generator instance which produces status update reports with details about the execution context of the REST routine.

#### 6.7.1.2 Example (watch)

    task = await my_service.my_db.delayed_hello_func.start({"refresh_rate": 3.0}, name="Rui")

    async for report in task.watch():
        if report.status == "RUNNING":
            print(report.progress)
        elif report.status === "ERROR":
            print(report.message)

### 6.7.2 Task.kill

`kill` is used to kill the underlying Async Task of a REST routine (`FUNCTION` or `PROCEDURE`) and cancel its execution.

#### 6.7.2.1 Example (kill)

    task = await my_service.my_db.delayed_hello_func.start({ "timeout": 4 }, name="Rui")

    async for report in task.watch():
        if report.status == "TIMEOUT":
            await task.kill()
        elif report.status === "CANCELLED":
            print(report.message)

# 7 Checking for NULL Column Values

MySQL supports `NOT NULL` constraints which ensure that the value in a given column cannot be NULL. By omission though, a column can hold `NULL` values. With the MySQL REST Service, records containing columns with NULL values can be included in or excluded from the result set using the `$null` or `$notnull` operators.

The TypeScript MRS SDK provides a special syntax for filtering records in a result set by a given field when it contains (or not) a `NULL` value. With a setup using the Sakila Sample Database where the schema is available under a REST service called `myService`, filtering records by `NULL` column values can be done as follows:

    myService.sakila.address.find({ select: ["address", "address2"], where: { address2: null } })
    [
        {
          "address": "47 MySakila Drive",
          "address2": null,
        },
        {
          "address": "28 MySQL Boulevard",
          "address2": null,
        },
        {
          "address": "23 Workhaven Lane",
          "address2": null,
        },
        {
          "address": "1411 Lillydale Drive",
          "address2": null,
        }
    ]

In the same way, filtering records where a given column does not contain `NULL` can be done as follows:

    myService.sakila.actor.findFirst({ select: ["address", "address2"], where: { address2: { not: null } } })
    {
      "address": "1913 Hanoi Way",
      "address2": "",
    }

Attempting to apply such a filter to a field that maps to a column with a `NOT NULL` constraint should yield a TypeScript compilation error:

    myService.sakila.actor.findFirst({ where: { address: null } })

    Type 'null' is not assignable to type 'string | DataFilterField<IMyServiceSakilaAddressParams, string | undefined> | ComparisonOpExpr<string | undefined>[] | undefined'.

# 8 Working with Data Types

MySQL supports and extensive list of data types that cannot be directly mapped development language native datatypes in many cases. This section discusses how to handles those MySQL datatypes in the Client SDK. The conversion rules apply consistently across REST View fields, REST Procedure input and output parameters and REST function input parameters and results.

## 8.1 Spatial Data Types

MySQL supports an extended SQL environment, based on the conventions established by the OpenGIS Geometry Model, that enables a set of spatial column data types to hold geometry values. Some of them hold single values:

- `POINT`
- `LINESTRING`
- `POLYGON`

On the other hand, there are spatial data types that are meant to hold collections of geometry values:

- `MULTIPOINT`
- `MULTILINESTRING`
- `MULTIPOLYGON`
- `GEOMETRYCOLLECTION`

`GEOMETRYCOLLECTION` can store a collection of objects of any type. The other collection types (`MULTIPOINT`, `MULTILINESTRING`, and `MULTIPOLYGON`) restrict collection members to those having a particular geometry type.

Additionally, there is a `GEOMETRY` data type that is meant to hold any kind of value for the data types described above.

### 8.1.1 TypeScript SDK

MySQL Rest Service (MRS) TypeScript SDK expects a GeoJSON-like object for representing, operating on, or manipulating spatial data.

For instance, with a setup using the Sakila Sample Database where the schema is available under a REST service called `myService`, when inserting records into the `address` table, you can use a GeoJSON-like object to specify the value for the `location` column, which has a generic `GEOMETRY` data type, as described in the following sections.

Upstream Commands

#### 8.1.1.1 Create

    // GeoJSON
    myService.sakila.address.create({ data: {
      location: {
        type: "Point",
        coordinates: [11.11, 12.22]
      }
    }})

#### 8.1.1.2 Update

The same convention should also apply when updating records on the same table.

    // GeoJSON
    myService.sakila.address.update({
      where: {
        address_id: 1
      }
      data: {
        location: {
          type: "Point",
          coordinates: [11.11, 12.22]
        }
      }
    })

    myService.sakila.address.updateMany({
      where: [{
        address_id: 1
      }, {
        address_id: 2
      }],
      data: {
        location: {
          type: "Point",
          coordinates: [11.11, 12.22]
        }
      }
    })

#### 8.1.1.3 Types Mismatch

If the column has a narrow data type such as `POINT`, instead of the more generic `GEOMETRY`, specifying an incompatible type on the client-side, should yield a compilation error. For example, assuming a table `mrs_tests`.`spatial_tests` created as follows:

    CREATE DATABASE IF NOT EXISTS mrs_tests;
    CREATE TABLE IF NOT EXISTS mrs_tests.spatial_tests (id INT AUTO_INCREMENT NOT NULL, ls LINESTRING, PRIMARY KEY (id));

With the table (and corresponding schema) available from the same `myService` REST service, trying to insert a `POINT` does not work, because the column only accepts a `LINESTRING`.

    myService.mrsTests.spatialTests.create({
      data: {
        ls: {
          type: "Point",
          coordinates: [0, 0]
        }
      }
    })

A command like the one above yields a compilation error.

> Type ‘Point’ is not assignable to type ‘LineString’.

In the same way, trying to insert or update multiple values for a single field when the column data type only allows a single value, or vice-versa, should also yield a compilation error. For, example, assuming the `mrs_tests.spatial_tests` table was created as follows:

    CREATE TABLE IF NOT EXISTS mrs_tests.spatial_tests (id INT AUTO_INCREMENT NOT NULL, ls GEOMETRYCOLLECTION, PRIMARY KEY (id));

Trying to insert a `POINT` does not work, because the column only accepts either a `MULTIPOINT`, a `MULTILINESTRING` or a `MULTIPOLYGON`.

    myService.mrsTests.spatialTests.create({
      data: {
        ls: {
          type: "Point",
          coordinates: [0, 0]
        }
      }
    })

In this case, the command yields the following compilation error:

> Type ‘Point’ is not assignable to type ‘MultiPoint | MultiLineString | MultiPolygon’.

### 8.1.2 Python SDK

MySQL Rest Service (MRS) Python SDK supports one format for representing, operating on, or manipulating spatial data:

- GeoJSON

This format can be used when inserting or updating (upstream operations) a record that contains a field matching a column of a Spatial data type. Symmetrically, for downstream operations such as finding records having spatial fields, such fields are specified as GeoJSON types.

For instance, with a setup using the Sakila Sample Database where the schema is available under a REST service called `myService`, when inserting records into the `address` table, you can work with the value for the `location` column, which has a generic `GEOMETRY` data type, as described in the following sections.

Upstream Commands

#### 8.1.2.1 Create

Inserting a record into the address table in the Sakila sample database.

    from sdk.python import MyService, IMyServiceSakilaAddress as Address

    my_service = MyService()

    address: Address = await myService.sakila.address.create(
        {
            "location": {
                "type": "Point",
                "coordinates": [11.11, 12.22],
            }
        }
    )

#### 8.1.2.2 Update

Updating a record from the address table in the Sakila sample database.

    from sdk.python import MyService, IMyServiceSakilaAddress as Address

    my_service = MyService()

    address: Address = await myService.sakila.address.update(
        data={
            "address_id": 1,
            "location": {
                "type": "Point",
                "coordinates": [11.11, 12.22],
            }
        }
    )

Downstream Commands

#### 8.1.2.3 Find

Finding a record from the address table in the Sakila sample database.

    from sdk.python import MyService, IMyServiceSakilaAddress as Address, MrsDocumentNotFoundError

    my_service = MyService()

    doc_id = 1
    try:
        address: Address = await myService.sakila.address.find_first_or_throw(
            where={"address_id": doc_id}
        )
    except MrsDocumentNotFoundError:
        raise MrsDocumentNotFoundError(msg=f"No address document exists matching actor_id={doc_id}")

    print(address.location)
    # {"type": "Point", "coordinates": [11.11, 12.22]}

#### 8.1.2.4 Types Mismatch

If the column has a narrow data type such as `POINT`, instead of the more generic `GEOMETRY`, specifying an incompatible type on the client-side, should yield a mypy (typing system) error. For example, assuming a table `mrs_tests.spatial_tests` created as follows:

    CREATE DATABASE IF NOT EXISTS mrs_tests;
    CREATE TABLE IF NOT EXISTS mrs_tests.spatial_tests (id INT AUTO_INCREMENT NOT NULL, ls LINESTRING, PRIMARY KEY (id));

Trying to insert a `Point` triggers a typing error, because the column only accepts a `LineString`.

    from sdk.python.my_service import IMyServiceMrsTestsSpatialTests as SpatialTests

    my_doc: SpatialTests = await myService.mrs_tests.spatial_tests.update(
        data={
            "id": 1,
            "ls": {
                "type": "Point",
                "coordinates": [0, 0],
            }
        }
    )

> Type `Point` is not assignable to type `LineString`.

In the same way, trying to insert or update multiple values for a single field when the column data type only allows a single value, or vice-versa, should also yield a typing error. For example, assuming the table `mrs_tests.spatial_tests` was, instead, created as follows:

    CREATE TABLE IF NOT EXISTS mrs_tests.spatial_tests (id INT AUTO_INCREMENT NOT NULL, ls GEOMETRYCOLLECTION, PRIMARY KEY (id));

Trying to insert a `Point` triggers a typing error, because the column only accepts either a `MultiPoint`, a `MultiLineString` or a `MultiPolygon`.

    from sdk.python.my_service import IMyServiceMrsTestsSpatialTests as SpatialTests

    my_doc: SpatialTests = await myService.mrs_tests.spatial_tests.create(
        {
            "ls": {
                "type": "Point",
                "coordinates": [0, 0],
            }
        }
    )

> Type `Point` is not assignable to type `MultiPoint`, `MultiLineString` or `MultiPolygon`.

## 8.2 Working with Date and Time Data Types

MySQL supports date and time data types for representing temporal values:

- TIMESTAMP - contain both date and time parts.
- DATETIME - contain both date and time parts.
- DATE - contain date part but no time part.
- TIME - contain time but not date part.
- YEAR - represent year values.

Visit the official documentation to know more about these MySQL data types.

### 8.2.1 Python SDK

MySQL Rest Service (MRS) Python SDK utilizes the following client-side data types for representing, operating on, or manipulating MySQL date and time data:

MRS Python SDK data type mapping
MySQL data type | MRS Python SDK data type
--- | ---
TIMESTAMP | `datetime.datetime`
DATETIME | `datetime.datetime`
DATE | `datetime.date`
TIME | `datetime.timedelta`
YEAR | `int`

The MRS Python SDK data types shall be used when inserting or updating (upstream operations) a record that contains a field matching a column of a date and time data type. Symmetrically, for downstream operations such as finding records having date and time fields, such fields are specified with MRS Python SDK data types according to the above equivalences.

In the following sections, fictional but relevant examples are presented to showcase the usage of MySQL Date and Time data types via the MRS Python SDK.

> The examples assume a sample database named `mrs_tests` exists.

#### 8.2.1.1 Example - View

Consider the following sample table:

    /*
    Sample table including a column for each date and time data type.
    */
    DROP TABLE IF EXISTS mrs_tests.table_date_and_time;

    CREATE TABLE mrs_tests.table_date_and_time (
        idx SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
        ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        dt DATETIME(6),
        d DATE,
        t TIME(6),
        y YEAR,
        PRIMARY KEY  (idx)
    );

    INSERT INTO  mrs_tests.table_date_and_time (dt, dt_shorter, d, t, y) VALUES
        ("2023-07-30 14:59:01", "2023-07-30 14:59:01", "1987-12-09", "119:10:0.100023", 1999),
        ("2025-02-27 09:41:25.000678", "2025-02-27 09:41:25.000678", "2010-01-01", "099:35:0.60003", 2005);

After you have added the corresponding schema (`mrs_tests`) and view (`table_date_and_time`) objects to the MRS service (let’s say `my_service`), you can start using the MRS Python SDK.

Upstream Commands

##### 8.2.1.1.1 Create

Inserting a record into the `table_date_and_time` table in the `mrs_tests` sample database.

    import datetime
    from sdk.python import MyService

    my_service = MyService()

    doc = await my_service.mrs_tests.table_date_and_time.create(
        {
            "dt": datetime.datetime.now(),
            "d": datetime.date(2020, 10, 20),
            "t": datetime.timedelta(days=31, microseconds=202023),
            "y": 1976,
        }
    )

> If you wanted field `d` to be `NULL`, for instance, you would have to use `None` instead of `datetime.date(...)`.

##### 8.2.1.1.2 Update

Updating a record from the `table_date_and_time` table in the `mrs_tests` sample database.

    import datetime
    from sdk.python import MyService

    my_service = MyService()

    doc = await my_service.mrs_tests.table_date_and_time.update(
        data={
            "idx": 1,
            "t": datetime.timedelta(days=4, hours=4, minutes=1, seconds=1),
        }
    )

Downstream Commands

##### 8.2.1.1.3 Find

Finding a record from the `table_date_and_time` table in the `mrs_tests` sample database.

    import datetime
    from sdk.python import MyService

    my_service = MyService()

    doc = await my_service.mrs_tests.table_date_and_time.find_first(
        where={
            "AND": [
                {
                    "dt": {
                        "lt": datetime.datetime.fromisoformat(
                            "2023-07-30 15:59:01"
                        )
                    }
                },
                {"d": {"gte": datetime.date.fromisoformat("1987-12-09")}},
            ]
        }
    )

#### 8.2.1.2 Example - Function

Consider the following sample function:

    /*
    Sample functions using date and time data types.
    */
    DROP FUNCTION IF EXISTS mrs_tests.func_date_and_time_ts;
    CREATE FUNCTION mrs_tests.func_date_and_time_ts (ts TIMESTAMP(4))
        RETURNS TIMESTAMP(4) DETERMINISTIC
        RETURN TIMESTAMPADD(MONTH, 1, ts);

After you have added the corresponding schema (`mrs_tests`) and function (`func_date_and_time_ts`) objects to the MRS service (let’s say `my_service`), you can start using the MRS Python SDK.

Calling the `func_date_and_time_ts` in the `mrs_tests` sample database:

    import datetime
    from sdk.python import MyService

    my_service = MyService()

    # `ts` stands for timestamp
    value = await self.my_service.mrs_tests.func_date_and_time_ts(ts=datetime.datetime.now())

#### 8.2.1.3 Types Mismatch

If an unexpected data type is specified on the client-side for a certain field (column), the Python environment via mypy should yield a typing error.

## 8.3 Working with Vector Data Types

MySQL supports the VECTOR data type for representing a structure that can hold up to a specified number of entries `N`, where each entry is a 4-byte (single-precision) floating-point value.

Visit the official documentation to know more about the MySQL VECTOR type.

### 8.3.1 Python SDK

#### 8.3.1.1 Client-Side Representation

MySQL Rest Service (MRS) Python SDK utilizes the following client-side data type for representing, operating on, or manipulating MySQL vector data:

Client-Side Representation of Vector Datatype
MySQL data type | MRS Python SDK data type
--- | ---
VECTOR | `list` of `floats`

The MRS Python SDK data type shall be used when inserting or updating (upstream operations) a record that contains a field matching a column of a vector data type. Symmetrically, for downstream operations such as finding records having vector fields, such fields are specified with MRS Python SDK data types according to the above mapping.

When a vector column has a `NULL` value, the Python SDK uses `None` on the client side to represent it.

#### 8.3.1.2 Out of Bounds

As specified for the MySQL Vector data type, each entry must be a 4-byte (single-precision) floating-point value. Python lists can store a wider set of floating point values, in this regard, nothing stops the application from specifying entries that are out of bounds, such as a double-precision values.

This event may happen unintentionally, or not, however, in any case, the client does not carry on a verification, in other words, entries are sent to the server as they are, and the client lets the server handle it (an error should be expected).

#### 8.3.1.3 Examples

In the following sections, fictional but relevant examples are presented to showcase the usage of the MySQL Vector data type via the MRS Python SDK.

> The examples assume a sample database named `mrs_tests` exists.

Consider the following sample table:

    /*
    Sample table including a column for vector type.
    */
    DROP TABLE IF EXISTS mrs_tests.table_vector;
    CREATE TABLE mrs_tests.table_vector (
        idx SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
        embedding VECTOR,
        PRIMARY KEY  (idx)
    );

After you have added the corresponding schema (`mrs_tests`) and view (`table_vector`) objects to the MRS service (let’s say `my_service`), you can start using the MRS Python SDK.

##### 8.3.1.3.1 Create

Inserting a record into the `table_vector` table in the `mrs_tests` sample database.

    from sdk.python import MyService

    async def main():
        my_service = MyService()

        data = [
            {
                "embedding": [
                    3.1415159702301025,
                    2.719064950942993,
                    -87.53939819335938,
                    44.11e+10,
                ]
            },
            {"embedding": [9.147116, -76.769115, -5.354053]},
            {"embedding": None},
        ]
        async for doc in my_service.mrs_tests.table_vector.create_many(data):
            print(doc.embedding)

        # ------STDOUT-------
        # [3.1415159702301025, 2.719064950942993, -87.53939819335938, 44.11e+10]
        # [9.147116, -76.769115, -5.354053]
        # None

    if __name__ == "__main__":
        asyncio.run(main())

##### 8.3.1.3.2 Update

Updating a record from the `table_vector` table in the `mrs_tests` sample database.

    from sdk.python import MyService

    async def main():
        my_service = MyService()

        doc = await my_service.mrs_tests.table_vector.update(
            data={
                "idx": 1,
                "embedding": [-3.141516, 5.769005, -0.334013, 33.333, -12.76],
            }
        )

    if __name__ == "__main__":
        asyncio.run(main())

##### 8.3.1.3.3 Find

Finding a record from the `table_vector` table in the `mrs_tests` sample database.

    from sdk.python import MyService

    async def main():
        my_service = MyService()

        doc_id = 2
        try:
            doc = await my_service.mrs_tests.table_vector.find_first_or_throw(
                where={"idx": doc_id}
            )
        except MrsDocumentNotFoundError:
            raise MrsDocumentNotFoundError(msg=f"No document exists matching idx={doc_id}")

        print(doc.embedding)

        # ------STDOUT-------
        # [9.147116, -76.769115, -5.354053]

    if __name__ == "__main__":
        asyncio.run(main())

## 8.4 Working with lossy numbers

TypeScript `number`s use a double precision 64-bit binary format as defined by the IEEE 754 standard. This means that it is not capable, without losing precision, of representing integers above 2^53-1 (which are valid in the 64-bit integer range) and also fixed-point arbitrary precision decimals. This is particularly important because the `BIGINT` data type in MySQL can represent numbers up to 2^64-1 and the `DECIMAL`/`NUMERIC` data type can represent fixed point numbers.

64-bit integers can still be represented using a `BigInt` type without losing precision, so the TypeScript SDK converts the raw JSON number into a corresponding instance of this type, if the value, in fact, looses precision. Otherwise it converts it to a regular `number` instance.

For example, consider a table as follows:

        CREATE TABLE IF NOT EXISTS my_db.my_table (small BIGINT UNSIGNED, large BIGINT UNSIGNED);
        INSERT INTO my_db.my_table (small, large) VALUES (1234, 18446744073709551615);

with a corresponding REST View created as follows:

        CREATE REST VIEW /myTable
            ON SERVICE /myService SCHEMA /myDb
            AS `my_db`.`my_table` {
                small: small,
                large: large,
            };

The document can be retrieved, using the TypeScript SDK, as follows:

    const doc = await myService.myDb.myTable.findFirst({ where: { large: 18446744073709551615n } })
    console.log(doc.small) // 123
    console.log(typeof doc.small) // number
    console.log(doc.large) // 18446744073709551615n
    console.log(typeof doc.large) // bigint

However, there is no similar construct for fixed-point decimals. In this case, the MRS TypeScript SDK handles these values as `string`s, if they, in fact, lose precision. For example, consider a table as follows:

        CREATE TABLE IF NOT EXISTS my_db.my_table (wide DECIMAL(18, 17) narrow DECIMAL(18, 17));
        INSERT INTO my_db.my_table (wide, narrow) VALUES (1.234, 1.23456789012345678);

with a corresponding REST View created as follows:

        CREATE REST VIEW /myTable
            ON SERVICE /myService SCHEMA /myDb
            AS `my_db`.`my_table` {
                wide: wide,
                narrow: narrow,
            };

The document can be retrieved, using the TypeScript SDK, as follows:

    const doc = await myService.myDb.myTable.findFirst({ where: { narrow: "1.23456789012345678" } })
    console.log(doc.wide) // 123
    console.log(typeof doc.wide) // number
    console.log(doc.narrow) // 1.23456789012345678
    console.log(typeof doc.narrow) // string

# 9 Application Metadata

Application-specific metadata can be attached to any MRS Resource (REST Service, Schema and/or Object).

## 9.1 REST Services

Consider a REST Service, with custom metadata, created as follows:

        CREATE OR REPLACE REST SERVICE /myService
            METADATA {
              "type": "service"
            };

After generating the TypeScript SDK for this service, the custom metadata can be obtained as follows:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        const metadata = await myService.getMetadata()
        console.log(metadata) // { type: "service" }

If the REST Service does not contain custom metadata:

        CREATE OR REPLACE REST SERVICE /myService

The command returns an empty object:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        const metadata = await myService.getMetadata()
        console.log(metadata) // {}

## 9.2 REST Schemas

Consider a MySQL database created as follows:

        CREATE DATABASE IF NOT EXISTS my_db;

and a corresponding REST Schema, with custom metadata, created as follows:

        CREATE OR REPLACE REST SCHEMA /myDb ON SERVICE /myService
            FROM `my_db`
            METADATA {
              "type": "schema"
            };

The custom metadata can be obtained as follows:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        const metadata = await myService.myDb.getMetadata()
        console.log(metadata) // { type: "schema" }

Just like for a REST Service, if a REST Schema does not specify custom metadata:

        CREATE OR REPLACE REST SCHEMA /myDb ON SERVICE /myService
            FROM `my_db`

The command returns an empty object:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        const metadata = await myService.myDb.getMetadata()
        console.log(metadata) // {}

If the REST Schema requires authentication:

        CREATE OR REPLACE REST SCHEMA /myDb ON SERVICE /myService
            FROM `my_db`
            AUTHENTICATION REQUIRED
            METADATA {
              "type": "schema"
            };

        CREATE USER foo IDENTIFIED BY 'bar';

        ALTER REST SERVICE /myService ADD AUTH APP "MySQL";

The command only succeeds if the client authenticates beforehand:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        await myService.authenticate({ username: "foo", password: "bar", app: "MySQL" })
        const metadata = await myService.myDb.getMetadata()

Otherwise, the command yields an authentication error:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        try {
            const metadata = await myService.myDb.getMetadata()
        } catch (err) {
            console.log(err) // Not authenticated. Please authenticate first before accessing the path /myService/myDb/_metadata.
        }

## 9.3 REST Objects

Custom metadata can be specified for any kind of REST object, be it a `VIEW`, `FUNCTION`, `PROCEDURE` or `SCRIPT`.

As an example, consider any MySQL Table, such as one created as follows:

        CREATE TABLE IF NOT EXISTS my_table (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(3), PRIMARY KEY (id));

and a corresponding REST View, with custom metadata, created as follows:

        CREATE OR REPLACE REST VIEW /myTable ON SERVICE /myService SCHEMA /myDb
            AS `my_db`.`my_table` CLASS MyServiceMyDbMyTable {
                id: id @SORTABLE @KEY,
                name: name
            }
            METADATA {
                "type": "table"
            };

The custom metadata can be obtained as follows:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        const metadata = await myService.myDb.myTable.getMetadata()
        console.log(metadata) // { type: "table" }

Just like for a REST Service and Schema, if the REST View does not specify custom metadata:

        CREATE OR REPLACE REST VIEW /myTable ON SERVICE /myService SCHEMA /myDb
            AS `my_db`.`my_table` CLASS MyServiceMyDbMyTable {
                id: id @SORTABLE @KEY,
                name: name
            };

The command returns an empty object:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        const metadata = await myService.myDb.myTable.getMetadata()
        console.log(metadata) // {}

If the REST View requires authentication:

        CREATE OR REPLACE REST VIEW /myTable ON SERVICE /myService SCHEMA /myDb
            AS `my_db`.`my_table` CLASS MyServiceMyDbMyTable {
                id: id @SORTABLE @KEY,
                name: name
            }
            AUTHENTICATION REQUIRED
            METADATA {
                "type": "table"
            };

Just like for a REST Schema, the command only succeeds if the client authenticates beforehand:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        await myService.authenticate({ username: "foo", password: "bar", app: "MySQL" })
        const metadata = await myService.myDb.myTable.getMetadata()
        console.log(metadata) // { type: "table" }

Otherwise, the command yields an authentication error:

        import { MyService } from "/path/to/sdk/myService"

        const myService = new MyService()

        try {
            const metadata = await myService.myDb.myTable.getMetadata()
        } catch (err) {
            console.log(err) // Not authenticated. Please authenticate first before accessing the path /myService/myDb/myTable/_metadata.
        }

Copyright (c) 2022, 2025, Oracle and/or its affiliates.