0

To not publicly disclose our amount of invoices, we want to add random value between 2 ids.

Instead of [1,2,3] we want something like [69,98,179]

UUID is not an option in that project, unfortunately.

Using Mysql 5.7, 8, or MariaDb get the same results.

Here is the approach is taken:

Consider a simple table invoices as follows:

CREATE TABLE `invoices` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8mb4;

The function to get random values:

DROP FUNCTION IF EXISTS random_integer;
CREATE FUNCTION random_integer(value_minimum INT, value_maximum INT)
RETURNS INT
LANGUAGE SQL
NOT DETERMINISTIC
RETURN FLOOR(value_minimum + RAND() * (value_maximum - value_minimum + 1));

The function to get the next id:

DROP FUNCTION IF EXISTS next_invoice_id_val;
DELIMITER //
CREATE FUNCTION next_invoice_id_val ()
RETURNS BIGINT(8)
LANGUAGE SQL
NOT DETERMINISTIC
BEGIN
  DECLARE lastId BIGINT(8) DEFAULT 1;
  DECLARE randId BIGINT(8) DEFAULT 1;
  DECLARE newId BIGINT(8) DEFAULT 1;
  DECLARE nextId BIGINT(8) DEFAULT 1;

  SELECT (SELECT MAX(`id`) FROM `invoices`) INTO lastId;
  SELECT (SELECT random_integer(1,10)) INTO randId;
  SELECT ( lastId + randId ) INTO nextId;

  IF lastId IS NULL
  THEN
    SET newId = randId;
  ELSE
    SET newId = nextId;
  END IF;

  RETURN newId;
END //
DELIMITER ;
SELECT next_invoice_id_val();

and the trigger:

DROP TRIGGER IF EXISTS next_invoice_id_val_trigger;
DELIMITER //
CREATE TRIGGER next_invoice_id_val_trigger
BEFORE INSERT
ON invoices FOR EACH ROW
BEGIN
  SET NEW.id = next_invoice_id_val();
END//
DELIMITER ;

That work like a charm, now if we want to generalize the behaviour to all tables.

We need a procedure to execute the query on any specific tables:

DROP PROCEDURE IF EXISTS last_id;
DELIMITER //
CREATE PROCEDURE last_id (IN tableName VARCHAR(50), OUT lastId BIGINT(8))
COMMENT 'Gets the last id value'
LANGUAGE SQL
NOT DETERMINISTIC
READS SQL DATA
BEGIN
  SET @s := CONCAT('SELECT MAX(`id`) FROM `',tableName,'`');
  PREPARE QUERY FROM @s;
  EXECUTE QUERY;
  DEALLOCATE PREPARE QUERY;
END //
DELIMITER ;
CALL last_id('invoices', @nextInvoiceId);
SELECT @nextInvoiceId;

The procedure for the next id value:

DROP PROCEDURE IF EXISTS next_id_val;
DELIMITER //
CREATE PROCEDURE next_id_val (IN tableName VARCHAR(50), OUT nextId BIGINT(8))
COMMENT 'Give the Next Id value + a random value'
LANGUAGE SQL
NOT DETERMINISTIC
READS SQL DATA
BEGIN
  DECLARE randId BIGINT(8) DEFAULT 1;

  SELECT (SELECT random_integer(1,10)) INTO randId;

  CALL last_id(tableName, @currentId);

  IF @currentId IS NULL
  THEN
    SET nextId = randId;
  ELSE
    SELECT ( @currentId + randId ) INTO nextId;
  END IF;
END //
DELIMITER ;
CALL next_id_val('invoices', @nextInvoiceId);
SELECT @nextInvoiceId;

and the trigger:

# Call the procedure from a trigger
DROP TRIGGER IF EXISTS next_invoice_id_val_trigger;
DELIMITER //
CREATE TRIGGER next_invoice_id_val_trigger
BEFORE INSERT
ON invoices FOR EACH ROW
BEGIN
  CALL next_id_val('invoices', @nextInvoiceId);
  SET NEW.id = @nextInvoiceId;
END//
DELIMITER ;

and we get => Dynamic SQL is not allowed in stored function or trigger

I've read that storing in a temporary table might be a workaround, but as all posts have between 5 to 10 years old, I think we might have a better solution for such a straightforward case.

  1. What is the workaround for using dynamic SQL in a stored Procedure
  2. #1336 - Dynamic SQL is not allowed in stored function or trigger
  3. Calling stored procedure that contains dynamic SQL from Trigger
  4. Alternatives to dynamic sql in stored function
Joel AZEMAR
  • 2,506
  • 25
  • 31
  • 1
    Side notes: Race condition alert! The `max(id)` might be outdated the moment after it was received, when another process `INSERT`s right then. (Unless you wrap everything in a transaction. But that might slow concurrency.) Why not using an (additional) `AUTO_INCREMENT` column? SQL injection alert! `tableName` can bet set to malicious values and inject potentially harmful code. You should somehow sanitize the value or check it against a white list (the/some table names from `information_schema.tables` for example). – sticky bit Jan 08 '22 at 20:13
  • And why do you even use dynamic SQL here at all? Get the value in the trigger, with static SQL or procedural means. If you use an `AUTO_INCREMENT` like I suggested, that'll be just access on the `new` pseudo record. If the column names are the same it would even be the same trigger body for every table. For example have real `id` (`AUTO_INCREMENT`) and a `obfuscated_id`. – sticky bit Jan 08 '22 at 20:19
  • 1
    Oh and, if the obfuscated IDs should be unique, are you really sure your algorithm guarantees that? Say, if you enter an ID of `1` and the random value is `2` and then enter an ID of `2` and the random value is `1`, which, if I understand your code right, is possible, you'll end up with two obfuscated IDs of `3`. – sticky bit Jan 08 '22 at 20:24
  • 1
    It would be much simpler to just use an auto-increment primary key, start it at 10000, and set `auto_increment_increment` to some arbitrary value like 11. That would obscure the number of invoices but eliminate a lot of complex code. – Bill Karwin Jan 09 '22 at 01:21
  • A lot of really valid points here! Thanks! @BillKarwin Indeed my first approach was about auto_increment_increment, auto_increment_offset. However, those apply to all tables from my understanding. Only AUTO_INCREMENT can be used for a table in particular. – Joel AZEMAR Jan 09 '22 at 16:41
  • @stickybit, it seems you get what I'm looking for, and all your points are precious. Unfortunately, I'm not sure how to implement your solution. If you are kind enough and have enough time, do you mind providing a code sample to illustrate that? – Joel AZEMAR Jan 09 '22 at 16:50
  • BTW I got it to work with PostgreSQL. https://gist.github.com/joel/7bad0916ee016cfc7f5df0b1f396ec78 I was curious to see if it was more straightforward with it. Who knows, can help someone... – Joel AZEMAR Jan 09 '22 at 16:57

0 Answers0