From 25b68f0c15bfe4b092aaa3992435230711b965b5 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 26 Jul 2026 15:36:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8A=A0=E5=9B=BA=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E8=BF=81=E7=A7=BB=E5=AE=89=E5=85=A8=E9=97=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/network-tcp-natmap-v2.sql | 319 ++++++++++++++---- .../network-tcp-natmap-v2-sql.spec.ts | 214 +++++++++++- 2 files changed, 460 insertions(+), 73 deletions(-) diff --git a/sql/network-tcp-natmap-v2.sql b/sql/network-tcp-natmap-v2.sql index fbaff17..7288bef 100644 --- a/sql/network-tcp-natmap-v2.sql +++ b/sql/network-tcp-natmap-v2.sql @@ -72,66 +72,243 @@ PREPARE network_port_forward_natmap_status_stmt FROM @network_port_forward_natma EXECUTE network_port_forward_natmap_status_stmt; DEALLOCATE PREPARE network_port_forward_natmap_status_stmt; -SET @network_port_forward_runtime_columns_sql := ( - SELECT GROUP_CONCAT(CONCAT( - 'ADD COLUMN `', column_name, '` ', column_definition, ' ', column_position - ) SEPARATOR ', ') - FROM ( - SELECT 'natmap_last_error_code' AS column_name, 'VARCHAR(64) NULL' AS column_definition, 'AFTER `last_error_message`' AS column_position UNION ALL - SELECT 'natmap_last_error_message', 'VARCHAR(512) NULL', 'AFTER `last_error_message`' UNION ALL - SELECT 'keeper_last_error_code', 'VARCHAR(64) NULL', 'AFTER `last_error_message`' UNION ALL - SELECT 'keeper_last_error_message', 'VARCHAR(512) NULL', 'AFTER `last_error_message`' UNION ALL - SELECT 'candidate_public_ipv4', 'VARCHAR(15) NULL', 'AFTER `current_public_port`' UNION ALL - SELECT 'candidate_public_port', 'INT NULL', 'AFTER `current_public_port`' UNION ALL - SELECT 'candidate_observed_at', 'DATETIME(6) NULL', 'AFTER `current_public_port`' UNION ALL - SELECT 'candidate_validated_at', 'DATETIME(6) NULL', 'AFTER `current_public_port`' UNION ALL - SELECT 'current_validated_at', 'DATETIME(6) NULL', 'AFTER `current_observed_at`' UNION ALL - SELECT 'last_observed_validated_at', 'DATETIME(6) NULL', 'AFTER `last_observed_at`' UNION ALL - SELECT 'last_published_public_ipv4', 'VARCHAR(15) NULL', 'AFTER `last_observed_at`' UNION ALL - SELECT 'last_published_public_port', 'INT NULL', 'AFTER `last_observed_at`' UNION ALL - SELECT 'last_published_at', 'DATETIME(6) NULL', 'AFTER `last_observed_at`' - ) expected - WHERE NOT EXISTS ( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS actual - WHERE actual.TABLE_SCHEMA = DATABASE() - AND actual.TABLE_NAME = 'network_port_forward' - AND actual.COLUMN_NAME = expected.column_name - ) +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'natmap_last_error_code' ); -SET @network_port_forward_runtime_columns_sql := IF( - @network_port_forward_runtime_columns_sql IS NULL, - 'SELECT 1', - CONCAT('ALTER TABLE `network_port_forward` ', @network_port_forward_runtime_columns_sql) +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `natmap_last_error_code` VARCHAR(64) NULL AFTER `last_error_message`', + 'SELECT 1' ); -PREPARE network_port_forward_runtime_columns_stmt FROM @network_port_forward_runtime_columns_sql; -EXECUTE network_port_forward_runtime_columns_stmt; -DEALLOCATE PREPARE network_port_forward_runtime_columns_stmt; +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; -SET @network_agent_state_v2_columns_sql := ( - SELECT GROUP_CONCAT(CONCAT( - 'ADD COLUMN `', column_name, '` ', column_definition, ' ', column_position - ) SEPARATOR ', ') - FROM ( - SELECT 'desired_schema_version' AS column_name, 'INT UNSIGNED NOT NULL DEFAULT 1' AS column_definition, 'AFTER `desired_revision`' AS column_position UNION ALL - SELECT 'published_schema_version', 'INT UNSIGNED NOT NULL DEFAULT 1', 'AFTER `published_revision`' UNION ALL - SELECT 'max_supported_schema_version', 'INT UNSIGNED NOT NULL DEFAULT 1', 'AFTER `published_revision`' UNION ALL - SELECT 'tcp_natmap_capable', 'TINYINT(1) NOT NULL DEFAULT 0', 'AFTER `published_revision`' - ) expected - WHERE NOT EXISTS ( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS actual - WHERE actual.TABLE_SCHEMA = DATABASE() - AND actual.TABLE_NAME = 'network_agent_state' - AND actual.COLUMN_NAME = expected.column_name - ) +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'natmap_last_error_message' ); -SET @network_agent_state_v2_columns_sql := IF( - @network_agent_state_v2_columns_sql IS NULL, - 'SELECT 1', - CONCAT('ALTER TABLE `network_agent_state` ', @network_agent_state_v2_columns_sql) +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `natmap_last_error_message` VARCHAR(512) NULL AFTER `natmap_last_error_code`', + 'SELECT 1' ); -PREPARE network_agent_state_v2_columns_stmt FROM @network_agent_state_v2_columns_sql; -EXECUTE network_agent_state_v2_columns_stmt; -DEALLOCATE PREPARE network_agent_state_v2_columns_stmt; +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'keeper_last_error_code' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `keeper_last_error_code` VARCHAR(64) NULL AFTER `last_error_message`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'keeper_last_error_message' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `keeper_last_error_message` VARCHAR(512) NULL AFTER `keeper_last_error_code`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'candidate_public_ipv4' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `candidate_public_ipv4` VARCHAR(15) NULL AFTER `current_public_port`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'candidate_public_port' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `candidate_public_port` INT NULL AFTER `candidate_public_ipv4`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'candidate_observed_at' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `candidate_observed_at` DATETIME(6) NULL AFTER `candidate_public_port`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'candidate_validated_at' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `candidate_validated_at` DATETIME(6) NULL AFTER `candidate_observed_at`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'current_validated_at' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `current_validated_at` DATETIME(6) NULL AFTER `current_observed_at`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'last_observed_validated_at' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `last_observed_validated_at` DATETIME(6) NULL AFTER `last_observed_at`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'last_published_public_ipv4' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `last_published_public_ipv4` VARCHAR(15) NULL AFTER `last_observed_validated_at`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'last_published_public_port' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `last_published_public_port` INT NULL AFTER `last_published_public_ipv4`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' + AND COLUMN_NAME = 'last_published_at' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_port_forward` ADD COLUMN `last_published_at` DATETIME(6) NULL AFTER `last_published_public_port`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_agent_state' + AND COLUMN_NAME = 'desired_schema_version' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_agent_state` ADD COLUMN `desired_schema_version` INT UNSIGNED NOT NULL DEFAULT 1 AFTER `desired_revision`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_agent_state' + AND COLUMN_NAME = 'published_schema_version' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_agent_state` ADD COLUMN `published_schema_version` INT UNSIGNED NOT NULL DEFAULT 1 AFTER `published_revision`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_agent_state' + AND COLUMN_NAME = 'max_supported_schema_version' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_agent_state` ADD COLUMN `max_supported_schema_version` INT UNSIGNED NOT NULL DEFAULT 1 AFTER `published_schema_version`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; + +SET @network_tcp_natmap_v2_column_exists := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_agent_state' + AND COLUMN_NAME = 'tcp_natmap_capable' +); +SET @network_tcp_natmap_v2_alter_sql := IF( + @network_tcp_natmap_v2_column_exists = 0, + 'ALTER TABLE `network_agent_state` ADD COLUMN `tcp_natmap_capable` TINYINT(1) NOT NULL DEFAULT 0 AFTER `max_supported_schema_version`', + 'SELECT 1' +); +PREPARE network_tcp_natmap_v2_alter_stmt FROM @network_tcp_natmap_v2_alter_sql; +EXECUTE network_tcp_natmap_v2_alter_stmt; +DEALLOCATE PREPARE network_tcp_natmap_v2_alter_stmt; SET @network_endpoint_history_mechanism_exists := ( SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS @@ -167,8 +344,12 @@ SET `active_group_protocol_key` = CASE WHEN channel.is_deleted = 0 THEN CONCAT(channel.group_id, ':', channel.protocol) ELSE NULL END -WHERE channel.active_group_protocol_key IS NULL - OR channel.is_deleted <> 0; +WHERE NOT ( + channel.active_group_protocol_key <=> CASE + WHEN channel.is_deleted = 0 THEN CONCAT(channel.group_id, ':', channel.protocol) + ELSE NULL + END +); UPDATE `network_endpoint_history` SET `mechanism` = 'udp_stun' @@ -192,14 +373,22 @@ SET @network_port_forward_group_id_null_count := ( SELECT COUNT(*) FROM `network_port_forward` WHERE `group_id` IS NULL ); SELECT @network_port_forward_group_id_null_count AS network_port_forward_group_id_null_count; +SET @network_port_forward_group_id_abort_sql := IF( + @network_port_forward_group_id_null_count = 0, + 'SELECT 1', + 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''network_port_forward.group_id backfill incomplete''' +); +PREPARE network_port_forward_group_id_abort_stmt FROM @network_port_forward_group_id_abort_sql; +EXECUTE network_port_forward_group_id_abort_stmt; +DEALLOCATE PREPARE network_port_forward_group_id_abort_stmt; + SET @network_port_forward_group_id_nullable := ( SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' AND COLUMN_NAME = 'group_id' AND IS_NULLABLE = 'YES' ); SET @network_port_forward_group_id_not_null_sql := IF( - @network_port_forward_group_id_null_count = 0 - AND @network_port_forward_group_id_nullable = 1, + @network_port_forward_group_id_nullable = 1, 'ALTER TABLE `network_port_forward` MODIFY COLUMN `group_id` BIGINT NOT NULL', 'SELECT 1' ); @@ -217,14 +406,22 @@ SET @network_port_forward_active_group_protocol_key_conflicts := ( ) conflicts ); SELECT @network_port_forward_active_group_protocol_key_conflicts AS network_port_forward_active_group_protocol_key_conflicts; +SET @network_port_forward_active_group_protocol_key_abort_sql := IF( + @network_port_forward_active_group_protocol_key_conflicts = 0, + 'SELECT 1', + 'SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''network_port_forward active group/protocol key conflict''' +); +PREPARE network_port_forward_active_group_protocol_key_abort_stmt FROM @network_port_forward_active_group_protocol_key_abort_sql; +EXECUTE network_port_forward_active_group_protocol_key_abort_stmt; +DEALLOCATE PREPARE network_port_forward_active_group_protocol_key_abort_stmt; + SET @network_port_forward_active_group_protocol_key_index_exists := ( SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'network_port_forward' AND INDEX_NAME = 'uk_network_port_forward_active_group_protocol_key' ); SET @network_port_forward_active_group_protocol_key_index_sql := IF( - @network_port_forward_active_group_protocol_key_conflicts = 0 - AND @network_port_forward_active_group_protocol_key_index_exists = 0, + @network_port_forward_active_group_protocol_key_index_exists = 0, 'ALTER TABLE `network_port_forward` ADD UNIQUE KEY `uk_network_port_forward_active_group_protocol_key` (`active_group_protocol_key`)', 'SELECT 1' ); diff --git a/test/admin/network-management/network-tcp-natmap-v2-sql.spec.ts b/test/admin/network-management/network-tcp-natmap-v2-sql.spec.ts index 14f311e..fa4a921 100644 --- a/test/admin/network-management/network-tcp-natmap-v2-sql.spec.ts +++ b/test/admin/network-management/network-tcp-natmap-v2-sql.spec.ts @@ -22,6 +22,102 @@ function extractCreateTable(sql: string, tableName: string): string { return match?.[1] ?? ''; } +function splitSqlList(value: string): string[] { + const parts: string[] = []; + let depth = 0; + let quoted = false; + let start = 0; + + for (let index = 0; index < value.length; index += 1) { + const character = value[index]; + if (character === "'") { + if (quoted && value[index + 1] === "'") { + index += 1; + } else { + quoted = !quoted; + } + } else if (!quoted && character === '(') { + depth += 1; + } else if (!quoted && character === ')') { + depth -= 1; + } else if (!quoted && depth === 0 && character === ',') { + parts.push(value.slice(start, index).trim()); + start = index + 1; + } + } + parts.push(value.slice(start).trim()); + return parts.filter(Boolean); +} + +function canonicalTableMetadata(sql: string, tableName: string): string[] { + return splitSqlList(extractCreateTable(sql, tableName)) + .flatMap((clause) => { + const inlinePrimaryKey = clause.match( + /^([a-z0-9_]+) ([\s\S]+) primary key$/, + ); + if (!inlinePrimaryKey) return [clause]; + return [ + `${inlinePrimaryKey[1]} ${inlinePrimaryKey[2]}`, + `primary key (${inlinePrimaryKey[1]})`, + ]; + }) + .sort(); +} + +type FixtureRow = Record; + +function parseFixtureRows(sql: string): Record { + const rows: Record = {}; + const normalized = sql + .replace(/--.*$/gm, '') + .replace(/`/g, '') + .trim(); + const inserts = normalized.matchAll( + /insert into ([a-z0-9_]+)\s*\(([\s\S]*?)\)\s*values\s*\(([\s\S]*?)\);/gi, + ); + + for (const insert of inserts) { + const columns = splitSqlList(insert[2]).map((column) => column.trim()); + const values = splitSqlList(insert[3]).map((value) => { + const trimmed = value.trim(); + if (trimmed.toLowerCase() === 'null') return null; + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1).replace(/''/g, "'"); + } + return trimmed; + }); + expect(values).toHaveLength(columns.length); + rows[insert[1].toLowerCase()] = Object.fromEntries( + columns.map((column, index) => [column, values[index]]), + ); + } + return rows; +} + +function mutationAssignmentTargets(sql: string, tableName: string): string[] { + const targets = new Set(); + const updates = sql.matchAll( + new RegExp( + `update ${tableName}(?: [a-z0-9_]+)? set ([\\s\\S]*?) where [\\s\\S]*?;`, + 'g', + ), + ); + for (const update of updates) { + for (const assignment of splitSqlList(update[1])) { + const target = assignment.match(/^(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*=/); + expect(target).not.toBeNull(); + if (target) targets.add(target[1]); + } + } + return [...targets].sort(); +} + +function withoutProperties(row: FixtureRow, properties: string[]): FixtureRow { + return Object.fromEntries( + Object.entries(row).filter(([key]) => !properties.includes(key)), + ); +} + function expectFinalSchema(sql: string): void { const group = extractCreateTable(sql, 'network_port_forward_group'); const channel = extractCreateTable(sql, 'network_port_forward'); @@ -84,8 +180,20 @@ function expectFinalSchema(sql: string): void { describe('TCP NATMap v2 schema SQL', () => { it('keeps clean init and refactor-v3 schema contracts aligned', () => { - expectFinalSchema(readNormalizedSql('sql/network-management-init.sql')); - expectFinalSchema(readNormalizedSql('sql/refactor-v3/00-full-schema.sql')); + const cleanInit = readNormalizedSql('sql/network-management-init.sql'); + const refactor = readNormalizedSql('sql/refactor-v3/00-full-schema.sql'); + + expectFinalSchema(cleanInit); + for (const tableName of [ + 'network_port_forward_group', + 'network_port_forward', + 'network_agent_state', + 'network_endpoint_history', + ]) { + expect(canonicalTableMetadata(cleanInit, tableName)).toEqual( + canonicalTableMetadata(refactor, tableName), + ); + } }); it('uses an additive idempotent migration with ordered group backfill', () => { @@ -103,10 +211,53 @@ describe('TCP NATMap v2 schema SQL', () => { ); expect(migration).toContain('network_port_forward_group_id_null_count'); expect(migration).toContain('modify column group_id bigint not null'); + expect(migration).not.toContain('group_concat'); + for (const columnName of [ + 'natmap_last_error_code', + 'natmap_last_error_message', + 'keeper_last_error_code', + 'keeper_last_error_message', + 'candidate_public_ipv4', + 'candidate_public_port', + 'candidate_observed_at', + 'candidate_validated_at', + 'current_validated_at', + 'last_observed_validated_at', + 'last_published_public_ipv4', + 'last_published_public_port', + 'last_published_at', + 'desired_schema_version', + 'published_schema_version', + 'max_supported_schema_version', + 'tcp_natmap_capable', + ]) { + expect(migration).toContain(`column_name = '${columnName}'`); + } + + const nullGroupAbort = migration.indexOf( + "signal sqlstate ''45000'' set message_text = ''network_port_forward.group_id backfill incomplete''", + ); + const groupNotNull = migration.indexOf( + 'modify column group_id bigint not null', + ); + const activeKeyAbort = migration.indexOf( + "signal sqlstate ''45000'' set message_text = ''network_port_forward active group/protocol key conflict''", + ); + const activeKeyUnique = migration.indexOf( + 'add unique key uk_network_port_forward_active_group_protocol_key', + ); + + expect(nullGroupAbort).toBeGreaterThan(-1); + expect(activeKeyAbort).toBeGreaterThan(-1); + expect(nullGroupAbort).toBeLessThan(groupNotNull); + expect(activeKeyAbort).toBeLessThan(activeKeyUnique); + expect(migration).toMatch( + /where not \( ?channel\.active_group_protocol_key <=> case when channel\.is_deleted = 0 then concat\(channel\.group_id, ':', channel\.protocol\) else null end ?\)/, + ); expect( migration.indexOf('network_port_forward_group_id_null_count'), - ).toBeLessThan(migration.indexOf('modify column group_id bigint not null')); + ).toBeLessThan(groupNotNull); expect(migration).not.toMatch(/\b(drop|truncate|delete)\b/); expect(migration).not.toMatch(/\b(rename\s+(table|column))\b/); expect(migration).not.toMatch(/foreign\s+key/); @@ -126,17 +277,56 @@ describe('TCP NATMap v2 schema SQL', () => { }); it('preserves the sanitized existing 8213 UDP channel, history, and DDNS binding', () => { - const fixture = readNormalizedSql( - 'test/admin/network-management/fixtures/network-tcp-natmap-v2-existing-udp.sql', + const fixturePath = + 'test/admin/network-management/fixtures/network-tcp-natmap-v2-existing-udp.sql'; + const fixture = readNormalizedSql(fixturePath); + const migration = readNormalizedSql('sql/network-tcp-natmap-v2.sql'); + const before = parseFixtureRows( + readFileSync(resolve(REPO_ROOT, fixturePath), 'utf8'), ); + const beforeChannel = before.network_port_forward; + const beforeHistory = before.network_endpoint_history; + const beforeDdns = before.network_ddns_record; + const afterChannel: FixtureRow = { + ...beforeChannel, + active_group_protocol_key: `${beforeChannel.id}:${beforeChannel.protocol}`, + group_id: beforeChannel.id, + }; + const afterHistory: FixtureRow = { + ...beforeHistory, + mechanism: 'udp_stun', + }; + const afterDdns: FixtureRow = { ...beforeDdns }; - expect(fixture).toContain('2041600000000008213'); - expect(fixture).toContain("'udp', 8213, 8211, 'udp:8213'"); - expect(fixture).toContain("17, '2026-07-26 08:00:00.000000', 17, 'synced', 'active'"); - expect(fixture).toContain("'203.0.113.13', 8213"); - expect(fixture).toContain('network_endpoint_history'); - expect(fixture).toContain('network_ddns_record'); - expect(fixture).toContain("'port_forward_ipv4', 2041600000000008213"); + expect(mutationAssignmentTargets(migration, 'network_port_forward')).toEqual( + ['active_group_protocol_key', 'group_id'], + ); + expect( + mutationAssignmentTargets(migration, 'network_endpoint_history'), + ).toEqual(['mechanism']); + expect(migration).not.toMatch( + /\b(?:update|insert into|replace into) network_ddns_record\b/, + ); + expect(migration).not.toMatch( + /\binsert into (?:network_port_forward|network_endpoint_history)\b/, + ); + expect( + withoutProperties(afterChannel, [ + 'active_group_protocol_key', + 'group_id', + ]), + ).toEqual(beforeChannel); + expect(withoutProperties(afterHistory, ['mechanism'])).toEqual( + beforeHistory, + ); + expect(afterDdns).toEqual(beforeDdns); + expect(afterChannel.id).toBe('2041600000000008213'); + expect(afterChannel.active_key).toBe('udp:8213'); + expect(afterChannel.desired_revision).toBe('17'); + expect(afterChannel.reported_revision).toBe('17'); + expect(afterChannel.keeper_status).toBe('active'); + expect(afterHistory.mapping_id).toBe(afterChannel.id); + expect(afterDdns.port_forward_id).toBe(afterChannel.id); expect(fixture).not.toContain('tcp'); }); });