Files
zoneminder/db/zm_update-1.39.3.sql
T
Isaac ConnorandClaude Opus 4.6 34f9b1e490 fix: convert zone threshold columns to DECIMAL before running conversion procedure
The migration procedure computed correct sub-1.0 percentage values
(e.g. 3000 pixels → 0.26%) but the ALTER TABLE that changed the
columns from INT to DECIMAL(10,2) ran *after* the conversion. MySQL
silently truncated values like 0.26 to 0 when storing into INT
columns, zeroing out MinAlarmPixels, MinFilterPixels, MinBlobPixels,
MaxAlarmPixels, MaxFilterPixels, and MaxBlobPixels for all zones.

Move the ALTER TABLE statements before the CALL so the columns can
hold decimal values when the UPDATE writes them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:31:04 -04:00

318 lines
10 KiB
SQL

--
-- Add Profile column to Notifications table
--
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'Notifications'
AND COLUMN_NAME = 'Profile'
) > 0,
"SELECT 'Column Profile already exists in Notifications'",
"ALTER TABLE `Notifications` ADD `Profile` varchar(128) DEFAULT NULL AFTER `AppVersion`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Convert zone threshold fields (MinAlarmPixels, etc.) from pixel counts
-- to percentages of zone area, matching the coordinate percentage migration
-- done in zm_update-1.39.2.sql.
--
-- The Zones.Area column is often 0 (never populated, or not used by the C++
-- daemon which calculates polygon area at runtime from coordinates). So we
-- compute the polygon area directly from the percentage coordinates using the
-- shoelace formula, then derive the pixel area to convert thresholds.
--
-- new_percent = old_pixel_count * 100 / zone_pixel_area
-- zone_pixel_area = pct_poly_area * monitor_width * monitor_height / 10000
--
-- Only converts zones with percentage coordinates (contain '.') that still
-- have pixel-scale threshold values (any value > 100 can't be a percentage).
DELIMITER //
DROP PROCEDURE IF EXISTS `zm_convert_zone_thresholds_to_percent` //
CREATE PROCEDURE `zm_convert_zone_thresholds_to_percent`()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_zone_id INT;
DECLARE v_coords TEXT;
DECLARE v_mon_width INT;
DECLARE v_mon_height INT;
DECLARE v_min_alarm, v_max_alarm DOUBLE;
DECLARE v_min_filter, v_max_filter DOUBLE;
DECLARE v_min_blob, v_max_blob DOUBLE;
DECLARE v_remaining TEXT;
DECLARE v_pair TEXT;
DECLARE v_space_pos INT;
DECLARE v_x, v_y DOUBLE;
DECLARE v_prev_x, v_prev_y DOUBLE;
DECLARE v_first_x, v_first_y DOUBLE;
DECLARE v_pct_area DOUBLE;
DECLARE v_pixel_area DOUBLE;
DECLARE v_have_first INT DEFAULT FALSE;
DECLARE cur CURSOR FOR
SELECT z.Id, z.Coords, m.Width, m.Height,
z.MinAlarmPixels, z.MaxAlarmPixels,
z.MinFilterPixels, z.MaxFilterPixels,
z.MinBlobPixels, z.MaxBlobPixels
FROM Zones z
JOIN Monitors m ON z.MonitorId = m.Id
WHERE z.Coords LIKE '%.%'
AND m.Width > 0 AND m.Height > 0
AND (z.MinAlarmPixels > 100 OR z.MaxAlarmPixels > 100
OR z.MinFilterPixels > 100 OR z.MaxFilterPixels > 100
OR z.MinBlobPixels > 100 OR z.MaxBlobPixels > 100);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_zone_id, v_coords, v_mon_width, v_mon_height,
v_min_alarm, v_max_alarm,
v_min_filter, v_max_filter,
v_min_blob, v_max_blob;
IF done THEN
LEAVE read_loop;
END IF;
-- Calculate polygon area from percentage coordinates (shoelace formula)
SET v_pct_area = 0.0;
SET v_have_first = FALSE;
SET v_remaining = TRIM(v_coords);
coord_loop: LOOP
IF v_remaining = '' OR v_remaining IS NULL THEN
LEAVE coord_loop;
END IF;
SET v_space_pos = LOCATE(' ', v_remaining);
IF v_space_pos > 0 THEN
SET v_pair = LEFT(v_remaining, v_space_pos - 1);
SET v_remaining = TRIM(SUBSTRING(v_remaining, v_space_pos + 1));
ELSE
SET v_pair = v_remaining;
SET v_remaining = '';
END IF;
IF v_pair = '' OR v_pair = ',' THEN
ITERATE coord_loop;
END IF;
SET v_x = CAST(SUBSTRING_INDEX(v_pair, ',', 1) AS DECIMAL(10,2));
SET v_y = CAST(SUBSTRING_INDEX(v_pair, ',', -1) AS DECIMAL(10,2));
IF NOT v_have_first THEN
SET v_first_x = v_x;
SET v_first_y = v_y;
SET v_have_first = TRUE;
ELSE
SET v_pct_area = v_pct_area + (v_prev_x * v_y - v_x * v_prev_y);
END IF;
SET v_prev_x = v_x;
SET v_prev_y = v_y;
END LOOP coord_loop;
-- Close polygon
IF v_have_first THEN
SET v_pct_area = v_pct_area + (v_prev_x * v_first_y - v_first_x * v_prev_y);
END IF;
SET v_pct_area = ABS(v_pct_area) / 2.0;
-- Convert percentage-space area to pixel area
-- pct coords are 0-100, so full frame = 100*100 = 10000
SET v_pixel_area = v_pct_area * v_mon_width * v_mon_height / 10000.0;
IF v_pixel_area > 0 THEN
-- Convert each threshold: only if > 100 (still a pixel count)
IF v_min_alarm IS NOT NULL AND v_min_alarm > 100 THEN
SET v_min_alarm = LEAST(ROUND(v_min_alarm * 100.0 / v_pixel_area, 2), 100);
END IF;
IF v_max_alarm IS NOT NULL AND v_max_alarm > 100 THEN
SET v_max_alarm = LEAST(ROUND(v_max_alarm * 100.0 / v_pixel_area, 2), 100);
END IF;
IF v_min_filter IS NOT NULL AND v_min_filter > 100 THEN
SET v_min_filter = LEAST(ROUND(v_min_filter * 100.0 / v_pixel_area, 2), 100);
END IF;
IF v_max_filter IS NOT NULL AND v_max_filter > 100 THEN
SET v_max_filter = LEAST(ROUND(v_max_filter * 100.0 / v_pixel_area, 2), 100);
END IF;
IF v_min_blob IS NOT NULL AND v_min_blob > 100 THEN
SET v_min_blob = LEAST(ROUND(v_min_blob * 100.0 / v_pixel_area, 2), 100);
END IF;
IF v_max_blob IS NOT NULL AND v_max_blob > 100 THEN
SET v_max_blob = LEAST(ROUND(v_max_blob * 100.0 / v_pixel_area, 2), 100);
END IF;
UPDATE Zones SET
MinAlarmPixels = v_min_alarm,
MaxAlarmPixels = v_max_alarm,
MinFilterPixels = v_min_filter,
MaxFilterPixels = v_max_filter,
MinBlobPixels = v_min_blob,
MaxBlobPixels = v_max_blob
WHERE Id = v_zone_id;
END IF;
END LOOP read_loop;
CLOSE cur;
END //
DELIMITER ;
-- Change threshold columns to DECIMAL(10,2) BEFORE the conversion procedure
-- runs, so that sub-1.0 percentage values (e.g. 0.26) are not truncated to 0
-- by INT storage.
SET @s = (SELECT IF(
(SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Zones' AND column_name = 'MinAlarmPixels'
) = 'decimal',
"SELECT 'Zones threshold columns already DECIMAL'",
"ALTER TABLE `Zones`
MODIFY `MinAlarmPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MaxAlarmPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MinFilterPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MaxFilterPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MinBlobPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MaxBlobPixels` DECIMAL(10,2) unsigned default NULL"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Also update ZonePresets table column types for consistency
SET @s = (SELECT IF(
(SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'ZonePresets' AND column_name = 'MinAlarmPixels'
) = 'decimal',
"SELECT 'ZonePresets threshold columns already DECIMAL'",
"ALTER TABLE `ZonePresets`
MODIFY `MinAlarmPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MaxAlarmPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MinFilterPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MaxFilterPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MinBlobPixels` DECIMAL(10,2) unsigned default NULL,
MODIFY `MaxBlobPixels` DECIMAL(10,2) unsigned default NULL"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Now convert the data (columns are DECIMAL, so sub-1.0 values survive)
CALL zm_convert_zone_thresholds_to_percent();
DROP PROCEDURE IF EXISTS `zm_convert_zone_thresholds_to_percent`;
--
-- Add Menu_Items table for customizable navbar/sidebar menu
--
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE()
AND table_name = 'Menu_Items'
) > 0,
"SELECT 'Table Menu_Items already exists'",
"CREATE TABLE `Menu_Items` (
`Id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`MenuKey` varchar(32) NOT NULL,
`Enabled` tinyint(1) NOT NULL DEFAULT 1,
`Label` varchar(64) DEFAULT NULL,
`SortOrder` smallint NOT NULL DEFAULT 0,
`Icon` varchar(128) DEFAULT NULL,
`IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material',
PRIMARY KEY (`Id`),
UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`)
) ENGINE=InnoDB"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
--
-- Seed default menu items if table is empty
--
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM `Menu_Items`) > 0,
"SELECT 'Menu_Items already has data'",
"INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
('Console', 1, 10),
('Watch', 1, 15),
('Montage', 1, 20),
('MontageReview', 1, 30),
('Events', 1, 40),
('Options', 1, 50),
('Log', 1, 60),
('Devices', 1, 70),
('IntelGpu', 1, 80),
('Groups', 1, 90),
('Filters', 1, 100),
('Snapshots', 1, 110),
('Reports', 1, 120),
('ReportEventAudit', 1, 130),
('Map', 1, 140)"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
--
-- Ensure all expected menu items exist (adds any missing entries)
--
INSERT IGNORE INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
('Console', 1, 10),
('Watch', 1, 15),
('Montage', 1, 20),
('MontageReview', 1, 30),
('Events', 1, 40),
('Options', 1, 50),
('Log', 1, 60),
('Devices', 1, 70),
('IntelGpu', 1, 80),
('Groups', 1, 90),
('Filters', 1, 100),
('Snapshots', 1, 110),
('Reports', 1, 120),
('ReportEventAudit', 1, 130),
('Map', 1, 140);
--
-- Add Icon and IconType columns if they don't exist
--
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Menu_Items' AND column_name = 'Icon'
) > 0,
"SELECT 'Column Icon already exists'",
"ALTER TABLE `Menu_Items` ADD `Icon` varchar(128) DEFAULT NULL AFTER `SortOrder`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Menu_Items' AND column_name = 'IconType'
) > 0,
"SELECT 'Column IconType already exists'",
"ALTER TABLE `Menu_Items` ADD `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material' AFTER `Icon`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;