MySQL

Get started

Everything the samples need on your machine, once: the .NET SDK, a project, the packages, the database and the connection string.

  1. Install the .NET SDK

    .NET SDK 10.0.100 or later 10.0.x. The samples target net10.0. Check what you have:

    Terminal
    dotnet --version
  2. Create a project

    Terminal
    dotnet new console -n ArxSample -f net10.0
    cd ArxSample
  3. Add the packages

    • ARX.SDK.MySQL1.0.0-rc.1: The MySQL provider. It brings ARX.SDK and MySqlConnector 2.6.2 with it.
    • ARX.SDK1.0.0-rc.1: The core: entities, queries, the schema catalog, errors. Comes with the provider.
    • MySqlConnector2.6.2: The MySQL driver (MIT licence, truly asynchronous). Comes with the provider.
    Terminal
    dotnet add package ARX.SDK.MySQL --version 1.0.0-rc.1

    Some advanced samples need one more package; their pages give the exact command.

  4. Create the database and the sample schema

    MySQL 8.4 (tested with 8.4.13), through MySqlConnector 2.6.2 or later (the lowest version, which the provider is built on; Versions and compatibility lists the versions it is tested on). With Docker, start one and create the database (choose your own password):

    Terminal
    docker run --name arx-samples-mysql -e MYSQL_ROOT_PASSWORD=your-password -e MYSQL_DATABASE=arx_samples -p 3306:3306 -d mysql:8.4 --log-bin-trust-function-creators=1

    Then create the sample tables: download sample-schema.sql, or copy it below, and run it. Run it again whenever you want to start over: it drops and recreates the sample tables, empty.

    Terminal

    macOS and Linux (bash, zsh)

    docker exec -i arx-samples-mysql mysql -uroot -pyour-password arx_samples < sample-schema.sql
    PowerShell

    Windows PowerShell

    Get-Content sample-schema.sql -Raw | docker exec -i arx-samples-mysql mysql -uroot -pyour-password arx_samples
    sample-schema.sql

    The sample schema the samples run against.

    -- Schema for the ARX.SDK MySQL documentation samples: a small CRM and product catalogue, and a table of every data type.
    -- Run it in a database of its own (arx_samples). Re-runnable: it drops, by name, only the tables, views and routines it
    -- creates below, so running it again starts over and nothing else is touched. Its last, optional part creates the
    -- database arx_samples_reporting when it doesn't exist and replaces one view in it; it never drops a database.
    -- A plain mysql client script: statements end with ;, and the procedures and functions are between DELIMITER $$
    -- and DELIMITER ; (their bodies have semicolons of their own). The samples run it themselves; to run it by hand:
    -- mysql -u <user> -p arx_samples < sample-schema.sql. The last part (a database of its own) is optional.
    --
    -- ARX conventions used here: the table name is the entity name, and a lookup column stores the referenced row's id.
    -- Every table's id column is declared in SampleCatalog.cs: most are a generated BIGINT "id" (AUTO_INCREMENT); country
    -- and device show a natural id and a supplied CHAR(36) Guid (MySQL can't hand back a generated Guid); price_list and
    -- activity_log have no id column (NoIdColumn). Selection set columns hold the option's code itself.
    
    DROP VIEW IF EXISTS vw_key_accounts, vw_account_overview, vw_contact_directory, vw_account_sales_summary;
    DROP PROCEDURE IF EXISTS sp_recalculate_order_totals;
    DROP PROCEDURE IF EXISTS sp_rename_account;
    DROP PROCEDURE IF EXISTS sp_account_totals;
    DROP PROCEDURE IF EXISTS sp_place_order;
    DROP PROCEDURE IF EXISTS sp_order_with_lines;
    DROP PROCEDURE IF EXISTS sp_account_report;
    DROP PROCEDURE IF EXISTS sp_products_page;
    DROP PROCEDURE IF EXISTS sp_products;
    DROP PROCEDURE IF EXISTS sp_contacts_named;
    DROP PROCEDURE IF EXISTS sp_accounts_with_status;
    DROP PROCEDURE IF EXISTS sp_reserve_credit;
    DROP PROCEDURE IF EXISTS sp_recalculate_totals;
    DROP PROCEDURE IF EXISTS sp_import_progress;
    DROP PROCEDURE IF EXISTS sp_slow;
    DROP FUNCTION IF EXISTS fn_order_count;
    DROP FUNCTION IF EXISTS fn_contact_count;
    SET FOREIGN_KEY_CHECKS = 0;
    DROP TABLE IF EXISTS legacy_customer, web_visit, activity_log, price_list, device, country, order_line, sales_order, data_type_showcase, product, product_category, contact, account;
    SET FOREIGN_KEY_CHECKS = 1;
    
    -- Customers. industry is an Integer selection set built from a .NET enum; status a String set the CHECK also guards;
    -- segments a multiple-choice String set stored as delimited text. created_on and modified_on are TIMESTAMP(6): instants,
    -- stored as UTC. A UNIQUE key allows many NULLs in MySQL, so several accounts may leave account_number empty.
    CREATE TABLE account (
        id                 BIGINT         AUTO_INCREMENT PRIMARY KEY,
        guid               CHAR(36)       NOT NULL DEFAULT (UUID()),
        name               VARCHAR(200)   NOT NULL,
        account_number     VARCHAR(20)    NULL,
        industry           SMALLINT,
        status             VARCHAR(10)    NOT NULL DEFAULT 'ACTIVE',
        segments           VARCHAR(200),
        website            VARCHAR(200),
        annual_revenue     DECIMAL(18, 2),
        employee_count     INT,
        credit_limit       DECIMAL(18, 2),
        is_key_account     BOOLEAN        NOT NULL DEFAULT FALSE,
        founded_on         DATE,
        parent_account_id  BIGINT,
        primary_contact_id BIGINT,        -- references contact (added after contact below; the preload samples follow it)
        created_on         TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
        modified_on        TIMESTAMP(6)   NULL,
        CONSTRAINT account_account_number_key UNIQUE (account_number),
        CONSTRAINT account_status_check CHECK (status IN ('ACTIVE', 'ON_HOLD', 'CLOSED')),
        CONSTRAINT account_employee_count_check CHECK (employee_count >= 0),
        CONSTRAINT account_parent_account_id_fkey FOREIGN KEY (parent_account_id) REFERENCES account (id)
    );
    
    -- People at a customer. account_id is a lookup (ArxEntityReference); email is unique. preferred_channel, lead_source
    -- and rating are selection sets (lead_source and rating come from the catalog's JSON).
    CREATE TABLE contact (
        id                BIGINT         AUTO_INCREMENT PRIMARY KEY,
        guid              CHAR(36)       NOT NULL DEFAULT (UUID()),
        account_id        BIGINT,
        first_name        VARCHAR(100)   NOT NULL,
        last_name         VARCHAR(100)   NOT NULL,
        job_title         VARCHAR(100),
        email             VARCHAR(254),
        phone             VARCHAR(30),
        preferred_channel CHAR(1),
        lead_source       VARCHAR(50),
        rating            SMALLINT,
        birthday          DATE,
        do_not_email      BOOLEAN        NOT NULL DEFAULT FALSE,
        consent_given_at  TIMESTAMP(6)   NULL,
        notes             TEXT,
        created_on        TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
        CONSTRAINT contact_email_key UNIQUE (email),
        CONSTRAINT contact_account_id_fkey FOREIGN KEY (account_id) REFERENCES account (id)
    );
    
    -- account.primary_contact_id: a contact of the account, the reference a preload follows to add contact.
    ALTER TABLE account ADD CONSTRAINT account_primary_contact_fkey FOREIGN KEY (primary_contact_id) REFERENCES contact (id);
    
    CREATE TABLE product_category (
        id    BIGINT        AUTO_INCREMENT PRIMARY KEY,
        name  VARCHAR(100)  NOT NULL,
        CONSTRAINT product_category_name_key UNIQUE (name)
    );
    
    -- The catalogue. attributes is JSON (ArxJsonB; MySQL checks the text and stores its own form); image is binary; tags
    -- is delimited text.
    CREATE TABLE product (
        id              BIGINT          AUTO_INCREMENT PRIMARY KEY,
        guid            CHAR(36)        NOT NULL DEFAULT (UUID()),
        sku             VARCHAR(32)     NOT NULL,
        name            VARCHAR(200)    NOT NULL,
        description     TEXT,
        category_id     BIGINT,
        unit_price      DECIMAL(12, 2)  NOT NULL,
        cost            DECIMAL(12, 4),
        weight_kg       DOUBLE,
        stock_quantity  INT             NOT NULL DEFAULT 0,
        is_active       BOOLEAN         NOT NULL DEFAULT TRUE,
        tags            VARCHAR(400),
        attributes      JSON,
        image           LONGBLOB,
        launched_on     DATE,
        created_on      TIMESTAMP(6)    NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
        CONSTRAINT product_sku_key UNIQUE (sku),
        CONSTRAINT product_unit_price_check CHECK (unit_price >= 0),
        CONSTRAINT product_category_id_fkey FOREIGN KEY (category_id) REFERENCES product_category (id)
    );
    
    -- An order and its lines, for the "one process, one transaction" samples.
    CREATE TABLE sales_order (
        id           BIGINT          AUTO_INCREMENT PRIMARY KEY,
        order_number VARCHAR(20)     NOT NULL,
        account_id   BIGINT          NOT NULL,
        ordered_on   TIMESTAMP(6)    NOT NULL,
        total        DECIMAL(14, 2)  NOT NULL DEFAULT 0,
        CONSTRAINT sales_order_order_number_key UNIQUE (order_number),
        CONSTRAINT sales_order_account_id_fkey FOREIGN KEY (account_id) REFERENCES account (id)
    );
    
    CREATE TABLE order_line (
        id          BIGINT          AUTO_INCREMENT PRIMARY KEY,
        order_id    BIGINT          NOT NULL,
        product_id  BIGINT          NOT NULL,
        quantity    INT             NOT NULL,
        unit_price  DECIMAL(12, 2)  NOT NULL,
        CONSTRAINT order_line_quantity_check CHECK (quantity > 0),
        CONSTRAINT order_line_order_id_fkey FOREIGN KEY (order_id) REFERENCES sales_order (id),
        CONSTRAINT order_line_product_id_fkey FOREIGN KEY (product_id) REFERENCES product (id)
    );
    
    -- One column for every kind of value the MySQL provider writes (README, "Type mapping").
    CREATE TABLE data_type_showcase (
        id                 BIGINT              AUTO_INCREMENT PRIMARY KEY,
        text_value         TEXT,
        varchar_value      VARCHAR(50),
        char_value         CHAR(3),
        tinyint_value      TINYINT,
        smallint_value     SMALLINT,
        int_value          INT,
        bigint_value       BIGINT,
        unsigned_value     INT UNSIGNED,
        decimal_value      DECIMAL(30, 6),
        float_value        FLOAT,
        double_value       DOUBLE,
        bool_value         BOOLEAN,
        uuid_value         CHAR(36),
        uuid_binary_value  BINARY(16),
        timestamp_value    TIMESTAMP(6)        NULL,
        datetime_value     DATETIME(6),
        datetime0_value    DATETIME,
        date_value         DATE,
        time_value         TIME(6),
        blob_value         LONGBLOB,
        json_value         JSON,
        mood               ENUM('happy', 'neutral', 'sad'),
        account_id         BIGINT,
        industry           SMALLINT,
        CONSTRAINT data_type_showcase_account_id_fkey FOREIGN KEY (account_id) REFERENCES account (id)
    );
    
    -- Ids that aren't a generated BIGINT: a natural text id and a Guid the application supplies. Then two tables with no id
    -- column: a price identified by region and SKU together, and a log whose rows may repeat. All declared in
    -- SampleCatalog.cs: ARX reads ids and keys from the database only when asked to preload them.
    CREATE TABLE country (
        code  CHAR(2)       PRIMARY KEY,
        name  VARCHAR(100)  NOT NULL
    );
    
    CREATE TABLE device (
        id             CHAR(36)     PRIMARY KEY,
        serial_number  VARCHAR(50)  NOT NULL,
        account_id     BIGINT,
        CONSTRAINT device_serial_number_key UNIQUE (serial_number),
        CONSTRAINT device_account_id_fkey FOREIGN KEY (account_id) REFERENCES account (id)
    );
    
    CREATE TABLE price_list (
        region      CHAR(2)         NOT NULL,
        sku         VARCHAR(32)     NOT NULL,
        unit_price  DECIMAL(12, 2)  NOT NULL,
        valid_from  DATE            NOT NULL,
        PRIMARY KEY (region, sku),
        CONSTRAINT price_list_unit_price_check CHECK (unit_price >= 0),
        CONSTRAINT price_list_region_fkey FOREIGN KEY (region) REFERENCES country (code)
    );
    
    -- No primary key at all: the same action can be logged twice.
    CREATE TABLE activity_log (
        account_id  BIGINT,
        action      VARCHAR(200)   NOT NULL,
        logged_at   TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
        CONSTRAINT activity_log_account_id_fkey FOREIGN KEY (account_id) REFERENCES account (id)
    );
    
    -- A table made outside ARX, its columns named outside ARX's name rule: used through aliases (Create Advanced10).
    CREATE TABLE legacy_customer (
        `Customer Code`  VARCHAR(20)     NOT NULL PRIMARY KEY,
        `first-name`     VARCHAR(100)    NOT NULL,
        `last-name`      VARCHAR(100)    NULL,
        `order.total`    DECIMAL(12, 2)  NULL
    );
    
    -- Web visits (declared NoIdColumn in SampleCatalog.cs).
    CREATE TABLE web_visit (
        account_id  BIGINT,
        page        VARCHAR(200)  NOT NULL,
        visited_on  DATE          NOT NULL,
        CONSTRAINT web_visit_account_id_fkey FOREIGN KEY (account_id) REFERENCES account (id)
    );
    
    -- Naming: views vw_, functions fn_, procedures sp_; tables have no prefix. The name says what an object is where only
    -- the name is visible (a query, a log, an error message), and ARX reads them all by name.
    
    -- Retrieve: a view an application reads like a table. MySQL has no functions that return rows: read a view, or call a
    -- procedure through Execute.
    CREATE VIEW vw_account_overview AS
    SELECT a.id, a.name, a.account_number, a.status, COUNT(c.id) AS contacts
    FROM account a
    LEFT JOIN contact c ON c.account_id = a.id
    GROUP BY a.id, a.name, a.account_number, a.status;
    
    -- An account summary, computed at each read (MySQL has no materialized views).
    CREATE VIEW vw_account_sales_summary AS
    SELECT a.id AS account_id, a.name, a.account_number, COUNT(o.id) AS orders, COALESCE(SUM(o.total), 0) AS revenue
    FROM account a
    LEFT JOIN sales_order o ON o.account_id = a.id
    GROUP BY a.id, a.name, a.account_number;
    
    DELIMITER $$
    
    -- One value: a scalar function (the only kind MySQL has).
    CREATE FUNCTION fn_order_count(p_account_id BIGINT) RETURNS BIGINT
    READS SQL DATA
    RETURN (SELECT COUNT(*) FROM sales_order WHERE account_id = p_account_id)$$
    
    -- A procedure: not a row source (Execute's, not Retrieve's).
    CREATE PROCEDURE sp_recalculate_order_totals()
    UPDATE sales_order o SET total = COALESCE((SELECT SUM(l.quantity * l.unit_price) FROM order_line l WHERE l.order_id = o.id), 0)$$
    
    DELIMITER ;
    
    -- Views in depth: a directory of contacts without their private columns (email, phone, birthday, notes), and a view
    -- built on another view.
    CREATE VIEW vw_contact_directory AS
    SELECT id, first_name, last_name, job_title, account_id
    FROM contact;
    
    CREATE VIEW vw_key_accounts AS
    SELECT id, name, account_number, contacts
    FROM vw_account_overview
    WHERE contacts >= 2;
    
    DELIMITER $$
    
    -- Execute: procedures (sp_) and functions (fn_) the samples call.
    
    -- Rename an account (no result); its totals for a year (IN, INOUT and OUT values). MySQL has no default parameter
    -- values: every argument is passed.
    CREATE PROCEDURE sp_rename_account(IN p_account_id BIGINT, IN p_name VARCHAR(200), IN p_reason VARCHAR(200))
    UPDATE account SET name = p_name, modified_on = CURRENT_TIMESTAMP(6) WHERE id = p_account_id$$
    
    CREATE PROCEDURE sp_account_totals(IN p_account_id BIGINT, INOUT p_year INT, OUT p_orders BIGINT, OUT p_revenue DECIMAL(18, 2))
    BEGIN
        SET p_year = COALESCE(p_year, YEAR(UTC_TIMESTAMP()));
        SELECT COUNT(*), COALESCE(SUM(total), 0) INTO p_orders, p_revenue
        FROM sales_order WHERE account_id = p_account_id AND YEAR(ordered_on) = p_year;
    END$$
    
    -- A value; rows (a procedure, since MySQL functions return one value only).
    CREATE FUNCTION fn_contact_count(p_account_id BIGINT) RETURNS BIGINT
    READS SQL DATA
    RETURN (SELECT COUNT(*) FROM contact WHERE account_id = p_account_id)$$
    
    CREATE PROCEDURE sp_contacts_named(IN p_last_name VARCHAR(100))
    SELECT id, first_name, job_title FROM contact WHERE last_name = p_last_name ORDER BY id$$
    
    -- Whole account rows with a status: every column the table has, whatever a preload listed.
    CREATE PROCEDURE sp_accounts_with_status(IN p_status VARCHAR(10))
    SELECT * FROM account WHERE status = p_status ORDER BY id$$
    
    -- Place an order: writes sales_order and order_line, returns the new id and the total. The SKUs are comma-separated.
    CREATE PROCEDURE sp_place_order(IN p_account_id BIGINT, IN p_order_number VARCHAR(20), IN p_skus TEXT, IN p_quantity INT, OUT p_order_id BIGINT, OUT p_total DECIMAL(14, 2))
    BEGIN
        INSERT INTO sales_order (order_number, account_id, ordered_on) VALUES (p_order_number, p_account_id, CURRENT_TIMESTAMP(6));
        SET p_order_id = LAST_INSERT_ID();
        INSERT INTO order_line (order_id, product_id, quantity, unit_price)
            SELECT p_order_id, p.id, p_quantity, p.unit_price FROM product p WHERE FIND_IN_SET(p.sku, REPLACE(p_skus, ' ', '')) > 0;
        SET p_total = (SELECT COALESCE(SUM(quantity * unit_price), 0) FROM order_line WHERE order_id = p_order_id);
        UPDATE sales_order SET total = p_total WHERE id = p_order_id;
    END$$
    
    -- Tables from a procedure: an order and its lines; an account report with three tables (result sets, in order).
    CREATE PROCEDURE sp_order_with_lines(IN p_order_id BIGINT)
    BEGIN
        SELECT o.order_number, o.ordered_on, o.total, a.name AS account FROM sales_order o JOIN account a ON a.id = o.account_id WHERE o.id = p_order_id;
        SELECT p.sku, l.quantity, l.unit_price FROM order_line l JOIN product p ON p.id = l.product_id WHERE l.order_id = p_order_id ORDER BY p.sku;
    END$$
    
    CREATE PROCEDURE sp_account_report(IN p_account_id BIGINT)
    BEGIN
        SELECT first_name, last_name, job_title FROM contact WHERE account_id = p_account_id ORDER BY first_name;
        SELECT order_number, total FROM sales_order WHERE account_id = p_account_id ORDER BY order_number;
        SELECT (SELECT COUNT(*) FROM contact WHERE account_id = p_account_id) AS contacts,
               (SELECT COUNT(*) FROM sales_order WHERE account_id = p_account_id) AS orders,
               (SELECT COALESCE(SUM(total), 0) FROM sales_order WHERE account_id = p_account_id) AS revenue;
    END$$
    
    -- Paging: a procedure that pages itself; a procedure's result set ARX pages.
    CREATE PROCEDURE sp_products_page(IN p_prefix VARCHAR(32), IN p_page INT, IN p_page_size INT, OUT p_total BIGINT)
    BEGIN
        DECLARE v_skip INT DEFAULT (p_page - 1) * p_page_size;
        SET p_total = (SELECT COUNT(*) FROM product WHERE sku LIKE CONCAT(p_prefix, '%'));
        SELECT sku, name, unit_price FROM product WHERE sku LIKE CONCAT(p_prefix, '%') ORDER BY sku
        LIMIT p_page_size OFFSET v_skip;
    END$$
    
    CREATE PROCEDURE sp_products(IN p_prefix VARCHAR(32))
    SELECT sku, name, unit_price FROM product WHERE sku LIKE CONCAT(p_prefix, '%') ORDER BY sku$$
    
    -- Business rules the database enforces: an account on hold and a credit limit, each with its own error number (SIGNAL).
    CREATE PROCEDURE sp_reserve_credit(IN p_account_id BIGINT, IN p_amount DECIMAL(18, 2))
    BEGIN
        DECLARE v_limit DECIMAL(18, 2);
        DECLARE v_status VARCHAR(10);
        DECLARE v_message VARCHAR(400);
        SELECT credit_limit, status INTO v_limit, v_status FROM account WHERE id = p_account_id;
    
        IF v_status = 'ON_HOLD' THEN
            SET v_message = CONCAT('account ', p_account_id, ' is on hold: release the hold first');
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_message, MYSQL_ERRNO = 50101;
        END IF;
    
        IF p_amount > COALESCE(v_limit, 0) THEN
            SET v_message = CONCAT('credit limit exceeded: limit ', COALESCE(v_limit, 0), ', asked ', p_amount, '; reserve ', COALESCE(v_limit, 0), ' or less');
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_message, MYSQL_ERRNO = 50102;
        END IF;
    
        UPDATE account SET credit_limit = credit_limit - p_amount, modified_on = CURRENT_TIMESTAMP(6) WHERE id = p_account_id;
    END$$
    
    -- A nightly job that commits each batch (MySQL lets a procedure COMMIT) and reports what it did as a note.
    CREATE PROCEDURE sp_recalculate_totals(IN p_prefix VARCHAR(20), IN p_batch INT)
    BEGIN
        DECLARE v_done INT DEFAULT 0;
        DECLARE v_rows INT DEFAULT 1;
        DECLARE v_message VARCHAR(200);
    
        WHILE v_rows > 0 DO
            START TRANSACTION;
            UPDATE sales_order o
            JOIN (SELECT id FROM sales_order WHERE order_number LIKE CONCAT(p_prefix, '%') AND total < 0 ORDER BY id LIMIT p_batch) AS b ON b.id = o.id
            SET o.total = COALESCE((SELECT SUM(l.quantity * l.unit_price) FROM order_line l WHERE l.order_id = o.id), 0);
            SET v_rows = ROW_COUNT();
            COMMIT;
            SET v_done = v_done + v_rows;
        END WHILE;
    
        SET v_message = CONCAT('done: ', v_done, ' orders');
        SIGNAL SQLSTATE '01000' SET MESSAGE_TEXT = v_message, MYSQL_ERRNO = 1642;
    END$$
    
    -- A note at the end: SIGNAL with an SQLSTATE of class 01 is a warning, and the procedure goes on. MySQL keeps only the
    -- last statement's warnings, so a procedure reports one note when it ends, not progress while it runs.
    CREATE PROCEDURE sp_import_progress(IN p_rows INT)
    BEGIN
        DECLARE v_message VARCHAR(128);
        SET v_message = CONCAT('imported ', p_rows, ' rows in ', FLOOR(p_rows / 250), ' steps of 250', IF(p_rows > 900, ': a large import', ''));
        SIGNAL SQLSTATE '01000' SET MESSAGE_TEXT = v_message, MYSQL_ERRNO = 1642;
    END$$
    
    -- A slow procedure. SLEEP returns 1 when a timeout or a cancellation wakes it (KILL QUERY) instead of failing, so the
    -- procedure raises MySQL's own "interrupted" error, as any long statement would.
    CREATE PROCEDURE sp_slow(IN p_seconds INT)
    BEGIN
        DECLARE v_woke INT;
        SELECT SLEEP(p_seconds) INTO v_woke;
        IF v_woke = 1 THEN
            SIGNAL SQLSTATE '70100' SET MESSAGE_TEXT = 'Query execution was interrupted', MYSQL_ERRNO = 1317;
        END IF;
    END$$
    
    DELIMITER ;
    
    -- Optional: a view in another database (MySQL's schema), read as "arx_samples_reporting.vw_product_catalogue" by
    -- RetrieveMultiple's Advanced 7 (ReadOtherRowSources). Creating a database needs the CREATE privilege on it: a user
    -- without it can leave this part out (the samples skip it and say so; only that sample needs it).
    CREATE DATABASE IF NOT EXISTS arx_samples_reporting;
    DROP VIEW IF EXISTS arx_samples_reporting.vw_product_catalogue;
    
    CREATE VIEW arx_samples_reporting.vw_product_catalogue AS
    SELECT p.id, p.sku, p.name, p.unit_price, c.name AS category
    FROM product p
    LEFT JOIN product_category c ON c.id = p.category_id
    WHERE p.is_active = TRUE;
    
  5. Configure the connection

    The samples read the connection string from the ARX_SAMPLES_MYSQL environment variable, never from the code. Set it in the terminal you run them from (with your password, where the database has one):

    Terminal

    macOS and Linux (bash, zsh)

    export ARX_SAMPLES_MYSQL="Server=localhost;Port=3306;Database=arx_samples;User ID=root;Password=your-password"
    PowerShell

    Windows PowerShell

    $env:ARX_SAMPLES_MYSQL = "Server=localhost;Port=3306;Database=arx_samples;User ID=root;Password=your-password"
  6. Add a sample and run it

    Each sample page lists its files: copy each into the project with the file name shown, then run:

    Terminal
    dotnet run

    The page shows the output to expect. Ids may differ from yours if the tables already hold rows. Most samples write values that must be unique (an account number, a code), so a second run of the same sample fails with ARX3001: run the schema script again first, to start from empty sample tables.