MariaDB: Create dynamic PrepareStatement inside a Procedure

All we need is an easy explanation of the problem, so here it is.

I am trying to make a stored procedure which returns me the records of a table that match filters that can be applied in layers.

The procedure receives certain variables as parameters and I want to construct a PrepareStatement that adds the non-null variables as filters. I am using MariaDB 10.6.2

The table I am working on (removing the foreign keys) looks like:

CREATE OR REPLACE TABLE Thesis_Detail(
    thesis_id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    year SMALLINT NOT NULL,
    file VARCHAR(255) NOT NULL UNIQUE,
    abstract TEXT NOT NULL,
    uploaded_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX(year),
    FULLTEXT(title)
) DEFAULT CHARACTER SET utf8mb4;

The goal itself is to create it this way

DELIMITER //
CREATE OR REPLACE PROCEDURE UThesis.searchThesisByFilters(
    IN year_in SMALLINT,
    IN title_in VARCHAR(255),
    IN limit_in TINYINT,
    IN offset_in TINYINT
)
BEGIN
    DECLARE first BIT DEFAULT 0;

    SET @sql = 'SELECT TD.title AS title,' ||
               'TD.year AS year,' ||
               'TD.file AS path,' ||
               'TD.abstract AS abstract,' ||
               'TD.thesis_id AS thesis_id ' ||
               'FROM Thesis_Detail TD ';

    IF NOT ISNULL(title_in) THEN
        SET first = 1;
        SET @sql = @sql + ' WHERE MATCH(title) AGAINST(? IN NATURAL LANGUAGE MODE)';
    END IF;

    IF NOT ISNULL(year_in) THEN
        IF first THEN
            SET @sql = @sql + ' WHERE';
        ELSE
            SET @sql = @sql + ' AND';
        END IF;
        SET @sql = @sql + ' TD.year = ?';
    END IF;

    SET @sql = @sql + ' LIMIT ?  OFFSET  ?';

    PREPARE stmt FROM @sql;
    EXECUTE stmt using title_in, year_in, limit_in, offset_in;
    DEALLOCATE PREPARE stmt;

END //

DELIMITER ;

The problem is that the following line would be dynamic, that is, it may or may not have the title_in or year_in

EXECUTE stmt using title_in, year_in, limit_in, offset_in;
EXECUTE stmt using year_in, limit_in, offset_in;
EXECUTE stmt using title_in, limit_in, offset_in;
EXECUTE stmt using limit_in, offset_in;

This example can be solved with combinations of whether one or two are null, but the problem is that I have to apply more filters. In total there are 5 filters but doing the case of each combination ends up being terrible. Any ideas how I can achieve this?

In the first link they make use of CONCAT, but I don’t know if that makes the procedure vulnerable to SQL injections.

CREATE OR REPLACE PROCEDURE UThesis.searchThesisByFilters(
    IN year_in SMALLINT,
    IN title_in VARCHAR(255),
    IN limit_in TINYINT,
    IN offset_in TINYINT
)
BEGIN
    DECLARE first BIT DEFAULT 0;

    SET @sql = 'SELECT TD.title AS title,' ||
               'TD.year AS year,' ||
               'TD.file AS path,' ||
               'TD.abstract AS abstract,' ||
               'TD.thesis_id AS thesis_id ' ||
               'FROM Thesis_Detail TD ';

    IF NOT ISNULL(title_in) THEN
        SET first = 1;
        SET @sql = @sql + ' WHERE MATCH(title) AGAINST(? IN NATURAL LANGUAGE MODE)';
    END IF;

    IF NOT ISNULL(title_in) THEN
        IF first THEN
            SET @sql = @sql + ' WHERE';
        ELSE
            SET @sql = @sql + ' AND';
        END IF;
        SET @sql = @sql + CONCAT(' TD.year = ', year_in);
    END IF;

    SET @sql = @sql + CONCAT(' LIMIT', limit_in, ' OFFSET ', offset_in);

    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;

END //

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

The only security risk is title_in, as all other are checkd and give an error if they are not a Number.

So you can’t contaminate the title_in, instead you make it a prepared statennet.

Basicallye you can make title_in and year_in prepared, if you wish, but as i said ints aren’t security issue

So your code would look like

CREATE OR REPLACE PROCEDURE UThesis.searchThesisByFilters(
    IN year_in SMALLINT,
    IN title_in VARCHAR(255),
    IN limit_in TINYINT,
    IN offset_in TINYINT
)
BEGIN
    DECLARE first BIT DEFAULT 0;

    SET @sql = 'SELECT TD.title AS title,' ||
               'TD.year AS year,' ||
               'TD.file AS path,' ||
               'TD.abstract AS abstract,' ||
               'TD.thesis_id AS thesis_id ' ||
               'FROM Thesis_Detail TD ';

    IF NOT ISNULL(title_in) THEN
        SET @title := title_in;
        SET first = 1;
        SET @sql = @sql + ' WHERE MATCH(title) AGAINST(? IN NATURAL LANGUAGE MODE)';
    END IF;

    IF NOT ISNULL(title_in) THEN
        IF first THEN
            SET @sql = @sql + ' WHERE';
        ELSE
            SET @sql = @sql + ' AND';
        END IF;
        SET @sql = @sql + CONCAT(' TD.year = ', year_in);
    END IF;

    SET @sql = @sql + CONCAT(' LIMIT', limit_in, ' OFFSET ', offset_in);

    PREPARE stmt FROM @sql;
    IF NOT ISNULL(title_in) THEN
    EXECUTE stmt USING @title;
    ELSE
       EXECUTE stmt;
    END IF;
    DEALLOCATE PREPARE stmt;

END //

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply