#!/bin/sh

# Copyright (C) 2025-2026 Internet Systems Consortium, Inc. ("ISC") #
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# Exit with error if commands exit with non-zero and if undefined variables are
# used.
set -eu

# shellcheck disable=SC2034
# SC2034: ... appears unused. Verify use (or export if used externally).
prefix="/usr"

# Include utilities based on location of this script. Check for sources first,
# so that the unexpected situations with weird paths fall on the default
# case of installed.
script_path=$(cd "$(dirname "${0}")" && pwd)
if test "${script_path}" = "/build/kea/src/build/src/share/database/scripts/mysql"; then
    # shellcheck source=./src/bin/admin/admin-utils.sh.in
    . "/build/kea/src/build/src/bin/admin/admin-utils.sh"
else
    # shellcheck source=./src/bin/admin/admin-utils.sh.in
    . "${prefix}/share/kea/scripts/admin-utils.sh"
fi

# Check only major version to allow for intermediary backported schema changes.
version=$(mysql_version "${@}" | cut -d '.' -f 1)
if test "${version}" != '33'; then
    printf 'This script upgrades 33.* to 34.0. '
    printf 'Reported version is %s. Skipping upgrade.\n' "${version}"
    exit 0
fi

# Get the schema name from database argument. We need this to
# query information_schema for the right database.
for arg in "${@}"
do
    if ! printf '%s' "${arg}" | grep -Eq -- '^--'
    then
        schema="$arg"
        break
    fi
done

# Make sure we have the schema.
if [ -z "$schema" ]
then
    printf "Could not find database schema name in cmd line args: %s\n" "${*}"
    exit 255
fi

mysql "$@" <<EOF

-- This line starts the schema upgrade to version 34.0.

-- Drop now obsolete isJsonSupported function and its use
DROP FUNCTION IF EXISTS isJsonSupported;

DROP TRIGGER IF EXISTS lease4_AINS;
DELIMITER $$
CREATE TRIGGER lease4_AINS AFTER INSERT ON lease4 FOR EACH ROW
BEGIN
    CALL lease4_AINS_lease4_stat(NEW.state, NEW.subnet_id);
    CALL lease4_AINS_lease4_pool_stat(NEW.state, NEW.subnet_id, NEW.pool_id);
    CALL lease4_AINS_lease4_stat_by_client_class(NEW.state, NEW.user_context);
END $$
DELIMITER ;

DROP TRIGGER IF EXISTS lease4_AUPD;
DELIMITER $$
CREATE TRIGGER lease4_AUPD AFTER UPDATE ON lease4 FOR EACH ROW
BEGIN
    CALL lease4_AUPD_lease4_stat(OLD.state, OLD.subnet_id, NEW.state, NEW.subnet_id);
    CALL lease4_AUPD_lease4_pool_stat(OLD.state, OLD.subnet_id, OLD.pool_id, NEW.state, NEW.subnet_id, NEW.pool_id);
    CALL lease4_AUPD_lease4_stat_by_client_class(OLD.state, OLD.user_context, NEW.state, NEW.user_context);
END $$
DELIMITER ;

DROP TRIGGER IF EXISTS lease4_ADEL;
DELIMITER $$
CREATE TRIGGER lease4_ADEL AFTER DELETE ON lease4 FOR EACH ROW
BEGIN
    CALL lease4_ADEL_lease4_stat(OLD.state, OLD.subnet_id);
    CALL lease4_ADEL_lease4_pool_stat(OLD.state, OLD.subnet_id, OLD.pool_id);
    CALL lease4_ADEL_lease4_stat_by_client_class(OLD.state, OLD.user_context);
END $$
DELIMITER ;

DROP TRIGGER IF EXISTS lease6_AINS;
DELIMITER $$
CREATE TRIGGER lease6_AINS AFTER INSERT ON lease6 FOR EACH ROW
BEGIN
    CALL lease6_AINS_lease6_stat(NEW.state, NEW.subnet_id, NEW.lease_type);
    CALL lease6_AINS_lease6_pool_stat(NEW.state, NEW.subnet_id, NEW.pool_id, NEW.lease_type);
    CALL lease6_AINS_lease6_stat_by_client_class(NEW.state, NEW.user_context, NEW.lease_type);
END $$
DELIMITER ;

DROP TRIGGER IF EXISTS lease6_AUPD;
DELIMITER $$
CREATE TRIGGER lease6_AUPD AFTER UPDATE ON lease6 FOR EACH ROW
BEGIN
    CALL lease6_AUPD_lease6_stat(OLD.state, OLD.subnet_id, OLD.lease_type, NEW.state, NEW.subnet_id, NEW.lease_type);
    CALL lease6_AUPD_lease6_pool_stat(OLD.state, OLD.subnet_id, OLD.pool_id, OLD.lease_type, NEW.state, NEW.subnet_id, NEW.pool_id, NEW.lease_type);
    CALL lease6_AUPD_lease6_stat_by_client_class(OLD.state, OLD.user_context, OLD.lease_type, NEW.state, NEW.user_context, NEW.lease_type);
END $$
DELIMITER ;

DROP TRIGGER IF EXISTS lease6_ADEL;
DELIMITER $$
CREATE TRIGGER lease6_ADEL AFTER DELETE ON lease6 FOR EACH ROW
BEGIN
    CALL lease6_ADEL_lease6_stat(OLD.state, OLD.subnet_id, OLD.lease_type);
    CALL lease6_ADEL_lease6_pool_stat(OLD.state, OLD.subnet_id, OLD.pool_id, OLD.lease_type);
    CALL lease6_ADEL_lease6_stat_by_client_class(OLD.state, OLD.user_context, OLD.lease_type);
END $$
DELIMITER ;

-- Create the SFLQ tables.

DROP TABLE IF EXISTS flq_pool4;
CREATE TABLE IF NOT EXISTS flq_pool4 (
    id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    start_address INT UNSIGNED NOT NULL,
    end_address INT UNSIGNED NOT NULL,
    last_returned_address INT UNSIGNED DEFAULT 0,
    subnet_id INT UNSIGNED NOT NULL,
    created_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    modification_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY flq_pool4_address_range (start_address, end_address),
    KEY flq_pool4_by_subnet_id (subnet_id)
) ENGINE=InnoDB;

DROP TABLE IF EXISTS free_lease4;
CREATE TABLE IF NOT EXISTS free_lease4 (
    address INT UNSIGNED NOT NULL,
    modification_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (address)
) ENGINE=InnoDB;

DROP TABLE IF EXISTS flq_pool6;
CREATE TABLE IF NOT EXISTS flq_pool6 (
    id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    start_address VARCHAR(45) NOT NULL,
    end_address VARCHAR(45) NOT NULL,
    lease_type TINYINT,
    delegated_len TINYINT UNSIGNED,
    last_returned_address VARCHAR(45) DEFAULT '::',
    subnet_id INT UNSIGNED NOT NULL,
    created_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    modification_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY (start_address, end_address),
    KEY flq_pool6_by_subnet_id (subnet_id)
) ENGINE=InnoDB;

DROP TABLE IF EXISTS free_lease6;
CREATE TABLE IF NOT EXISTS free_lease6 (
    address VARCHAR(45) NOT NULL,
    bin_address BINARY(16) NOT NULL,
    modification_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (address),
    UNIQUE KEY (bin_address)
) ENGINE=InnoDB;

-- Function to increment a V6 prefix by one added at prefix_len.
DROP FUNCTION IF EXISTS incrementV6Prefix;
DELIMITER $$
CREATE FUNCTION incrementV6Prefix(prefix binary(16), prefix_len INT UNSIGNED)
RETURNS binary(16)
DETERMINISTIC
BEGIN
    DECLARE new_prefix BINARY(16);
    DECLARE n_bytes INT UNSIGNED;
    DECLARE n_bits TINYINT UNSIGNED;
    DECLARE mask TINYINT UNSIGNED;
    DECLARE start_idx INT UNSIGNED;
    DECLARE tmp INT UNSIGNED;
    DECLARE i TINYINT UNSIGNED;

    IF (prefix_len < 1 OR prefix_len > 128)
    THEN
        return (prefix);
    END IF;

    SET new_prefix = prefix;

    -- Explanation: n_bytes specifies number of full bytes that are in-prefix.
    -- They can also be used as an offset for the first byte that is not in
    -- prefix. n_bits specifies number of lsb bits on the last byte that
    -- are not in-prefix. For example for a /125 prefix, the values
    -- are 15 and 3, respectively. Mask is a bitmask that has the least
    -- significant bit from the prefix set.

    SET n_bytes = (prefix_len - 1) DIV 8;
    SET n_bits = 8 - (prefix_len - (n_bytes * 8));
    IF (n_bits = 0)
    THEN
        SET mask = 1;
    ELSE
        SET mask = (n_bits * 2);
    END IF;

    -- Index of first byte to increment (1..16 i.e. not zero based)
    SET start_idx = n_bytes + 1;

    -- Get the starting byte as an int and increment it.
    SELECT (conv(hex(substring(new_prefix, start_idx, 1)), 16, 10) + mask) into tmp;

    -- If the new value is less than 256, we safely increase just the starting byte.
    IF (tmp < 256)
    THEN
        SET new_prefix = INSERT(new_prefix, start_idx, 1, UNHEX(HEX(tmp)));
        RETURN (new_prefix);
    END IF;

    -- Overflow, set the byte to the remainder.
    SET tmp = tmp - 256;
    SET new_prefix = INSERT(new_prefix, start_idx, 1, UNHEX(HEX(tmp)));

    --  Now propagate the overflow.
    SET i = start_idx - 1;
    WHILE i >= 1 DO
        SELECT (conv(hex(substring(new_prefix, i, 1)), 16, 10) + 1) into tmp;
        IF (tmp != 256)
        THEN
            -- No more overflow, store the byte and we're done.
            SET new_prefix = INSERT(new_prefix, i, 1, UNHEX(HEX(tmp)));
            RETURN (new_prefix);
        END IF;

        -- Store zero in the current byte, and move to the next one.
        SET new_prefix = INSERT(new_prefix, i, 1, 0x00);
        SET i = i - 1;
    END WHILE;

    -- All done.
    RETURN (new_prefix);
END $$
DELIMITER ;

-- Procedure to set session variable to the DB limit on recursion. The
-- variable has different names under MySQL and Mariadb (naturally).
-- This procedure tolerates the failed SQL errors on variable names
-- that don't exist. It sets the session variable @kea_recursion_limit
-- if it is not already set. If it fails to find either variable it
-- defaults to 1000.
DROP PROCEDURE IF EXISTS setLoopLimit;
DELIMITER $$
CREATE PROCEDURE setLoopLimit()
SP:
BEGIN
    DECLARE sqlv VARCHAR(512);

    -- Ignore unknown variable errors.
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;

    IF (@kea_recursion_limit IS NOT NULL)
    THEN
        -- Already set it.
        LEAVE SP;
    END IF;

    -- Try Mariadb variable
    SET @sqlv = "SELECT @@max_recursive_iterations INTO @kea_recursion_limit";
    PREPARE stmt FROM @sqlv;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;

    IF (@kea_recursion_limit = 0)
    THEN
        -- Recursion is disabled. Override it.
        SET @sqlv = "SET @@max_recursive_iterations = 1000";
        PREPARE stmt FROM @sqlv;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
        SET @kea_recursion_limit = 1000;
        LEAVE SP;
    END IF;

    IF (@kea_recursion_limit IS NULL)
    THEN
        -- Try MySQL variable.
        SET @sqlv = "SELECT @@cte_max_recursion_depth INTO @kea_recursion_limit";
        PREPARE stmt FROM @sqlv;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
        IF (@kea_recursion_limit = 0)
        THEN
            -- Recursion is disabled. Override it.
            SET @sqlv = "SET @@cte_max_recursion_depth = 1000";
            PREPARE stmt FROM @sqlv;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
            SET @kea_recursion_limit = 1000;
            LEAVE SP;
        END IF;
    END IF;

    IF (@kea_recursion_limit IS NULL)
    THEN
        -- Found neither use a default.
        SET @kea_recursion_limit = 1000;
    ELSE
        -- Keep it sane.
        IF (@kea_recursion_limit <= 0 OR @kea_recursion_limit >= 1000)
        THEN
            SET @kea_recursion_limit = 1000;
        END IF;
    END IF;
END $$
DELIMITER ;

-- Populate flq_pool4 and free_lease4 based on an address range.
DROP PROCEDURE IF EXISTS sflqCreateFlqPool4;
DELIMITER $$
CREATE PROCEDURE sflqCreateFlqPool4(IN p_start_address INT UNSIGNED,
                                    IN p_end_address INT UNSIGNED,
                                    IN p_subnet_id INT UNSIGNED,
                                    IN p_recreate TINYINT)
BEGIN
    DECLARE loop_limit INT UNSIGNED;
    DECLARE next_start_address INT UNSIGNED;
    DECLARE next_end_address INT UNSIGNED;
    DECLARE remaining INT UNSIGNED;
    DECLARE error_msg VARCHAR(512);

    IF (p_start_address = inet_aton('0.0.0.0') OR
        p_start_address > p_end_address)
    THEN
        SET error_msg = CONCAT('Invalid range: ', inet_ntoa(p_start_address),
                               ' - ', inet_ntoa(p_end_address));
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = error_msg;
    END IF;

    START TRANSACTION;

    -- Create the flq_pool4 row. Concurrent attempts will hang until
    -- this one commits and then they will fail with duplicate key
    -- error. Callers should treat the duplicate error as success.
    IF p_recreate = 1
    THEN
        -- (Re)creating should ignore duplicate on insert
        INSERT INTO flq_pool4 (start_address, end_address, subnet_id)
           VALUES (p_start_address, p_end_address, p_subnet_id)
        ON DUPLICATE KEY UPDATE start_address = start_address;
    ELSE
        INSERT INTO flq_pool4 (start_address, end_address, subnet_id)
           VALUES (p_start_address, p_end_address, p_subnet_id);
    END IF;

    -- Wipe out existing free addresses. This ensures we fix any mixed allocation entries.
    DELETE FROM free_lease4 WHERE address >= p_start_address and address <= p_end_address;

    -- Insert available leases into free_lease4 table.  If insert fails on
    -- duplicate ignore it. MySql limits recursive queries so we iterate
    -- pool address range in chunks.
    CALL setLoopLimit();
    SET loop_limit = @kea_recursion_limit - 1;
    SET next_start_address = p_start_address;
    SET next_end_address = p_start_address;
    start_loop: WHILE next_start_address <= p_end_address DO
        SET remaining = p_end_address - next_start_address;
        IF (remaining > loop_limit)
        THEN
            SET next_end_address = next_end_address + loop_limit;
        ELSE
            SET next_end_address = next_end_address + remaining;
        END IF;

        INSERT INTO free_lease4 (address)
            WITH RECURSIVE addresses AS (
                SELECT next_start_address AS avail
                UNION ALL
                SELECT avail + 1 FROM addresses WHERE avail < next_end_address
            )
            SELECT avail FROM addresses
                LEFT JOIN lease4 on avail = lease4.address
                WHERE (lease4.address IS NULL OR lease4.state = 2)
            ON DUPLICATE KEY UPDATE free_lease4.address = avail;

        IF next_start_address + loop_limit <= 4294967295 THEN
            SET next_start_address = next_start_address + loop_limit;
        ELSE
            LEAVE start_loop;
        END IF;
    END WHILE start_loop;

    -- Update the modification time in the flq_pool row.
    UPDATE flq_pool4 SET modification_ts = now()
        WHERE (start_address = p_start_address AND end_address = p_end_address);

    COMMIT;
END $$
DELIMITER ;

-- Select a free address from an address range.
DROP FUNCTION IF EXISTS sflqPickFreeLease4;
DELIMITER $$
CREATE FUNCTION sflqPickFreeLease4(alloc_start_address INT UNSIGNED,
                                   alloc_end_address INT UNSIGNED)
RETURNS INT UNSIGNED
READS SQL DATA
BEGIN
    -- Declarations
    DECLARE pool_id BIGINT(20);
    DECLARE last_address INT UNSIGNED;
    DECLARE free_address INT UNSIGNED;

    SET free_address = 0;

    -- Find the pool, get the id and last addressed picked.
    SELECT id, last_returned_address INTO pool_id, last_address
        FROM flq_pool4
        WHERE start_address = alloc_start_address
        AND end_address = alloc_end_address;

    IF (pool_id IS NULL)
    THEN
        -- No matching pool, return empty address.
        RETURN free_address;
    END IF;

    IF (last_address < alloc_start_address OR last_address >= alloc_end_address)
    THEN
        -- Set last pick to start address less one.
        SET last_address = alloc_start_address - 1;
    END IF;

    -- Find first address greater than the last pick.
    SELECT f.address INTO free_address FROM free_lease4 f
        WHERE (f.address > last_address and f.address <= alloc_end_address)
        ORDER BY f.address ASC
        LIMIT 1;

    -- Didn't find one, so try front half of the range, including
    -- the last pick. This avoids returning nothing if there is only
    -- one address free.
    IF (free_address = 0)
    THEN
        SELECT f.address INTO free_address FROM free_lease4 f
            WHERE (f.address >= alloc_start_address AND f.address <= last_address)
            ORDER BY f.address ASC
            LIMIT 1;
    END IF;

    IF (free_address != 0)
    THEN
        -- Update the pool table with the pick.
        UPDATE flq_pool4 SET last_returned_address = free_address
            WHERE id = pool_id;
    END IF;

    RETURN free_address;
END $$
DELIMITER ;

-- Populate flq_pool6 and free_lease6 based on an address
-- range and delegated len. Use 128 for NA addresses.
DROP PROCEDURE IF EXISTS sflqCreateFlqPool6;
DELIMITER $$
CREATE PROCEDURE sflqCreateFlqPool6(IN p_start_address VARCHAR(45),
                                    IN p_end_address VARCHAR(45),
                                    IN p_lease_type TINYINT UNSIGNED,
                                    IN p_delegated_len TINYINT UNSIGNED,
                                    IN p_subnet_id INT UNSIGNED,
                                    IN p_recreate TINYINT)
BEGIN
    DECLARE bin_next_address BINARY(16);
    DECLARE bin_end_address BINARY(16);
    DECLARE lease_address VARCHAR(45);
    DECLARE error_msg VARCHAR(512);

    SET bin_next_address = INET6_ATON(p_start_address);
    SET bin_end_address = INET6_ATON(p_end_address);

    IF (p_start_address =  "::" OR
        bin_next_address > bin_end_address)
    THEN
        SET error_msg = CONCAT('Invalid range: ', p_start_address,
                               ' - ', p_end_address);
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = error_msg;
    END IF;

    IF (p_delegated_len < 1 OR p_delegated_len > 128)
    THEN
        SET error_msg = CONCAT('Invalid p_delegated_len: ', p_delegated_len);
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = error_msg;
    END IF;

    START TRANSACTION;

    -- Create the flq_pool6 row. Concurrent attempts will hang until
    -- this one commits and then they will fail with duplicate key
    -- error. Callers should treat the duplicate error as success.
    IF p_recreate = 1
    THEN
        -- (Re)creating should ignore duplicate on insert
        INSERT INTO flq_pool6
            (start_address, end_address, lease_type, delegated_len, subnet_id)
            VALUES
            (p_start_address, p_end_address, p_lease_type, p_delegated_len, p_subnet_id)
            ON DUPLICATE KEY UPDATE start_address = start_address;
    ELSE
        INSERT INTO flq_pool6
            (start_address, end_address, lease_type, delegated_len, subnet_id)
            VALUES
            (p_start_address, p_end_address, p_lease_type, p_delegated_len, p_subnet_id);
    END IF;

    -- Wipe out existing free addresses. This ensures we fix any mixed allocation entries.
    DELETE FROM free_lease6 WHERE bin_address >= bin_next_address and bin_address <= bin_end_address;

    -- Enumerate the addresses into a temp table to use in a bulk insert.
    DROP TEMPORARY TABLE IF EXISTS _flq6_candidates;
    CREATE TEMPORARY TABLE _flq6_candidates (
        bin_address BINARY(16) NOT NULL PRIMARY KEY,
        address VARCHAR(45) NOT NULL
    ) ENGINE=InnoDB;

    label: WHILE bin_next_address <= bin_end_address
    DO
        INSERT INTO _flq6_candidates (bin_address, address)
            VALUES (bin_next_address, INET6_NTOA(bin_next_address));

        IF (bin_next_address = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
        THEN
            LEAVE label;
        END IF;

        SET bin_next_address = incrementV6Prefix(bin_next_address, p_delegated_len);
    END WHILE label;

    -- Bulk insert available leases into free_lease6 table. If insert fails on
    -- duplicate ignore it.
    INSERT INTO free_lease6 (address, bin_address)
        SELECT c.address, c.bin_address
        FROM _flq6_candidates c
        WHERE NOT EXISTS (
            SELECT 1 FROM lease6 l
            WHERE l.address = c.address AND l.state != 2
        )
        ON DUPLICATE KEY UPDATE free_lease6.address = c.address;

    DROP TEMPORARY TABLE _flq6_candidates;

    -- Update the modification time in the flq_pool row.
    UPDATE flq_pool6 SET modification_ts = now()
        WHERE (start_address = p_start_address AND end_address = p_end_address);

    COMMIT;
END $$
DELIMITER ;

-- Find a free lease with an address range.
DROP FUNCTION IF EXISTS sflqPickFreeLease6;
DELIMITER $$
CREATE FUNCTION sflqPickFreeLease6(alloc_start_address VARCHAR(45),
                                   alloc_end_address VARCHAR(45))
RETURNS VARCHAR(45)
READS SQL DATA
BEGIN
    -- Declarations
    DECLARE pool_id BIGINT(20);
    DECLARE free_address VARCHAR(45);
    DECLARE bin_start_address BINARY(16);
    DECLARE bin_end_address BINARY(16);
    DECLARE bin_last_address BINARY(16);

    -- Find the pool, get the id and last addressed picked.
    SELECT id, INET6_ATON(last_returned_address) INTO pool_id, bin_last_address
        FROM flq_pool6
        WHERE start_address = alloc_start_address
        AND end_address = alloc_end_address;

    SET free_address = "::";
    IF (pool_id IS NULL)
    THEN
        -- No matching pool, return empty address.
        RETURN ("::");
    END IF;

    -- Convert to binary for range comparisons.
    SET bin_start_address = INET6_ATON(alloc_start_address);
    SET bin_end_address = INET6_ATON(alloc_end_address);

    IF (bin_last_address < bin_start_address OR bin_last_address >= alloc_end_address)
    THEN
        -- Set last pick to start address less one.
        SET bin_last_address = bin_start_address;
    END IF;

    -- Find first address greater than the last pick.
    SELECT f.address INTO free_address FROM free_lease6 f
        WHERE (f.bin_address > bin_last_address
               AND f.bin_address <= bin_end_address)
        ORDER BY f.bin_address
        LIMIT 1;

    -- Didn't find one, so try front half of the range, including
    -- the last pick. This avoids returning nothing if there is only
    -- one address free.
    IF (found_rows() = 0)
    THEN
        SELECT f.address INTO free_address FROM free_lease6 f
            WHERE (f.bin_address >= bin_start_address
                   AND f.bin_address <= bin_last_address)
            ORDER BY f.bin_address
            LIMIT 1;
    END IF;

    -- Update the pool table with the pick, even if there wasn't one.
    UPDATE flq_pool6 SET last_returned_address = free_address
            WHERE id = pool_id;

    RETURN free_address;
END $$
DELIMITER ;

-- Function to insert a v4 lease and update SFLQ data.
DROP FUNCTION IF EXISTS sflqInsertLease4;
DELIMITER $$
CREATE FUNCTION sflqInsertLease4(
    p_address INT UNSIGNED,
    p_hwaddr  VARBINARY(20),
    p_client_id VARBINARY(255),
    p_valid_lifetime INT UNSIGNED,
    p_expire TIMESTAMP,
    p_subnet_id INT UNSIGNED,
    p_fqdn_fwd TINYINT(1),
    p_fqdn_rev TINYINT(1),
    p_hostname VARCHAR(255),
    p_state INT UNSIGNED,
    p_user_context TEXT,
    p_relay_id VARBINARY(255),
    p_remote_id VARBINARY(255),
    p_pool_id INT UNSIGNED)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE lease_row_count INT;

    INSERT INTO lease4 (address, hwaddr, client_id, valid_lifetime, expire,
                        subnet_id, fqdn_fwd, fqdn_rev, hostname, state,
                        user_context, relay_id, remote_id, pool_id)
         VALUES ( p_address, p_hwaddr, p_client_id, p_valid_lifetime, p_expire,
                  p_subnet_id, p_fqdn_fwd, p_fqdn_rev, p_hostname, p_state,
                  p_user_context, p_relay_id, p_remote_id, p_pool_id);

    -- Remember the lease affected row count.
    SET lease_row_count = row_count();

    IF (lease_row_count > 0)
    THEN
        -- Update SFLQ data.
        -- If it is expired-reclaimed add it to the free lease queue
        -- otherwise delete it from the free lease queue.
        IF (p_state = 2)
        THEN
            -- Insert into free queue. Ignore duplicates.
            INSERT INTO free_lease4 (address) VALUES (p_address)
            ON DUPLICATE KEY UPDATE address = p_address;
        ELSE
            -- Delete it from the free queue. Ignore nothing deleted.
            DELETE FROM free_lease4 WHERE address = p_address;
        END IF;
    END IF;

    RETURN (lease_row_count);
END $$
DELIMITER ;

-- Procedure to update a v4 lease and update SFLQ data.
DROP FUNCTION IF EXISTS sflqUpdateLease4;
DELIMITER $$
CREATE FUNCTION sflqUpdateLease4(
    p_address INT UNSIGNED,
    p_hwaddr  VARBINARY(20),
    p_client_id VARBINARY(255),
    p_valid_lifetime INT UNSIGNED,
    p_expire TIMESTAMP,
    p_subnet_id INT UNSIGNED,
    p_fqdn_fwd TINYINT(1),
    p_fqdn_rev TINYINT(1),
    p_hostname VARCHAR(255),
    p_state INT UNSIGNED,
    p_user_context TEXT,
    p_relay_id VARBINARY(255),
    p_remote_id VARBINARY(255),
    p_pool_id INT UNSIGNED,
    p_where_address INT UNSIGNED,
    p_where_expire TIMESTAMP)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE lease_row_count INT;

    UPDATE lease4 SET address = p_address, hwaddr = p_hwaddr, client_id = p_client_id,
                      valid_lifetime = p_valid_lifetime, expire = p_expire,
                      subnet_id = p_subnet_id, fqdn_fwd = p_fqdn_fwd,
                      fqdn_rev = p_fqdn_rev, hostname = p_hostname, state = p_state,
                      user_context = p_user_context, relay_id = p_relay_id,
                      remote_id = p_remote_id, pool_id = p_pool_id
        WHERE (address = p_where_address and expire = p_where_expire);

    -- Remember the lease affected row count.
    SET lease_row_count = row_count();

    IF (lease_row_count > 0)
    THEN
        -- Update SFLQ data.
        -- If it is expired-reclaimed add it to the free lease queue
        -- otherwise delete it from the free lease queue.
        IF (p_state = 2)
        THEN
            -- Insert into free queue. Ignore duplicates.
            INSERT INTO free_lease4 (address) VALUES (p_address)
                ON DUPLICATE KEY UPDATE address = p_address;
        ELSE
            -- Delete it from the free queue. Ignore nothing deleted.
            DELETE FROM free_lease4 WHERE address = p_address;
        END IF;
    END IF;

    RETURN (lease_row_count);
END $$
DELIMITER ;

-- Function  to delete a v4 lease by address and update SFLQ data.
DROP FUNCTION IF EXISTS sflqDeleteLease4;
DELIMITER $$
CREATE FUNCTION sflqDeleteLease4(
    p_address INT UNSIGNED,
    p_expire TIMESTAMP)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE lease_row_count INT;

    -- Delete the lease from lease4.
    DELETE FROM lease4 WHERE address = p_address AND expire = p_expire;

    -- Remember the lease affected row count.
    SET lease_row_count = row_count();

    -- If we didn't delete it either it was already gone or expire
    -- didn't match. In case of the former it should already be in
    -- the flq and in the latter, it should not be.
    IF (row_count() > 0)
    THEN
        INSERT INTO free_lease4 (address) VALUES (p_address)
        ON DUPLICATE KEY UPDATE address = p_address;
    END IF;

    RETURN (lease_row_count);
END $$
DELIMITER ;

-- Function to insert a v6 lease and update SFLQ data.
DROP FUNCTION IF EXISTS sflqInsertLease6;
DELIMITER $$
CREATE FUNCTION sflqInsertLease6(
    p_address BINARY(16),
    p_duid VARBINARY(128),
    p_valid_lifetime INT UNSIGNED,
    p_expire TIMESTAMP,
    p_subnet_id INT UNSIGNED,
    p_pref_lifetime INT UNSIGNED,
    p_lease_type TINYINT,
    p_iaid INT UNSIGNED,
    p_prefix_len TINYINT UNSIGNED,
    p_fqdn_fwd TINYINT(1),
    p_fqdn_rev TINYINT(1),
    p_hostname VARCHAR(255),
    p_hwaddr VARBINARY(20),
    p_hwtype SMALLINT UNSIGNED,
    p_hwaddr_source INT UNSIGNED,
    p_state INT UNSIGNED,
    p_user_context TEXT,
    p_pool_id INT UNSIGNED)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE lease_row_count INT;

    INSERT INTO lease6 (address, duid, valid_lifetime, expire, subnet_id,
                        pref_lifetime, lease_type, iaid, prefix_len,
                        fqdn_fwd, fqdn_rev, hostname, hwaddr, hwtype,
                        hwaddr_source, state, user_context, pool_id)
         VALUES (p_address, p_duid, p_valid_lifetime, p_expire, p_subnet_id,
                 p_pref_lifetime, p_lease_type, p_iaid, p_prefix_len,
                 p_fqdn_fwd, p_fqdn_rev, p_hostname, p_hwaddr, p_hwtype,
                 p_hwaddr_source, p_state, p_user_context, p_pool_id);

    -- Remember the lease affected row count.
    SET lease_row_count = row_count();

    IF (lease_row_count > 0)
    THEN
        -- Update SFLQ data.
        -- If it is expired-reclaimed add it to the free lease queue
        -- otherwise delete it from the free lease queue.
        IF (p_state = 2)
        THEN
            -- Insert into free queue. Ignore duplicates.
            INSERT INTO free_lease6 (address, bin_address)
                VALUES (INET6_NTOA(p_address), p_address)
            ON DUPLICATE KEY UPDATE bin_address = p_address;
        ELSE
            -- Delete it from the free queue. Ignore nothing deleted.
            DELETE FROM free_lease6 WHERE bin_address = p_address;
        END IF;
    END IF;

    RETURN (lease_row_count);
END $$
DELIMITER ;

-- Function to update a v6 lease and update SFLQ data.
DROP FUNCTION IF EXISTS sflqUpdateLease6;
DELIMITER $$
CREATE FUNCTION sflqUpdateLease6(
    p_address BINARY(16),
    p_duid VARBINARY(128),
    p_valid_lifetime INT UNSIGNED,
    p_expire TIMESTAMP,
    p_subnet_id INT UNSIGNED,
    p_pref_lifetime INT UNSIGNED,
    p_lease_type TINYINT,
    p_iaid INT UNSIGNED,
    p_prefix_len TINYINT UNSIGNED,
    p_fqdn_fwd TINYINT(1),
    p_fqdn_rev TINYINT(1),
    p_hostname VARCHAR(255),
    p_hwaddr VARBINARY(20),
    p_hwtype SMALLINT UNSIGNED,
    p_hwaddr_source INT UNSIGNED,
    p_state INT UNSIGNED,
    p_user_context TEXT,
    p_pool_id INT UNSIGNED,
    p_where_address BINARY(16),
    p_where_expire TIMESTAMP)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE lease_row_count INT;

    UPDATE lease6 SET address = p_address, duid = p_duid, valid_lifetime = p_valid_lifetime,
                      expire = p_expire, subnet_id = p_subnet_id, pref_lifetime = p_pref_lifetime,
                      lease_type = p_lease_type, iaid = p_iaid, prefix_len = p_prefix_len,
                      fqdn_fwd = p_fqdn_fwd, fqdn_rev = p_fqdn_rev, hostname = p_hostname,
                      hwaddr = p_hwaddr, hwtype = p_hwtype, hwaddr_source = p_hwaddr_source,
                      state = p_state, user_context = p_user_context, pool_id = p_pool_id
        WHERE (address = p_where_address and expire = p_where_expire);

    -- Remember the lease affected row count.
    SET lease_row_count = row_count();

    IF (lease_row_count > 0)
    THEN
        -- Update SFLQ data.
        -- If it is expired-reclaimed add it to the free lease queue
        -- otherwise delete it from the free lease queue.
        IF (p_state = 2)
        THEN
            -- Insert into free queue. Ignore duplicates.
            INSERT INTO free_lease6 (address, bin_address)
                VALUES (INET6_NTOA(p_address), p_address)
            ON DUPLICATE KEY UPDATE bin_address = p_address;
        ELSE
            -- Delete it from the free queue. Ignore nothing deleted.
            DELETE FROM free_lease6 WHERE bin_address = p_address;
        END IF;
    END IF;

    RETURN (lease_row_count);
END $$
DELIMITER ;

-- Function to delete a v6 lease by address and update SFLQ data.
DROP FUNCTION IF EXISTS sflqDeleteLease6;
DELIMITER $$
CREATE FUNCTION sflqDeleteLease6(
    p_address BINARY(16),
    p_expire TIMESTAMP)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE lease_row_count INT;

    -- Delete the lease from lease4.
    DELETE FROM lease6 WHERE address = p_address AND expire = p_expire;

    -- Remember the lease affected row count.
    SET lease_row_count = row_count();

    -- If we didn't delete it either it was already gone or expire
    -- didn't match. In case of the former it should already be in
    -- the flq and in the latter, it should not be.
    IF (row_count() > 0)
    THEN
        -- Insert the lease into the free queue. Ignore duplicates.
        INSERT INTO free_lease6 (address, bin_address)
            VALUES (INET6_NTOA(p_address), p_address)
        ON DUPLICATE KEY UPDATE bin_address = p_address;
    END IF;

    RETURN (lease_row_count);
END $$
DELIMITER ;

-- Update the schema version number.
UPDATE schema_version
    SET version = '34', minor = '0';

-- This line concludes the schema upgrade to version 34.0.

EOF
