USE [RobotInstaller]; GO SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON; GO IF OBJECT_ID(N'dbo.ApplicationPackages', N'U') IS NOT NULL OR OBJECT_ID(N'dbo.PackageVersions', N'U') IS NOT NULL OR OBJECT_ID(N'dbo.Applications', N'U') IS NOT NULL OR OBJECT_ID(N'dbo.Packages', N'U') IS NOT NULL OR OBJECT_ID(N'dbo.EmailConfirmationTokens', N'U') IS NOT NULL OR OBJECT_ID(N'dbo.Users', N'U') IS NOT NULL BEGIN THROW 50001, 'Schema tables already exist. Review, drop, or migrate existing tables before running 02_schema.sql.', 1; END; GO CREATE TABLE dbo.Users ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_Users PRIMARY KEY CLUSTERED CONSTRAINT DF_Users_Id DEFAULT NEWSEQUENTIALID(), Username NVARCHAR(100) NOT NULL, Email NVARCHAR(255) NOT NULL, PasswordHash NVARCHAR(500) NOT NULL, FullName NVARCHAR(200) NULL, Role NVARCHAR(50) NOT NULL CONSTRAINT DF_Users_Role DEFAULT N'User', IsActive BIT NOT NULL CONSTRAINT DF_Users_IsActive DEFAULT 1, CreatedAt DATETIME2(3) NOT NULL CONSTRAINT DF_Users_CreatedAt DEFAULT SYSUTCDATETIME(), UpdatedAt DATETIME2(3) NULL, CONSTRAINT CK_Users_Role CHECK (Role IN (N'Admin', N'User')), CONSTRAINT CK_Users_Username_NotBlank CHECK (LEN(LTRIM(RTRIM(Username))) > 0), CONSTRAINT CK_Users_Email_NotBlank CHECK (LEN(LTRIM(RTRIM(Email))) > 0) ); GO CREATE TABLE dbo.EmailConfirmationTokens ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_EmailConfirmationTokens PRIMARY KEY CLUSTERED CONSTRAINT DF_EmailConfirmationTokens_Id DEFAULT NEWSEQUENTIALID(), UserId UNIQUEIDENTIFIER NOT NULL, TokenHash CHAR(64) NOT NULL, ExpiresAt DATETIME2(3) NOT NULL, ConfirmedAt DATETIME2(3) NULL, CreatedAt DATETIME2(3) NOT NULL CONSTRAINT DF_EmailConfirmationTokens_CreatedAt DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_EmailConfirmationTokens_User FOREIGN KEY (UserId) REFERENCES dbo.Users(Id) ON DELETE CASCADE ); GO CREATE TABLE dbo.Packages ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_Packages PRIMARY KEY CLUSTERED CONSTRAINT DF_Packages_Id DEFAULT NEWSEQUENTIALID(), PackageCode NVARCHAR(100) NOT NULL, PackageName NVARCHAR(200) NOT NULL, PackageType NVARCHAR(20) NOT NULL, Description NVARCHAR(MAX) NULL, CreatedByUserId UNIQUEIDENTIFIER NOT NULL, CreatedAt DATETIME2(3) NOT NULL CONSTRAINT DF_Packages_CreatedAt DEFAULT SYSUTCDATETIME(), UpdatedAt DATETIME2(3) NULL, IsActive BIT NOT NULL CONSTRAINT DF_Packages_IsActive DEFAULT 1, CONSTRAINT FK_Packages_CreatedByUser FOREIGN KEY (CreatedByUserId) REFERENCES dbo.Users(Id), CONSTRAINT CK_Packages_PackageType CHECK (PackageType IN (N'deb', N'docker')), CONSTRAINT CK_Packages_PackageCode_NotBlank CHECK (LEN(LTRIM(RTRIM(PackageCode))) > 0), CONSTRAINT CK_Packages_PackageName_NotBlank CHECK (LEN(LTRIM(RTRIM(PackageName))) > 0) ); GO CREATE TABLE dbo.PackageVersions ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_PackageVersions PRIMARY KEY CLUSTERED CONSTRAINT DF_PackageVersions_Id DEFAULT NEWSEQUENTIALID(), PackageId UNIQUEIDENTIFIER NOT NULL, Version NVARCHAR(50) NOT NULL, FilePath NVARCHAR(1000) NULL, DockerImage NVARCHAR(500) NULL, FileChecksumSha256 CHAR(64) NULL, FileSizeBytes BIGINT NULL, ChangeLog NVARCHAR(MAX) NULL, ReleaseDate DATETIME2(3) NOT NULL CONSTRAINT DF_PackageVersions_ReleaseDate DEFAULT SYSUTCDATETIME(), UploadedAt DATETIME2(3) NOT NULL CONSTRAINT DF_PackageVersions_UploadedAt DEFAULT SYSUTCDATETIME(), IsLatest BIT NOT NULL CONSTRAINT DF_PackageVersions_IsLatest DEFAULT 0, IsDeprecated BIT NOT NULL CONSTRAINT DF_PackageVersions_IsDeprecated DEFAULT 0, CONSTRAINT FK_PackageVersions_Package FOREIGN KEY (PackageId) REFERENCES dbo.Packages(Id) ON DELETE CASCADE, CONSTRAINT CK_PackageVersions_Version_NotBlank CHECK (LEN(LTRIM(RTRIM(Version))) > 0), CONSTRAINT CK_PackageVersions_FileSizeBytes CHECK (FileSizeBytes IS NULL OR FileSizeBytes >= 0), CONSTRAINT CK_PackageVersions_FileChecksumSha256 CHECK ( FileChecksumSha256 IS NULL OR FileChecksumSha256 NOT LIKE '%[^0-9A-Fa-f]%' ) ); GO CREATE TABLE dbo.Applications ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_Applications PRIMARY KEY CLUSTERED CONSTRAINT DF_Applications_Id DEFAULT NEWSEQUENTIALID(), AppCode NVARCHAR(100) NOT NULL, AppName NVARCHAR(200) NOT NULL, AppVersion NVARCHAR(50) NOT NULL CONSTRAINT DF_Applications_AppVersion DEFAULT N'1.0.0', Description NVARCHAR(MAX) NULL, CreatedByUserId UNIQUEIDENTIFIER NOT NULL, CreatedAt DATETIME2(3) NOT NULL CONSTRAINT DF_Applications_CreatedAt DEFAULT SYSUTCDATETIME(), UpdatedAt DATETIME2(3) NULL, Status NVARCHAR(50) NOT NULL CONSTRAINT DF_Applications_Status DEFAULT N'Draft', Notes NVARCHAR(500) NULL, CONSTRAINT FK_Applications_CreatedByUser FOREIGN KEY (CreatedByUserId) REFERENCES dbo.Users(Id), CONSTRAINT CK_Applications_Status CHECK (Status IN (N'Draft', N'Released', N'Archived')), CONSTRAINT CK_Applications_AppCode_NotBlank CHECK (LEN(LTRIM(RTRIM(AppCode))) > 0), CONSTRAINT CK_Applications_AppName_NotBlank CHECK (LEN(LTRIM(RTRIM(AppName))) > 0), CONSTRAINT CK_Applications_AppVersion_NotBlank CHECK (LEN(LTRIM(RTRIM(AppVersion))) > 0) ); GO CREATE TABLE dbo.ApplicationPackages ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_ApplicationPackages PRIMARY KEY CLUSTERED CONSTRAINT DF_ApplicationPackages_Id DEFAULT NEWSEQUENTIALID(), ApplicationId UNIQUEIDENTIFIER NOT NULL, PackageId UNIQUEIDENTIFIER NOT NULL, SelectedVersionId UNIQUEIDENTIFIER NULL, AddedAt DATETIME2(3) NOT NULL CONSTRAINT DF_ApplicationPackages_AddedAt DEFAULT SYSUTCDATETIME(), Notes NVARCHAR(500) NULL, CONSTRAINT FK_ApplicationPackages_Application FOREIGN KEY (ApplicationId) REFERENCES dbo.Applications(Id) ON DELETE CASCADE, CONSTRAINT FK_ApplicationPackages_Package FOREIGN KEY (PackageId) REFERENCES dbo.Packages(Id) ON DELETE CASCADE ); GO CREATE UNIQUE INDEX UX_Users_Username ON dbo.Users(Username); CREATE UNIQUE INDEX UX_Users_Email ON dbo.Users(Email); GO CREATE UNIQUE INDEX UX_EmailConfirmationTokens_TokenHash ON dbo.EmailConfirmationTokens(TokenHash); CREATE INDEX IX_EmailConfirmationTokens_UserId ON dbo.EmailConfirmationTokens(UserId); GO CREATE UNIQUE INDEX UX_Packages_PackageCode ON dbo.Packages(PackageCode); CREATE INDEX IX_Packages_CreatedByUserId ON dbo.Packages(CreatedByUserId); CREATE INDEX IX_Packages_PackageType ON dbo.Packages(PackageType); GO CREATE UNIQUE INDEX UX_PackageVersions_PackageId_Version ON dbo.PackageVersions(PackageId, Version); CREATE UNIQUE INDEX UX_PackageVersions_Id_PackageId ON dbo.PackageVersions(Id, PackageId); CREATE UNIQUE INDEX UX_PackageVersions_OneLatestPerPackage ON dbo.PackageVersions(PackageId) WHERE IsLatest = 1; CREATE INDEX IX_PackageVersions_PackageId_ReleaseDate ON dbo.PackageVersions(PackageId, ReleaseDate DESC); GO CREATE UNIQUE INDEX UX_Applications_AppCode ON dbo.Applications(AppCode); CREATE INDEX IX_Applications_CreatedByUserId ON dbo.Applications(CreatedByUserId); CREATE INDEX IX_Applications_Status ON dbo.Applications(Status); GO CREATE UNIQUE INDEX UX_ApplicationPackages_ApplicationId_PackageId ON dbo.ApplicationPackages(ApplicationId, PackageId); CREATE INDEX IX_ApplicationPackages_PackageId ON dbo.ApplicationPackages(PackageId); CREATE INDEX IX_ApplicationPackages_SelectedVersionId ON dbo.ApplicationPackages(SelectedVersionId); GO ALTER TABLE dbo.ApplicationPackages ADD CONSTRAINT FK_ApplicationPackages_SelectedVersionBelongsToPackage FOREIGN KEY (SelectedVersionId, PackageId) REFERENCES dbo.PackageVersions(Id, PackageId); GO CREATE OR ALTER PROCEDURE dbo.SetLatestPackageVersion @PackageVersionId UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @PackageId UNIQUEIDENTIFIER; SELECT @PackageId = PackageId FROM dbo.PackageVersions WHERE Id = @PackageVersionId; IF @PackageId IS NULL BEGIN THROW 50002, 'Package version does not exist.', 1; END; BEGIN TRANSACTION; UPDATE dbo.PackageVersions SET IsLatest = 0 WHERE PackageId = @PackageId; UPDATE dbo.PackageVersions SET IsLatest = 1, IsDeprecated = 0 WHERE Id = @PackageVersionId; COMMIT TRANSACTION; END; GO CREATE OR ALTER PROCEDURE dbo.DeletePackageVersion @PackageVersionId UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @PackageId UNIQUEIDENTIFIER; DECLARE @WasLatest BIT; SELECT @PackageId = PackageId, @WasLatest = IsLatest FROM dbo.PackageVersions WHERE Id = @PackageVersionId; IF @PackageId IS NULL BEGIN THROW 50003, 'Package version does not exist.', 1; END; BEGIN TRANSACTION; DELETE FROM dbo.ApplicationPackages WHERE SelectedVersionId = @PackageVersionId; DELETE FROM dbo.PackageVersions WHERE Id = @PackageVersionId; IF @WasLatest = 1 BEGIN DECLARE @NextLatestId UNIQUEIDENTIFIER; SELECT TOP (1) @NextLatestId = Id FROM dbo.PackageVersions WHERE PackageId = @PackageId AND IsDeprecated = 0 ORDER BY ReleaseDate DESC, UploadedAt DESC; IF @NextLatestId IS NOT NULL BEGIN EXEC dbo.SetLatestPackageVersion @NextLatestId; END; END; COMMIT TRANSACTION; END; GO PRINT N'RobotInstaller schema was created successfully.'; GO