Add Machine Data seed (tables, stored procedures, sample data) and fix SA password for shell compatibility

This commit is contained in:
Joseph Doherty
2026-03-16 14:41:28 -04:00
parent 0513a104a9
commit e3a418d603
7 changed files with 397 additions and 19 deletions

View File

@@ -24,11 +24,17 @@ The MS SQL container does not auto-run init scripts. After the first `docker com
```bash
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaLink_Dev1!' -C \
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
-i /docker-entrypoint-initdb.d/setup.sql
```
This creates the `ScadaLinkConfig` and `ScadaLinkMachineData` databases and the `scadalink_app` login.
This creates the `ScadaLinkConfig` and `ScadaLinkMachineData` databases and the `scadalink_app` login. Then seed the Machine Data database:
```bash
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
-i /docker-entrypoint-initdb.d/machinedata_seed.sql
```
## Stopping & Teardown

View File

@@ -34,11 +34,12 @@ services:
- "1433:1433"
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "ScadaLink_Dev1!"
MSSQL_SA_PASSWORD: "ScadaLink_Dev1#"
MSSQL_PID: "Developer"
volumes:
- scadalink-mssql-data:/var/opt/mssql
- ./mssql/setup.sql:/docker-entrypoint-initdb.d/setup.sql:ro
- ./mssql/machinedata_seed.sql:/docker-entrypoint-initdb.d/machinedata_seed.sql:ro
restart: unless-stopped
smtp:

View File

@@ -0,0 +1,330 @@
-- ScadaLink Machine Data Database seed script
-- Populates ScadaLinkMachineData with realistic SCADA/MES tables,
-- sample data, and stored procedures for development and testing.
--
-- Run after setup.sql:
-- docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
-- -S localhost -U sa -P 'ScadaLink_Dev1#' -C \
-- -i /docker-entrypoint-initdb.d/machinedata_seed.sql
USE ScadaLinkMachineData;
GO
-- =========================================================================
-- Tables
-- =========================================================================
-- Tag history: time-series data collected from OPC UA / custom protocols
IF OBJECT_ID('dbo.TagHistory', 'U') IS NULL
CREATE TABLE dbo.TagHistory (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
TagPath NVARCHAR(256) NOT NULL,
Timestamp DATETIME2(3) NOT NULL,
Value FLOAT NULL,
StringValue NVARCHAR(512) NULL,
Quality TINYINT NOT NULL DEFAULT 192, -- 192 = Good
SiteId NVARCHAR(64) NOT NULL,
INDEX IX_TagHistory_TagPath_Timestamp (TagPath, Timestamp DESC),
INDEX IX_TagHistory_SiteId_Timestamp (SiteId, Timestamp DESC)
);
GO
-- Production counts: shift/line production totals
IF OBJECT_ID('dbo.ProductionCounts', 'U') IS NULL
CREATE TABLE dbo.ProductionCounts (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
SiteId NVARCHAR(64) NOT NULL,
LineName NVARCHAR(128) NOT NULL,
ShiftDate DATE NOT NULL,
ShiftNumber TINYINT NOT NULL,
GoodCount INT NOT NULL DEFAULT 0,
RejectCount INT NOT NULL DEFAULT 0,
Efficiency DECIMAL(5,2) NULL,
RecordedAt DATETIME2(3) NOT NULL DEFAULT SYSUTCDATETIME(),
INDEX IX_ProductionCounts_Site_Date (SiteId, ShiftDate DESC)
);
GO
-- Equipment events: state changes, faults, maintenance
IF OBJECT_ID('dbo.EquipmentEvents', 'U') IS NULL
CREATE TABLE dbo.EquipmentEvents (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
SiteId NVARCHAR(64) NOT NULL,
EquipmentId NVARCHAR(128) NOT NULL,
EventType NVARCHAR(32) NOT NULL, -- 'StateChange', 'Fault', 'Maintenance', 'Alarm'
PreviousState NVARCHAR(64) NULL,
NewState NVARCHAR(64) NOT NULL,
Description NVARCHAR(512) NULL,
Timestamp DATETIME2(3) NOT NULL,
INDEX IX_EquipmentEvents_Equipment_Time (EquipmentId, Timestamp DESC),
INDEX IX_EquipmentEvents_Site_Type (SiteId, EventType, Timestamp DESC)
);
GO
-- Batch records: production batch tracking
IF OBJECT_ID('dbo.BatchRecords', 'U') IS NULL
CREATE TABLE dbo.BatchRecords (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
BatchId NVARCHAR(64) NOT NULL UNIQUE,
SiteId NVARCHAR(64) NOT NULL,
RecipeId NVARCHAR(64) NOT NULL,
Status NVARCHAR(32) NOT NULL DEFAULT 'InProgress', -- 'InProgress', 'Complete', 'Aborted'
StartTime DATETIME2(3) NOT NULL,
EndTime DATETIME2(3) NULL,
TotalQuantity DECIMAL(10,2) NULL,
Operator NVARCHAR(128) NULL,
Notes NVARCHAR(1024) NULL,
INDEX IX_BatchRecords_Site_Status (SiteId, Status),
INDEX IX_BatchRecords_StartTime (StartTime DESC)
);
GO
-- Alarm history: historical alarm events
IF OBJECT_ID('dbo.AlarmHistory', 'U') IS NULL
CREATE TABLE dbo.AlarmHistory (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
SiteId NVARCHAR(64) NOT NULL,
AlarmName NVARCHAR(256) NOT NULL,
Severity TINYINT NOT NULL, -- 1=Low, 2=Medium, 3=High, 4=Critical
State NVARCHAR(32) NOT NULL, -- 'Active', 'Acknowledged', 'Cleared'
ActivatedAt DATETIME2(3) NOT NULL,
AcknowledgedAt DATETIME2(3) NULL,
ClearedAt DATETIME2(3) NULL,
AcknowledgedBy NVARCHAR(128) NULL,
Message NVARCHAR(512) NULL,
INDEX IX_AlarmHistory_Site_Active (SiteId, State, ActivatedAt DESC),
INDEX IX_AlarmHistory_Severity (Severity, ActivatedAt DESC)
);
GO
-- =========================================================================
-- Stored Procedures
-- =========================================================================
-- Get tag history for a tag path within a date range
IF OBJECT_ID('dbo.usp_GetTagHistory', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetTagHistory;
GO
CREATE PROCEDURE dbo.usp_GetTagHistory
@TagPath NVARCHAR(256),
@StartTime DATETIME2(3),
@EndTime DATETIME2(3),
@MaxRows INT = 10000
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (@MaxRows)
TagPath, Timestamp, Value, StringValue, Quality, SiteId
FROM dbo.TagHistory
WHERE TagPath = @TagPath
AND Timestamp >= @StartTime
AND Timestamp <= @EndTime
ORDER BY Timestamp DESC;
END;
GO
-- Get production summary for a site over a date range
IF OBJECT_ID('dbo.usp_GetProductionSummary', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetProductionSummary;
GO
CREATE PROCEDURE dbo.usp_GetProductionSummary
@SiteId NVARCHAR(64),
@StartDate DATE,
@EndDate DATE
AS
BEGIN
SET NOCOUNT ON;
SELECT
LineName,
SUM(GoodCount) AS TotalGood,
SUM(RejectCount) AS TotalReject,
SUM(GoodCount) + SUM(RejectCount) AS TotalProduced,
CASE
WHEN SUM(GoodCount) + SUM(RejectCount) > 0
THEN CAST(SUM(GoodCount) * 100.0 / (SUM(GoodCount) + SUM(RejectCount)) AS DECIMAL(5,2))
ELSE 0
END AS YieldPercent,
AVG(Efficiency) AS AvgEfficiency,
COUNT(DISTINCT ShiftDate) AS DaysReported
FROM dbo.ProductionCounts
WHERE SiteId = @SiteId
AND ShiftDate >= @StartDate
AND ShiftDate <= @EndDate
GROUP BY LineName
ORDER BY LineName;
END;
GO
-- Insert a batch record (used by CachedWrite from scripts)
IF OBJECT_ID('dbo.usp_InsertBatchRecord', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_InsertBatchRecord;
GO
CREATE PROCEDURE dbo.usp_InsertBatchRecord
@BatchId NVARCHAR(64),
@SiteId NVARCHAR(64),
@RecipeId NVARCHAR(64),
@TotalQuantity DECIMAL(10,2) = NULL,
@Operator NVARCHAR(128) = NULL,
@Notes NVARCHAR(1024) = NULL
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.BatchRecords (BatchId, SiteId, RecipeId, Status, StartTime, TotalQuantity, Operator, Notes)
VALUES (@BatchId, @SiteId, @RecipeId, 'InProgress', SYSUTCDATETIME(), @TotalQuantity, @Operator, @Notes);
SELECT SCOPE_IDENTITY() AS Id, @BatchId AS BatchId;
END;
GO
-- Complete or abort a batch
IF OBJECT_ID('dbo.usp_CompleteBatch', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_CompleteBatch;
GO
CREATE PROCEDURE dbo.usp_CompleteBatch
@BatchId NVARCHAR(64),
@Status NVARCHAR(32), -- 'Complete' or 'Aborted'
@TotalQuantity DECIMAL(10,2) = NULL,
@Notes NVARCHAR(1024) = NULL
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.BatchRecords
SET Status = @Status,
EndTime = SYSUTCDATETIME(),
TotalQuantity = COALESCE(@TotalQuantity, TotalQuantity),
Notes = COALESCE(@Notes, Notes)
WHERE BatchId = @BatchId
AND Status = 'InProgress';
IF @@ROWCOUNT = 0
THROW 50001, 'Batch not found or not in progress.', 1;
SELECT BatchId, Status, StartTime, EndTime, TotalQuantity
FROM dbo.BatchRecords
WHERE BatchId = @BatchId;
END;
GO
-- Get recent equipment events
IF OBJECT_ID('dbo.usp_GetEquipmentEvents', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetEquipmentEvents;
GO
CREATE PROCEDURE dbo.usp_GetEquipmentEvents
@SiteId NVARCHAR(64),
@EquipmentId NVARCHAR(128) = NULL,
@EventType NVARCHAR(32) = NULL,
@Hours INT = 24
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Since DATETIME2(3) = DATEADD(HOUR, -@Hours, SYSUTCDATETIME());
SELECT
EquipmentId, EventType, PreviousState, NewState, Description, Timestamp
FROM dbo.EquipmentEvents
WHERE SiteId = @SiteId
AND Timestamp >= @Since
AND (@EquipmentId IS NULL OR EquipmentId = @EquipmentId)
AND (@EventType IS NULL OR EventType = @EventType)
ORDER BY Timestamp DESC;
END;
GO
-- Get active alarms for a site
IF OBJECT_ID('dbo.usp_GetActiveAlarms', 'P') IS NOT NULL DROP PROCEDURE dbo.usp_GetActiveAlarms;
GO
CREATE PROCEDURE dbo.usp_GetActiveAlarms
@SiteId NVARCHAR(64),
@MinSeverity TINYINT = 1
AS
BEGIN
SET NOCOUNT ON;
SELECT
AlarmName, Severity, State, ActivatedAt, AcknowledgedAt,
AcknowledgedBy, Message
FROM dbo.AlarmHistory
WHERE SiteId = @SiteId
AND State IN ('Active', 'Acknowledged')
AND Severity >= @MinSeverity
ORDER BY Severity DESC, ActivatedAt DESC;
END;
GO
-- =========================================================================
-- Sample Data
-- =========================================================================
-- Tag history (last few hours of data for two sites)
DECLARE @now DATETIME2(3) = SYSUTCDATETIME();
INSERT INTO dbo.TagHistory (TagPath, Timestamp, Value, Quality, SiteId) VALUES
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -120, @now), 45.2, 192, 'SiteA'),
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -110, @now), 46.1, 192, 'SiteA'),
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -100, @now), 44.8, 192, 'SiteA'),
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -90, @now), 47.3, 192, 'SiteA'),
('SiteA/Pump-001/Pressure', DATEADD(MINUTE, -80, @now), 45.9, 192, 'SiteA'),
('SiteA/Pump-001/Flow', DATEADD(MINUTE, -120, @now), 120.5, 192, 'SiteA'),
('SiteA/Pump-001/Flow', DATEADD(MINUTE, -110, @now), 121.2, 192, 'SiteA'),
('SiteA/Pump-001/Flow', DATEADD(MINUTE, -100, @now), 119.8, 192, 'SiteA'),
('SiteA/Tank-001/Level', DATEADD(MINUTE, -120, @now), 72.0, 192, 'SiteA'),
('SiteA/Tank-001/Level', DATEADD(MINUTE, -110, @now), 73.5, 192, 'SiteA'),
('SiteA/Tank-001/Level', DATEADD(MINUTE, -100, @now), 75.1, 192, 'SiteA'),
('SiteA/Tank-001/Level', DATEADD(MINUTE, -90, @now), 76.8, 192, 'SiteA'),
('SiteA/Tank-001/Temperature', DATEADD(MINUTE, -120, @now), 65.3, 192, 'SiteA'),
('SiteA/Tank-001/Temperature', DATEADD(MINUTE, -110, @now), 65.5, 192, 'SiteA'),
('SiteA/Conv-001/Speed', DATEADD(MINUTE, -120, @now), 2.4, 192, 'SiteA'),
('SiteA/Conv-001/Speed', DATEADD(MINUTE, -110, @now), 0.0, 0, 'SiteA'), -- Bad quality (stopped)
('SiteA/Conv-001/Speed', DATEADD(MINUTE, -100, @now), 2.3, 192, 'SiteA'),
('SiteB/Mixer-001/RPM', DATEADD(MINUTE, -120, @now), 450.0, 192, 'SiteB'),
('SiteB/Mixer-001/RPM', DATEADD(MINUTE, -110, @now), 452.0, 192, 'SiteB'),
('SiteB/Mixer-001/RPM', DATEADD(MINUTE, -100, @now), 448.0, 192, 'SiteB'),
('SiteB/Mixer-001/Temperature',DATEADD(MINUTE, -120, @now), 82.1, 192, 'SiteB'),
('SiteB/Mixer-001/Temperature',DATEADD(MINUTE, -110, @now), 83.0, 192, 'SiteB');
-- Production counts (last 3 days, 2 shifts per day)
DECLARE @today DATE = CAST(SYSUTCDATETIME() AS DATE);
INSERT INTO dbo.ProductionCounts (SiteId, LineName, ShiftDate, ShiftNumber, GoodCount, RejectCount, Efficiency) VALUES
('SiteA', 'Line-1', DATEADD(DAY, -2, @today), 1, 4100, 82, 92.5),
('SiteA', 'Line-1', DATEADD(DAY, -2, @today), 2, 3900, 95, 91.2),
('SiteA', 'Line-2', DATEADD(DAY, -2, @today), 1, 3050, 120, 88.1),
('SiteA', 'Line-2', DATEADD(DAY, -2, @today), 2, 2900, 105, 87.5),
('SiteA', 'Line-1', DATEADD(DAY, -1, @today), 1, 4200, 75, 93.1),
('SiteA', 'Line-1', DATEADD(DAY, -1, @today), 2, 4050, 88, 92.0),
('SiteA', 'Line-2', DATEADD(DAY, -1, @today), 1, 3100, 98, 89.2),
('SiteA', 'Line-2', DATEADD(DAY, -1, @today), 2, 3000, 110, 88.0),
('SiteA', 'Line-1', @today, 1, 2100, 40, 93.5),
('SiteA', 'Line-2', @today, 1, 1550, 65, 88.8),
('SiteB', 'Mixing', DATEADD(DAY, -1, @today), 1, 850, 12, 95.0),
('SiteB', 'Mixing', DATEADD(DAY, -1, @today), 2, 820, 15, 94.2),
('SiteB', 'Packing', DATEADD(DAY, -1, @today), 1, 1600, 45, 90.5),
('SiteB', 'Packing', DATEADD(DAY, -1, @today), 2, 1550, 50, 89.8);
-- Equipment events
INSERT INTO dbo.EquipmentEvents (SiteId, EquipmentId, EventType, PreviousState, NewState, Description, Timestamp) VALUES
('SiteA', 'PUMP-001', 'StateChange', 'Idle', 'Running', 'Shift start', DATEADD(HOUR, -8, @now)),
('SiteA', 'PUMP-001', 'Fault', 'Running', 'Faulted', 'Overcurrent detected', DATEADD(HOUR, -5, @now)),
('SiteA', 'PUMP-001', 'StateChange', 'Faulted', 'Running', 'Manual reset by operator', DATEADD(HOUR, -4, @now)),
('SiteA', 'TANK-001', 'Alarm', NULL, 'HighLevel','Tank level exceeded 90%', DATEADD(HOUR, -3, @now)),
('SiteA', 'TANK-001', 'Alarm', NULL, 'Normal', 'Tank level returned to normal',DATEADD(HOUR, -2, @now)),
('SiteA', 'CONV-001', 'Maintenance', 'Running', 'Maintenance', 'Scheduled belt inspection', DATEADD(HOUR, -6, @now)),
('SiteA', 'CONV-001', 'StateChange', 'Maintenance', 'Running', 'Maintenance complete', DATEADD(HOUR, -5, @now)),
('SiteB', 'MIXER-001','StateChange', 'Idle', 'Running', 'Batch R-100 started', DATEADD(HOUR, -7, @now)),
('SiteB', 'MIXER-001','StateChange', 'Running', 'Idle', 'Batch R-100 complete', DATEADD(HOUR, -3, @now));
-- Batch records
INSERT INTO dbo.BatchRecords (BatchId, SiteId, RecipeId, Status, StartTime, EndTime, TotalQuantity, Operator, Notes) VALUES
('BATCH-20260314-001', 'SiteA', 'R-100', 'Complete', DATEADD(HOUR, -26, @now), DATEADD(HOUR, -22, @now), 450.00, 'jsmith', 'Normal run'),
('BATCH-20260314-002', 'SiteA', 'R-200', 'Complete', DATEADD(HOUR, -20, @now), DATEADD(HOUR, -16, @now), 380.50, 'jsmith', 'Slight yield loss on Material-B'),
('BATCH-20260315-001', 'SiteB', 'R-100', 'Complete', DATEADD(HOUR, -10, @now), DATEADD(HOUR, -6, @now), 445.00, 'mdoe', NULL),
('BATCH-20260315-002', 'SiteA', 'R-150', 'Aborted', DATEADD(HOUR, -8, @now), DATEADD(HOUR, -7, @now), NULL, 'jsmith', 'Material-A out of spec, aborted early'),
('BATCH-20260316-001', 'SiteA', 'R-100', 'InProgress', DATEADD(HOUR, -2, @now), NULL, NULL, 'bwilson', 'Current batch');
-- Alarm history
INSERT INTO dbo.AlarmHistory (SiteId, AlarmName, Severity, State, ActivatedAt, AcknowledgedAt, ClearedAt, AcknowledgedBy, Message) VALUES
('SiteA', 'Tank-001 High Level', 3, 'Cleared', DATEADD(HOUR, -3, @now), DATEADD(HOUR, -3, @now), DATEADD(HOUR, -2, @now), 'jsmith', 'Level exceeded 90% setpoint'),
('SiteA', 'Pump-001 Overcurrent', 4, 'Cleared', DATEADD(HOUR, -5, @now), DATEADD(HOUR, -5, @now), DATEADD(HOUR, -4, @now), 'jsmith', 'Current draw 15.2A (limit 12A)'),
('SiteA', 'Conv-001 Belt Slip', 2, 'Active', DATEADD(HOUR, -1, @now), NULL, NULL, NULL, 'Belt speed deviation >5%'),
('SiteA', 'Tank-001 Temperature High', 3, 'Acknowledged', DATEADD(MINUTE, -30, @now), DATEADD(MINUTE, -25, @now), NULL, 'bwilson', 'Temperature 68.2C (limit 65C)'),
('SiteB', 'Mixer-001 Vibration', 2, 'Active', DATEADD(HOUR, -2, @now), NULL, NULL, NULL, 'Vibration level elevated');
GO
PRINT 'ScadaLinkMachineData seed complete.';
PRINT 'Tables: TagHistory, ProductionCounts, EquipmentEvents, BatchRecords, AlarmHistory';
PRINT 'Stored Procedures: usp_GetTagHistory, usp_GetProductionSummary, usp_InsertBatchRecord, usp_CompleteBatch, usp_GetEquipmentEvents, usp_GetActiveAlarms';
GO

View File

@@ -14,7 +14,7 @@ GO
-- Create application login
IF NOT EXISTS (SELECT name FROM sys.server_principals WHERE name = 'scadalink_app')
CREATE LOGIN scadalink_app WITH PASSWORD = 'ScadaLink_Dev1!', DEFAULT_DATABASE = ScadaLinkConfig;
CREATE LOGIN scadalink_app WITH PASSWORD = 'ScadaLink_Dev1#', DEFAULT_DATABASE = ScadaLinkConfig;
GO
-- Grant db_owner on ScadaLinkConfig

View File

@@ -10,7 +10,7 @@ import pymssql
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 1433
DEFAULT_USER = "sa"
DEFAULT_PASSWORD = "ScadaLink_Dev1!"
DEFAULT_PASSWORD = "ScadaLink_Dev1#"
EXPECTED_DBS = ["ScadaLinkConfig", "ScadaLinkMachineData"]