54 KiB
Database Schema Specification
Purpose
The database schema defines the SQL Server cache database used by the JDE Scoping Tool application. This database serves as an intermediate cache layer between the enterprise source systems (JDE Oracle and CMS Sybase) and the application's search functionality. The schema is designed for compatibility with .NET 10 and supports both Dapper micro-ORM and EF Core data access patterns. The schema includes:
- Application tables - Search request management and data update tracking
- Cache tables - Local copies of JDE/CMS data split into current and historical partitions
- Reference tables - Lookup data for items, users, work centers, etc.
- Views - Union views combining current/historical data and computed aggregations
- Stored procedures - Search lifecycle management (atomic operations)
- User-defined types - Table-valued parameters for bulk filtering
- Functions - MIS data matching logic
Source Reference
| Legacy Files | Purpose |
|---|---|
| OLD/Database/Tables/Search.sql | User search request tracking |
| OLD/Database/Tables/DataUpdate.sql | Cache refresh tracking |
| OLD/Database/Tables/WorkOrder_Curr.sql | Current work orders |
| OLD/Database/Tables/WorkOrder_Hist.sql | Historical work orders |
| OLD/Database/Tables/WorkOrderStep_Curr.sql | Current work order steps |
| OLD/Database/Tables/WorkOrderStep_Hist.sql | Historical work order steps |
| OLD/Database/Tables/WorkOrderTime_Curr.sql | Current work order time transactions |
| OLD/Database/Tables/WorkOrderTime_Hist.sql | Historical work order time transactions |
| OLD/Database/Tables/WorkOrderComponent_Curr.sql | Current work order components |
| OLD/Database/Tables/WorkOrderComponent_Hist.sql | Historical work order components |
| OLD/Database/Tables/WorkOrderRouting_Curr.sql | Work order routing transactions |
| OLD/Database/Tables/LotUsage_Curr.sql | Current lot usage (cardex) |
| OLD/Database/Tables/LotUsage_Hist.sql | Historical lot usage |
| OLD/Database/Tables/Lot.sql | Lot master data |
| OLD/Database/Tables/LotLocation.sql | Lot location tracking |
| OLD/Database/Tables/Item.sql | Item master data |
| OLD/Database/Tables/WorkCenter.sql | Work center reference |
| OLD/Database/Tables/ProfitCenter.sql | Profit center reference |
| OLD/Database/Tables/Branch.sql | Branch reference |
| OLD/Database/Tables/JdeUser.sql | JDE user (operator) reference |
| OLD/Database/Tables/StatusCode.sql | Work order status codes |
| OLD/Database/Tables/FunctionCode.sql | Function code reference |
| OLD/Database/Tables/OrgHierarchy.sql | Organization hierarchy mapping |
| OLD/Database/Tables/RouteMaster.sql | Item routing master |
| OLD/Database/Tables/MisData.sql | CMS MIS data cache |
| OLD/Database/Views/WorkOrder.sql | Union of current/historical work orders |
| OLD/Database/Views/WorkOrderStep.sql | Union of current/historical steps with function descriptions |
| OLD/Database/Views/WorkOrderTime.sql | Union of current/historical time transactions |
| OLD/Database/Views/WorkOrderComponent.sql | Union of current/historical components |
| OLD/Database/Views/LotUsage.sql | Union of current/historical lot usage |
| OLD/Database/Views/LastDataUpdates.sql | Most recent successful data updates by table |
| OLD/Database/Views/WorkOrderTotalScrap.sql | Aggregated scrap quantities by work order |
| OLD/Database/StoredProcedures/SubmitSearch.sql | Insert new search request |
| OLD/Database/StoredProcedures/StartSearch.sql | Mark search as started |
| OLD/Database/StoredProcedures/CompleteSearch.sql | Mark search as completed |
| OLD/Database/StoredProcedures/ResetPartialSearches.sql | Reset incomplete searches on startup |
| OLD/Database/Functions/MatchMis.sql | MIS data matching function |
| OLD/Database/Types/*.sql | Table-valued parameter types for filtering |
| OLD/Database/Setup.sql | Complete database setup script |
Requirements
Requirement: Search table
The system SHALL store user search requests, criteria, status, and results.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| ID | INT IDENTITY(1,1) | NOT NULL | Primary key, auto-increment |
| UserName | VARCHAR(128) | NOT NULL | Username of search creator |
| Name | VARCHAR(128) | NULL | User-friendly search name |
| Status | SMALLINT | NOT NULL | Status code (0=New, 1=Submitted, 2=Started, 3=Ended, 4=Error) |
| SubmitDT | DATETIME | NULL | Timestamp when search was submitted |
| StartDT | DATETIME | NULL | Timestamp when processing started |
| EndDT | DATETIME | NULL | Timestamp when processing completed |
| Criteria | VARCHAR(MAX) | NULL | JSON-serialized search criteria |
| Results | VARBINARY(MAX) | NULL | Excel file binary output |
Primary Key
ID(clustered)
Indexes
IX_Search_UserNameon[UserName]
Business Rules
- Status values: 0=New, 1=Submitted, 2=Started, 3=Ended, 4=Error
- Results is populated only when Status = 3 (Ended)
- Criteria stores JSON-serialized SearchCriteria object
Scenario: Insert new search request
- WHEN a user "jdoe" submits a search named "Q4 Report" and the usp_SubmitSearch stored procedure is called
- THEN a new row is inserted with Status = 1 (Submitted), SubmitDT is set to current timestamp, and ID is auto-generated
Scenario: Search completes successfully
- WHEN a search with ID = 123 and Status = 2 (Started) exists and usp_CompleteSearch is called with WasSuccessful = 1
- THEN Status is updated to 3 (Ended), Results contains the Excel binary data, and EndDT is set to current timestamp
Scenario: Query searches by user
- WHEN multiple searches exist for different users and querying with UserName = "jdoe"
- THEN IX_Search_UserName index is used and only searches for that user are returned
Requirement: DataUpdate table
The system SHALL track cache data refresh operations for monitoring and incremental updates.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| ID | INT IDENTITY(1,1) | NOT NULL | Primary key, auto-increment |
| SourceSystem | VARCHAR(50) | NOT NULL | Source system name (JDE, CMS) |
| SourceData | VARCHAR(50) | NOT NULL | Source data type identifier |
| TableName | VARCHAR(50) | NOT NULL | Target cache table name |
| StartDT | DATETIME | NOT NULL | Update start timestamp |
| EndDT | DATETIME | NOT NULL | Update end timestamp |
| UpdateType | SMALLINT | NOT NULL | Update type (1=Hourly, 2=Daily, 3=Mass) |
| WasSuccessful | BIT | NOT NULL | Success indicator |
| NumberRecords | BIGINT | NOT NULL | Number of records processed |
Primary Key
ID(clustered)
Indexes
IX_DataUpdate_LastUpdateon[TableName], [WasSuccessful], [StartDT DESC]
Business Rules
- UpdateType enum: 1=Hourly, 2=Daily, 3=Mass
- Used to determine incremental update windows
Scenario: Record successful data refresh
- WHEN a daily refresh of WorkOrder table completes and 5000 records are processed successfully
- THEN a row is inserted with UpdateType = 2, WasSuccessful = 1, NumberRecords = 5000
Scenario: Query last successful update
- WHEN multiple updates exist for WorkOrder table and querying for incremental sync window
- THEN IX_DataUpdate_LastUpdate returns most recent successful update by StartDT DESC
Requirement: WorkOrder_Curr / WorkOrder_Hist tables
The system SHALL store JDE work order data, partitioned into current (active) and historical tables.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| WorkOrderNumber | BIGINT | NOT NULL | Primary key, JDE work order number |
| BranchCode | VARCHAR(12) | NULL | Branch/plant code |
| LotNumber | VARCHAR(30) | NULL | Assigned lot number |
| ItemNumber | VARCHAR(25) | NULL | Product item number |
| ShortItemNumber | BIGINT | NOT NULL | Numeric item identifier |
| ParentWorkOrderNumber | VARCHAR(8) | NULL | Parent work order (sub-assembly) |
| OrderQuantity | DECIMAL(15,2) | NOT NULL | Quantity ordered |
| HeldQuantity | DECIMAL(15,2) | NOT NULL | Quantity on hold |
| ShippedQuantity | DECIMAL(15,2) | NOT NULL | Quantity shipped |
| StatusCode | VARCHAR(10) | NULL | Work order status code |
| StatusCodeUpdateDT | DATETIME | NULL | Last status update timestamp |
| IssueDate | DATETIME | NOT NULL | Date work order was issued |
| StartDate | DATETIME | NOT NULL | Date work order was started |
| RoutingType | VARCHAR(3) | NULL | Routing type code |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
WorkOrderNumber(clustered)
Indexes
IX_WorkOrder_Curr_ParentWorkOrderNumberon[ParentWorkOrderNumber]IX_WorkOrder_Curr_ItemNumberon[ItemNumber]- (Same indexes on _Hist table)
Business Rules
- Data partitioned between _Curr and _Hist tables based on work order status
- Accessed via WorkOrder view which unions both tables
Scenario: Insert work order to current table
- WHEN a new active work order 12345 from JDE is received and the sync process inserts the record
- THEN it is stored in WorkOrder_Curr table and can be queried via WorkOrder view
Scenario: Query work orders by item
- WHEN work orders exist for item "ABC123" and querying by ItemNumber
- THEN IX_WorkOrder_Curr_ItemNumber index is used for efficient lookup
Scenario: Query sub-assembly work orders
- WHEN a parent work order has child work orders and querying by ParentWorkOrderNumber
- THEN IX_WorkOrder_Curr_ParentWorkOrderNumber enables parent-child traversal
Requirement: WorkOrderStep_Curr / WorkOrderStep_Hist tables
The system SHALL store JDE work order operation steps.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| WorkOrderNumber | BIGINT | NOT NULL | Work order identifier |
| WorkCenterCode | VARCHAR(12) | NOT NULL | Work center code |
| StepNumber | DECIMAL(7,2) | NOT NULL | Operation sequence number |
| StepTypeCode | VARCHAR(2) | NOT NULL | Operation type code |
| BranchCode | VARCHAR(12) | NOT NULL | Branch code |
| StepDescription | VARCHAR(30) | NULL | Step description |
| StartDT | DATETIME | NULL | Step start timestamp |
| EndDT | DATETIME | NULL | Step end timestamp |
| FunctionCode | VARCHAR(15) | NULL | Function code |
| ScrappedQuantity | DECIMAL(18,2) | NOT NULL | Quantity scrapped |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
WorkOrderNumber, WorkCenterCode, StepNumber, StepTypeCode(clustered composite)
Indexes
IX_WorkOrderStep_Curr_WorkCenterCodeon[WorkCenterCode]INCLUDE[WorkOrderNumber]
Business Rules
- StepNumber is DECIMAL(7,2) to support sub-steps (e.g., 10.01, 10.02)
- ScrappedQuantity is NOT NULL with default 0
Requirement: WorkOrderTime_Curr / WorkOrderTime_Hist tables
The system SHALL store F31122 work order time transaction records (operator labor).
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| UniqueID | BIGINT | NOT NULL | Primary key |
| WorkOrderNumber | BIGINT | NOT NULL | Work order identifier |
| StepNumber | DECIMAL(7,2) | NOT NULL | Operation sequence |
| WorkCenterCode | VARCHAR(12) | NOT NULL | Work center code |
| BranchCode | VARCHAR(12) | NOT NULL | Branch code |
| AddressNumber | BIGINT | NOT NULL | Operator address number |
| GlDate | DATETIME | NULL | G/L processing date |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
UniqueID(clustered)
Indexes
IX_WorkOrderTime_Curr_Lookupon[WorkOrderNumber, WorkCenterCode, StepNumber]IX_WorkOrderTime_Curr_AddressNumberon[AddressNumber]INCLUDE[WorkOrderNumber, WorkCenterCode, StepNumber, LastUpdateDT]
Business Rules
- Links operators to work order operations via AddressNumber -> JdeUser
Requirement: WorkOrderComponent_Curr / WorkOrderComponent_Hist tables
The system SHALL store work order component usage (materials consumed).
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| UniqueID | BIGINT | NOT NULL | Primary key |
| WorkOrderNumber | BIGINT | NOT NULL | Work order identifier |
| LotNumber | VARCHAR(30) | NOT NULL | Component lot number |
| BranchCode | VARCHAR(12) | NULL | Branch code |
| ShortItemNumber | BIGINT | NOT NULL | Component item number |
| Quantity | DECIMAL(15,2) | NOT NULL | Transaction quantity |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
UniqueID(clustered)
Indexes
IX_WorkOrderComponent_Curr_WorkOrderNumberon[WorkOrderNumber]IX_WorkOrderComponent_Curr_LotLookupon[LotNumber, ShortItemNumber, BranchCode]
Requirement: LotUsage_Curr / LotUsage_Hist tables
The system SHALL store cardex entries recording component lot consumption in work orders.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| UniqueID | BIGINT | NOT NULL | Primary key |
| WorkOrderNumber | BIGINT | NOT NULL | Associated work order |
| LotNumber | VARCHAR(30) | NOT NULL | Component lot number |
| BranchCode | VARCHAR(12) | NULL | Branch code |
| ShortItemNumber | BIGINT | NOT NULL | Component item number |
| Quantity | DECIMAL(15,2) | NOT NULL | Transaction quantity |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
UniqueID(clustered)
Indexes
IX_LotUsage_Curr_WorkOrderNumberon[WorkOrderNumber]IX_LotUsage_Curr_LotLookupon[LotNumber, ShortItemNumber, BranchCode]
Business Rules
- Structure identical to WorkOrderComponent tables
- Represents different JDE transaction type (cardex vs component list)
Requirement: WorkOrderRouting table
The system SHALL store work order routing transaction records (F3112Z1).
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| UserID | VARCHAR(40) | NOT NULL | Transaction user ID |
| BatchNumber | VARCHAR(60) | NOT NULL | Transaction batch number |
| TransactionNumber | VARCHAR(88) | NOT NULL | Transaction number |
| LineNumber | INT | NOT NULL | Transaction line number |
| StepNumber | DECIMAL(7,2) | NOT NULL | Operation sequence |
| WorkCenterCode | VARCHAR(12) | NOT NULL | Work center code |
| WorkOrderNumber | BIGINT | NOT NULL | Work order identifier |
| RoutingType | VARCHAR(12) | NULL | Routing type |
| BranchCode | VARCHAR(12) | NULL | Branch code |
| StepDescription | VARCHAR(120) | NULL | Step description |
| FunctionCode | VARCHAR(60) | NULL | Function code |
| TransactionDate | DATETIME | NOT NULL | Transaction original date |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
UserID, BatchNumber, TransactionNumber, LineNumber, StepNumber, WorkCenterCode(clustered composite)
Indexes
IX_WorkOrderRouting_Lookupon[WorkOrderNumber, WorkCenterCode, FunctionCode]INCLUDE[StepNumber]
Business Rules
- No current/historical split (single table)
- Used for MIS matching to determine original operation sequence
Requirement: Lot table
The system SHALL store JDE lot master data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| LotNumber | VARCHAR(30) | NOT NULL | Lot identifier |
| BranchCode | VARCHAR(12) | NOT NULL | Business unit code |
| ShortItemNumber | BIGINT | NOT NULL | Numeric item identifier |
| ItemNumber | VARCHAR(25) | NULL | Item number |
| SupplierCode | BIGINT | NOT NULL | Supplier address number |
| StatusCode | CHAR(1) | NULL | Single-character lot status |
| Memo1 | VARCHAR(30) | NULL | Memo line 1 |
| Memo2 | VARCHAR(30) | NULL | Memo line 2 |
| Memo3 | VARCHAR(30) | NULL | Memo line 3 |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
LotNumber, ShortItemNumber, BranchCode(clustered composite)
Indexes
IX_Lot_SupplierCodeon[SupplierCode]INCLUDE[LotNumber]
Business Rules
- Composite key because same lot number can exist for different items/branches
- StatusCode is single character (CHAR(1))
Requirement: LotLocation table
The system SHALL store JDE lot location tracking data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| LotNumber | VARCHAR(120) | NOT NULL | Lot identifier |
| ShortItemNumber | BIGINT | NOT NULL | Item identifier |
| BranchCode | VARCHAR(12) | NOT NULL | Business unit code |
| Location | VARCHAR(80) | NOT NULL | Physical location code |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
LotNumber, ShortItemNumber, BranchCode, Location(clustered composite)
Business Rules
- Location defaults to empty string
- LotNumber size (120) is larger than Lot table (30) to accommodate JDE location-specific lot identifiers
Requirement: Item table
The system SHALL store JDE item (part type) master data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| ShortItemNumber | BIGINT | NOT NULL | Primary key, numeric identifier |
| ItemNumber | VARCHAR(25) | NOT NULL | Alphanumeric item number |
| Description | VARCHAR(30) | NULL | Item description |
| PlanningFamily | VARCHAR(3) | NULL | Master planning family |
| StockingType | CHAR(1) | NULL | Stocking type code |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
ShortItemNumber(clustered)
Indexes
IX_Item_ItemNumberon[ItemNumber]
Business Rules
- ShortItemNumber is the JDE internal key
- ItemNumber is the human-readable identifier
Requirement: WorkCenter table
The system SHALL store JDE work center reference data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| Code | VARCHAR(12) | NOT NULL | Primary key, work center code |
| Description | VARCHAR(40) | NULL | Work center description |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
Code(clustered)
Indexes
IX_WorkCenter_Lookupon[Description]
Requirement: ProfitCenter table
The system SHALL store JDE profit center reference data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| Code | VARCHAR(12) | NOT NULL | Primary key, profit center code |
| Description | VARCHAR(40) | NULL | Profit center description |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
Code(clustered)
Indexes
IX_ProfitCenter_Lookupon[Description]
Requirement: Branch table
The system SHALL store JDE branch reference data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| Code | VARCHAR(12) | NOT NULL | Primary key, branch code |
| Description | VARCHAR(40) | NULL | Branch description |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
Code(clustered)
Indexes
IX_Branch_Lookupon[Description]
Requirement: JdeUser table
The system SHALL store JDE user (operator) reference data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| AddressNumber | BIGINT | NOT NULL | Primary key, JDE address number |
| UserID | VARCHAR(10) | NULL | Login identifier |
| FullName | VARCHAR(40) | NOT NULL | Full name |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
AddressNumber(clustered)
Indexes
IX_JdeUser_UserIDon[UserID]IX_JdeUser_FullNameon[FullName]
Requirement: StatusCode table
The system SHALL store JDE work order status code lookup data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| Code | VARCHAR(12) | NOT NULL | Primary key, status code |
| Description | VARCHAR(40) | NULL | Status description |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
Code(clustered)
Indexes
IX_StatusCode_Lookupon[Description]
Requirement: FunctionCode table
The system SHALL store JDE function code lookup data.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| Code | VARCHAR(10) | NOT NULL | Primary key, function code |
| Description | VARCHAR(MAX) | NULL | Function description |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
Code(clustered)
Business Rules
- Description is VARCHAR(MAX) (unbounded length) - consider constraining to VARCHAR(2000) in future migration
Requirement: OrgHierarchy table
The system SHALL store organization hierarchy mapping (work center to profit center).
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| WorkCenterCode | VARCHAR(12) | NOT NULL | Work center code |
| BranchCode | VARCHAR(12) | NOT NULL | Branch code |
| ProfitCenterCode | VARCHAR(12) | NOT NULL | Profit center code |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
WorkCenterCode, BranchCode(clustered composite)
Indexes
IX_OrgHierarchy_ProfitCenterCodeon[ProfitCenterCode]INCLUDE[WorkCenterCode]
Business Rules
- Maps work centers to profit centers for filtering
- Same work center can belong to different profit centers by branch
Requirement: RouteMaster table
The system SHALL store JDE item routing master data (F3003).
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| BranchCode | VARCHAR(12) | NOT NULL | Branch code |
| ItemNumber | VARCHAR(25) | NOT NULL | Item number |
| RoutingType | VARCHAR(3) | NOT NULL | Router type |
| SequenceNumber | DECIMAL(7,2) | NOT NULL | Job step number |
| FunctionCode | VARCHAR(15) | NULL | Function code |
| WorkCenterCode | VARCHAR(12) | NULL | Work center code |
| StartDate | DATETIME | NOT NULL | Effectivity start date |
| EndDate | DATETIME | NULL | Effectivity end date |
| LastUpdateDT | DATETIME | NOT NULL | Last update timestamp |
Primary Key
BranchCode, ItemNumber, RoutingType, SequenceNumber, StartDate(clustered composite)
Business Rules
- Used for MIS matching when routing transaction data not available
- EndDate is nullable (open-ended effectivity)
Requirement: MisData table
The system SHALL store CMS MIS (Manufacturing Information System) data from Sybase.
Columns
| Column | Type | Nullable | Description |
|---|---|---|---|
| ItemNumber | VARCHAR(32) | NOT NULL | Item number |
| BranchCode | VARCHAR(32) | NOT NULL | Branch code |
| SequenceNumber | VARCHAR(32) | NOT NULL | Operation step number |
| MisNumber | VARCHAR(32) | NOT NULL | MIS unique number |
| RevID | VARCHAR(32) | NOT NULL | MIS revision ID |
| CharNumber | VARCHAR(32) | NOT NULL | Characteristic number |
| TestDescription | VARCHAR(2000) | NULL | Test description |
| SamplingType | VARCHAR(32) | NULL | Type of sampling |
| SamplingValue | VARCHAR(32) | NULL | Sampling selection value |
| ToolsGauges | VARCHAR(2000) | NULL | Tools and gauges |
| WorkInstructions | VARCHAR(2000) | NULL | Work instructions |
| Status | VARCHAR(32) | NOT NULL | Release status (Current, BackLevel) |
| ReleaseDate | DATETIME | NULL | Release date |
| ObsoleteDate | DATETIME | NULL | Obsolete date |
Primary Key
ItemNumber, BranchCode, SequenceNumber, MisNumber, RevID, Status, CharNumber(clustered composite - 7 columns)
Business Rules
- Sourced from CMS (Sybase), not JDE
- Status values: 'Current', 'BackLevel'
- No LastUpdateDT column (full refresh on each sync)
Requirement: DbUp migration scripts
The system SHALL use DbUp migration scripts to create and maintain the database schema.
Migration Script Organization
- Scripts SHALL be numbered sequentially (NNN_Description.sql)
- Scripts SHALL be embedded as resources in JdeScoping.Database assembly
- Scripts SHALL execute in dependency order (tables → views → types → procedures)
Script Numbering Ranges
| Range | Category |
|---|---|
| 001-025 | Tables |
| 026-032 | Views |
| 033-039 | Table-valued parameter types |
| 040-043 | Stored procedures |
| 044+ | Functions |
Scenario: Fresh database deployment
- WHEN the application starts against an empty database
- THEN DbUp creates all 44 database objects in dependency order
- AND the SchemaVersions table records each applied migration
Scenario: Incremental migration
- WHEN the application starts against a database with some migrations applied
- THEN DbUp applies only new migrations not in SchemaVersions
- AND existing data is preserved
Requirement: Migration idempotency
The system SHALL ensure migration scripts are idempotent for safe re-execution.
Idempotency Patterns
- Tables: Use
IF NOT EXISTSchecks - Views: Use
CREATE OR ALTER VIEW - Types: Check sys.types before creation
- Procedures: Use
CREATE OR ALTER PROCEDURE - Functions: Use
CREATE OR ALTER FUNCTION
Scenario: Re-run migration on existing database
- WHEN a migration script runs against a database where the object already exists
- THEN the script completes without error
- AND the object definition matches the script
Views
Requirement: WorkOrder view
The system SHALL provide a union of WorkOrder_Curr and WorkOrder_Hist tables.
CREATE VIEW dbo.WorkOrder AS
(
SELECT hist.* FROM dbo.WorkOrder_Hist hist
UNION ALL
SELECT curr.* FROM dbo.WorkOrder_Curr curr
)
Scenario: Query all work orders transparently
- WHEN work orders exist in both _Curr and _Hist tables and querying the WorkOrder view
- THEN results include records from both tables seamlessly
Requirement: WorkOrderStep view
The system SHALL provide a union of WorkOrderStep_Curr and WorkOrderStep_Hist with FunctionCode description join.
CREATE VIEW [dbo].[WorkOrderStep] AS
SELECT wos.WorkOrderNumber, wos.WorkCenterCode, wos.StepNumber, wos.StepTypeCode,
wos.BranchCode, wos.StepDescription, wos.StartDT, wos.EndDT, wos.FunctionCode,
fc.Description AS FunctionOperationDescription,
wos.ScrappedQuantity, wos.LastUpdateDT
FROM (
SELECT ... FROM dbo.WorkOrderStep_Hist hist
UNION ALL
SELECT ... FROM dbo.WorkOrderStep_Curr curr
) wos LEFT OUTER JOIN dbo.FunctionCode fc ON (wos.FunctionCode = fc.Code)
Business Rules
- Adds FunctionOperationDescription via LEFT OUTER JOIN to FunctionCode
Scenario: Enrich steps with function descriptions
- WHEN a work order step with FunctionCode = "ASSY" exists and querying WorkOrderStep view
- THEN FunctionOperationDescription is populated from FunctionCode.Description
Scenario: Handle missing function code
- WHEN a work order step with NULL FunctionCode exists and querying WorkOrderStep view
- THEN FunctionOperationDescription is NULL (LEFT OUTER JOIN)
Requirement: WorkOrderTime view
The system SHALL provide a union of WorkOrderTime_Curr and WorkOrderTime_Hist tables.
Requirement: WorkOrderComponent view
The system SHALL provide a union of WorkOrderComponent_Curr and WorkOrderComponent_Hist tables.
Requirement: LotUsage view
The system SHALL provide a union of LotUsage_Curr and LotUsage_Hist tables.
Requirement: LastDataUpdates view
The system SHALL provide a pivot table showing most recent successful update timestamps by table and update type.
CREATE VIEW [dbo].[LastDataUpdates] AS
WITH UPDATE_CTE AS (
SELECT du.TableName, du.UpdateType, du.StartDT,
ROW_NUMBER() OVER (PARTITION BY du.TableName, du.UpdateType ORDER BY du.StartDT DESC) AS RN
FROM dbo.DataUpdate du
WHERE du.WasSuccessful = 1
)
SELECT TableName,
COALESCE([3], '1970-01-01') AS MassUpdateDT,
COALESCE(COALESCE([2], [3]), '1970-01-01') AS DailyUpdateDT,
COALESCE(COALESCE(COALESCE([1], [2]), [3]), '1970-01-01') AS HourlyUpdateDT
FROM (SELECT ... FROM UPDATE_CTE WHERE RN = 1) AS Source
PIVOT (MAX(StartDT) FOR UpdateType IN ([1], [2], [3])) AS PivotTable
Business Rules
- Returns most recent successful update per table for each update type
- Cascades defaults: Hourly falls back to Daily, Daily falls back to Mass
- Default date is '1970-01-01' when no updates exist
Scenario: Get incremental sync window
- WHEN WorkOrder table has Mass update at 2024-01-01 and Daily at 2024-06-15 and querying LastDataUpdates for WorkOrder
- THEN MassUpdateDT = 2024-01-01, DailyUpdateDT = 2024-06-15
Scenario: Cascade to Mass when Daily missing
- WHEN WorkOrder table has only Mass update at 2024-01-01 and querying LastDataUpdates for WorkOrder
- THEN DailyUpdateDT = 2024-01-01 (cascaded from Mass)
Scenario: Default when no updates exist
- WHEN a table has no successful updates and querying LastDataUpdates
- THEN all timestamps return '1970-01-01'
Requirement: WorkOrderTotalScrap view
The system SHALL aggregate scrap quantities by work order.
CREATE VIEW [dbo].[WorkOrderTotalScrap] AS
SELECT wos.WorkOrderNumber,
COALESCE(SUM(wos.ScrappedQuantity), 0) AS TotalScrappedQuantity
FROM dbo.WorkOrderStep wos
WHERE wos.StepNumber = ROUND(wos.StepNumber, 0)
GROUP BY wos.WorkOrderNumber;
Business Rules
- Only counts whole-number steps (excludes sub-steps like 10.01)
- Returns 0 if no scrap exists
Scenario: Sum scrap for whole-number steps only
- WHEN work order 12345 has steps 10, 10.01, 20 with scrap 5, 3, 2 and querying WorkOrderTotalScrap
- THEN TotalScrappedQuantity = 7 (5 + 2, excludes sub-step 10.01)
Scenario: Return zero when no scrap
- WHEN work order 67890 has no scrapped quantities and querying WorkOrderTotalScrap
- THEN TotalScrappedQuantity = 0
Stored Procedures
Requirement: usp_SubmitSearch stored procedure
The system SHALL insert a new search request with Status = Submitted (1).
CREATE PROCEDURE [dbo].[usp_SubmitSearch] (
@p_UserName VARCHAR(128),
@p_Name VARCHAR(128),
@p_Criteria VARCHAR(MAX),
@o_SearchID INT OUTPUT
)
Business Rules
- Sets Status = 1 (Submitted)
- Sets SubmitDT = GETDATE()
- Returns new ID via OUTPUT parameter
Scenario: Submit new search request
- WHEN user "jdoe" submits criteria for Q4 analysis and usp_SubmitSearch is called with UserName, Name, Criteria
- THEN a new Search row is created with Status = 1 and @o_SearchID OUTPUT contains the new ID
.NET 10 Implementation Notes
Decision: Keep as stored procedure. This atomic insert-and-return-ID operation benefits from stored procedure encapsulation. The OUTPUT parameter pattern maps directly to Dapper's Query<int> with @o_SearchID OUTPUT or EF Core's FromSqlRaw.
Requirement: usp_StartSearch stored procedure
The system SHALL mark a search as started.
CREATE PROCEDURE [dbo].[usp_StartSearch] (@p_SearchID INT)
Business Rules
- Sets Status = 2 (Started)
- Sets StartDT = GETDATE()
Scenario: Worker picks up queued search
- WHEN search ID 123 with Status = 1 (Submitted) exists and usp_StartSearch(123) is called
- THEN Status = 2 (Started) and StartDT = current time
.NET 10 Implementation Notes
Decision: Candidate for C# service layer. Simple single-row UPDATE could be implemented as repository method for better testability. However, keeping as stored procedure is acceptable for consistency with other search lifecycle procedures.
Requirement: usp_CompleteSearch stored procedure
The system SHALL mark a search as completed with results.
CREATE PROCEDURE [dbo].[usp_CompleteSearch] (
@p_SearchID INT,
@p_WasSuccessful BIT,
@p_Results VARBINARY(MAX)
)
Business Rules
- Sets Status = 3 (Ended) if successful, 4 (Error) if not
- Sets Results to provided binary data
- Sets EndDT = GETDATE()
Scenario: Search completes successfully
- WHEN search ID 123 with Status = 2 (Started) exists and usp_CompleteSearch(123, 1, ) is called
- THEN Status = 3 (Ended), Results = , EndDT = current time
Scenario: Search fails with error
- WHEN search ID 456 with Status = 2 (Started) exists and usp_CompleteSearch(456, 0, NULL) is called
- THEN Status = 4 (Error), Results = NULL, EndDT = current time
.NET 10 Implementation Notes
Decision: Keep as stored procedure. The conditional status logic (success vs error) and VARBINARY handling make this a good fit for stored procedure. When migrating to blob storage, this procedure would be modified to store a blob URL instead.
Requirement: usp_ResetPartialSearches stored procedure
The system SHALL reset incomplete searches on application startup.
CREATE PROCEDURE [dbo].[usp_ResetPartialSearches]
Business Rules
- Resets Status to 1 (Submitted) for any search with StartDT but no EndDT
- Clears StartDT to NULL
- Used on service startup to recover from crashes
Scenario: Recover from service crash
- WHEN search ID 789 has Status = 2, StartDT = yesterday, EndDT = NULL and usp_ResetPartialSearches is called on service startup
- THEN Status = 1 (Submitted), StartDT = NULL, and the search will be re-processed
Scenario: Ignore completed searches
- WHEN search ID 111 has Status = 3, StartDT and EndDT both set and usp_ResetPartialSearches is called
- THEN search ID 111 is unchanged (EndDT is not NULL)
.NET 10 Implementation Notes
Decision: Keep as stored procedure. Bulk UPDATE with conditional logic is efficient as stored procedure. Called once at startup, so performance is not critical, but atomic execution ensures consistency.
User-Defined Types
Requirement: WorkOrderFilterParameter type
The system SHALL provide a table-valued parameter for work order number filtering.
CREATE TYPE [dbo].[WorkOrderFilterParameter] AS TABLE (
WorkOrderNumber BIGINT
);
Requirement: ItemNumberFilterParameter type
The system SHALL provide a table-valued parameter for item number filtering.
CREATE TYPE [dbo].[ItemNumberFilterParameter] AS TABLE (
ItemNumber VARCHAR(25)
);
Requirement: ProfitCenterFilterParameter type
The system SHALL provide a table-valued parameter for profit center filtering.
CREATE TYPE [dbo].[ProfitCenterFilterParameter] AS TABLE (
Code VARCHAR(12)
);
Requirement: WorkCenterFilterParameter type
The system SHALL provide a table-valued parameter for work center filtering.
CREATE TYPE [dbo].[WorkCenterFilterParameter] AS TABLE (
Code VARCHAR(12)
);
Requirement: OperatorFilterParameter type
The system SHALL provide a table-valued parameter for operator filtering.
CREATE TYPE [dbo].[OperatorFilterParameter] AS TABLE (
UserName VARCHAR(10)
);
Requirement: ComponentLotFilterParameter type
The system SHALL provide a table-valued parameter for component lot filtering.
CREATE TYPE [dbo].[ComponentLotFilterParameter] AS TABLE (
ComponentLotNumber VARCHAR(30),
ItemNumber VARCHAR(128)
);
Requirement: ItemOperationMisFilterParameter type
The system SHALL provide a table-valued parameter for item/operation/MIS filtering.
CREATE TYPE [dbo].[ItemOperationMisFilterParameter] AS TABLE (
ItemNumber VARCHAR(32),
OperationNumber VARCHAR(32),
MisNumber VARCHAR(32),
MisRevision VARCHAR(32)
);
.NET 10 Compatibility Notes
Table-valued parameters (TVPs) are fully supported in .NET 10 with both Dapper and EF Core:
Dapper approach:
// Using DataTable
var table = new DataTable();
table.Columns.Add("WorkOrderNumber", typeof(long));
foreach (var wo in workOrders)
table.Rows.Add(wo);
var param = new { filter = table.AsTableValuedParameter("WorkOrderFilterParameter") };
Modern alternative with IEnumerable<SqlDataRecord>:
// More memory-efficient for large datasets
public static IEnumerable<SqlDataRecord> ToWorkOrderRecords(IEnumerable<long> workOrders)
{
var metadata = new SqlMetaData("WorkOrderNumber", SqlDbType.BigInt);
var record = new SqlDataRecord(metadata);
foreach (var wo in workOrders)
{
record.SetInt64(0, wo);
yield return record;
}
}
EF Core approach (EF Core 8+):
// EF Core supports TVPs via FromSqlRaw with structured parameters
var results = await context.WorkOrders
.FromSqlRaw("SELECT * FROM dbo.WorkOrder WHERE WorkOrderNumber IN (SELECT WorkOrderNumber FROM @filter)",
new SqlParameter("@filter", SqlDbType.Structured) { TypeName = "WorkOrderFilterParameter", Value = table })
.ToListAsync();
Functions
Requirement: Search Criteria Extraction Functions
The system SHALL provide SQL functions to extract search criteria values from the Search.Criteria JSON column.
Scalar Functions
| Function | Return Type | Description |
|---|---|---|
fn_GetSearchMinimumDt(@SearchId INT) |
DATETIME2(7) | Extracts $.MinimumDt value |
fn_GetSearchMaximumDt(@SearchId INT) |
DATETIME2(7) | Extracts $.MaximumDt value |
fn_GetSearchExtractMisData(@SearchId INT) |
BIT | Extracts $.ExtractMisData value |
Table-Valued Functions (Simple Arrays)
| Function | Return Columns | Description |
|---|---|---|
fn_GetSearchWorkOrders(@SearchId INT) |
WorkOrderNumber BIGINT | Extracts $.WorkOrderNumbers array |
fn_GetSearchItemNumbers(@SearchId INT) |
ItemNumber VARCHAR(128) | Extracts $.ItemNumbers array |
fn_GetSearchProfitCenters(@SearchId INT) |
Code VARCHAR(12) | Extracts $.ProfitCenters array |
fn_GetSearchWorkCenters(@SearchId INT) |
Code VARCHAR(12) | Extracts $.WorkCenters array |
fn_GetSearchOperatorIDs(@SearchId INT) |
OperatorID VARCHAR(128) | Extracts $.OperatorIDs array |
Table-Valued Functions (Complex Objects)
| Function | Return Columns | Description |
|---|---|---|
fn_GetSearchComponentLots(@SearchId INT) |
LotNumber VARCHAR(30), ItemNumber VARCHAR(128) | Extracts $.ComponentLotNumbers array |
fn_GetSearchPartOperations(@SearchId INT) |
ItemNumber VARCHAR(128), OperationNumber VARCHAR(10), MisNumber VARCHAR(10), MisRevision VARCHAR(10) | Extracts $.PartOperations array |
Business Rules
- Scalar functions return NULL if search not found, criteria is NULL, or JSON is invalid
- Table-valued functions return empty result set if search not found, criteria is NULL, or JSON is invalid
- TVFs use CTE pattern to pre-filter valid JSON before OPENJSON (prevents runtime errors)
- TVFs use
OPENJSON...WITHsyntax for type-safe extraction
Scenario: Extract work order filter values
- WHEN Search ID 123 has Criteria containing
{"WorkOrderNumbers":[12345,67890]} - THEN
SELECT * FROM dbo.fn_GetSearchWorkOrders(123)returns two rows with WorkOrderNumber values 12345 and 67890
Scenario: Handle invalid JSON gracefully
- WHEN Search ID 456 has Criteria containing invalid JSON text
- THEN extraction functions return NULL (scalar) or empty result set (TVF) without throwing errors
Requirement: Validate Search Criteria Procedure
The system SHALL provide a stored procedure for strict validation of search criteria with error reporting.
Procedure Signature
CREATE PROCEDURE dbo.usp_ValidateSearchCriteria(@SearchId INT)
Error Codes
| Error Code | Condition | Message Pattern |
|---|---|---|
| 50001 | Search ID not found | "Search ID {id} not found" |
| 50002 | Criteria is NULL or empty | "Search ID {id} has no criteria" |
| 50003 | Criteria is not valid JSON | "Search ID {id} has invalid JSON" |
Business Rules
- Procedure throws errors using
THROWstatement for calling code to handle - Procedure returns 0 (success) when validation passes
- Used for pre-flight validation before query execution
Scenario: Validate valid search
- WHEN
usp_ValidateSearchCriteriais called for a search with valid JSON criteria - THEN procedure returns 0 (success) without throwing
Scenario: Validate missing search
- WHEN
usp_ValidateSearchCriteriais called for non-existent search ID 99999 - THEN procedure throws error 50001 with message "Search ID 99999 not found"
Requirement: fn_MatchMIS function
The system SHALL provide a table-valued function to match work order steps to MIS data.
Parameters
| Parameter | Type | Description |
|---|---|---|
| @workOrderNumber | BIGINT | Work order to match |
| @itemNumber | VARCHAR(25) | Item number |
| @branchCode | VARCHAR(12) | Branch code |
| @routingType | VARCHAR(3) | Routing type |
| @issueDate | DATETIME | Work order issue date |
| @workCenterCode | VARCHAR(12) | Work center code |
| @sequenceNumber | DECIMAL(7,2) | Step sequence number |
| @steptimestamp | DATETIME | Step timestamp |
| @functionCode | VARCHAR(15) | Function code |
| @functionOperationDescription | VARCHAR(80) | Function description |
Return Table
| Column | Type | Description |
|---|---|---|
| WorkOrderNumber | BIGINT | Input work order |
| ItemNumber | VARCHAR(25) | Input item |
| ItemDescription | VARCHAR(30) | Item description lookup |
| BranchCode | VARCHAR(12) | Input branch |
| WorkCenterCode | VARCHAR(12) | Input work center |
| StepTimestamp | DATETIME | Input timestamp |
| SequenceNumber | DECIMAL(7,2) | Input sequence |
| FunctionCode | VARCHAR(15) | Input function code |
| FunctionOperationDescription | VARCHAR(80) | Input function description |
| MatchedSequenceNumber | DECIMAL(7,2) | Matched sequence from routing |
| RoutingMatch | BIT | Found via F3112Z1 routing |
| MasterMatch | BIT | Found via F3003 master routing |
| MisNumber | VARCHAR(32) | MIS number |
| RevID | VARCHAR(32) | MIS revision |
| CharNumber | VARCHAR(32) | Characteristic number |
| MisSequenceNumber | VARCHAR(32) | MIS sequence number |
| TestDescription | VARCHAR(2000) | Test description |
| SamplingType | VARCHAR(32) | Sampling type |
| SamplingValue | VARCHAR(32) | Sampling value |
| ToolsGauges | VARCHAR(2000) | Tools and gauges |
| WorkInstructions | VARCHAR(2000) | Work instructions |
| Status | VARCHAR(32) | MIS status |
| ReleaseDate | DATETIME | MIS release date |
Business Rules
- Looks up item description from Item table
- Finds parent work order (if exists)
- Searches F3112Z1 (WorkOrderRouting) for sequence aliases matching work center + function code
- Falls back to F3003 (RouteMaster) if no routing matches found
- Attempts to match MIS data with Status = 'Current' first
- Falls back to Status = 'BackLevel' if no current match
- Returns routing/master matches without MIS if no MIS data found
- Returns input parameters with NULL MIS fields if no matches at all
Scenario: Match via routing with current MIS
- WHEN work order 12345, step 10 with function code "ASSY" is processed and fn_MatchMIS finds a routing entry in F3112Z1 and MIS data exists with Status = 'Current'
- THEN RoutingMatch = 1, MasterMatch = 0, MIS fields populated
Scenario: Fallback to master routing
- WHEN work order 12345, step 10 with no routing entry is processed and fn_MatchMIS finds match in F3003 RouteMaster
- THEN RoutingMatch = 0, MasterMatch = 1, MatchedSequenceNumber from master
Scenario: Fallback to BackLevel MIS
- WHEN routing match found but no 'Current' MIS exists and 'BackLevel' MIS exists for the item/operation
- THEN Status = 'BackLevel' MIS data is returned
Scenario: No MIS data available
- WHEN routing match found but no MIS data exists and fn_MatchMIS completes
- THEN RoutingMatch/MasterMatch set, MIS fields = NULL
Domain Model Cross-Reference
| Database Table | Domain Model Entity | Notes |
|---|---|---|
| Search | Search | Direct mapping; Criteria column maps to CriteriaJSON property |
| DataUpdate | DataUpdate | Direct mapping |
| WorkOrder (view) | WorkOrder | Access via view; _Curr/_Hist not exposed |
| WorkOrderStep (view) | WorkOrderStep | View adds FunctionOperationDescription |
| WorkOrderTime (view) | WorkOrderTime | Direct mapping via view |
| WorkOrderComponent (view) | WorkOrderComponent | Direct mapping via view |
| WorkOrderRouting | WorkOrderRouting | Single table, no view |
| LotUsage (view) | LotUsage | Direct mapping via view |
| Lot | Lot | Direct mapping |
| LotLocation | LotLocation | Direct mapping |
| Item | Item | Direct mapping |
| WorkCenter | WorkCenter | Direct mapping |
| ProfitCenter | ProfitCenter | Direct mapping |
| Branch | Branch | Direct mapping |
| JdeUser | JdeUser | Direct mapping |
| StatusCode | StatusCode | Direct mapping |
| FunctionCode | FunctionCode | Direct mapping |
| OrgHierarchy | OrgHierarchy | Direct mapping |
| RouteMaster | RouteMaster | Direct mapping |
| MisData | MisData | ObsoleteDate column included in domain model |
EF Core Compatibility Notes
Schema Patterns
The schema is compatible with EF Core 8+ with the following considerations:
-
Composite Primary Keys: EF Core supports composite keys but requires explicit configuration via
HasKey()inOnModelCreating. The 7-column composite key on MisData is supported but may be cumbersome; consider using[Keyless]entity with manual queries if write operations are not needed. -
Views as Entities: Union views (WorkOrder, WorkOrderStep, etc.) can be mapped as keyless entities using
HasNoKey()andToView(). This provides read-only access which matches the intended usage. -
Table-Valued Functions: MatchMIS can be called via
FromSqlRawor configured as a TVF mapping in EF Core 8+. -
No Foreign Keys: The schema intentionally omits foreign key constraints for bulk load performance. EF Core navigation properties can still be configured using
.HasForeignKey()without database constraints (shadow foreign keys). -
DECIMAL Precision: Ensure
HasPrecision(15, 2)orHasPrecision(18, 2)is configured for decimal columns to match database precision.
Recommended EF Core Configuration
// For views - read-only access
modelBuilder.Entity<WorkOrder>()
.HasNoKey()
.ToView("WorkOrder");
// For composite keys
modelBuilder.Entity<WorkOrderStep>()
.HasKey(e => new { e.WorkOrderNumber, e.WorkCenterCode, e.StepNumber, e.StepTypeCode });
// For navigation without FK constraints
modelBuilder.Entity<WorkOrderTime>()
.HasOne<JdeUser>()
.WithMany()
.HasForeignKey(e => e.AddressNumber)
.HasPrincipalKey(u => u.AddressNumber);
Migration Notes
Data Type Recommendations
| Current Type | Recommended Type | Rationale | Migration Priority |
|---|---|---|---|
| DATETIME | DATETIME2(7) | Better precision (100ns vs 3.33ms), wider range (0001-9999 vs 1753-9999), no daylight saving ambiguity | Future - new columns only |
| GETDATE() | SYSUTCDATETIME() | UTC timestamps for distributed systems consistency | Future - new procedures |
| VARCHAR(MAX) for Criteria | NVARCHAR(MAX) | Unicode support for international characters | Low - backward compatible |
| SMALLINT for Status | INT | Consistent with .NET enum defaults (int) | Low - no functional impact |
| VARBINARY(MAX) for Results | Azure Blob Storage URL | Reduces database size, improves scalability | Medium - see blob storage notes |
| VARCHAR(MAX) for Description | VARCHAR(2000) | Bounded length improves query planning | Low - FunctionCode only |
Excel Results Storage Migration Path
Current implementation stores Excel results as VARBINARY(MAX) in the Search.Results column. For improved scalability:
Phase 1 (Current): Keep VARBINARY(MAX) for initial .NET 10 migration
- Maintains backward compatibility
- No infrastructure changes required
- Suitable for single-database deployment
Phase 2 (Future): Migrate to Azure Blob Storage
- Add
ResultsBlobUrl VARCHAR(500)column to Search table - Modify CompleteSearch to store blob URL instead of binary
- Create migration script to move existing Results to blob storage
- Eventually remove Results VARBINARY(MAX) column
Blob Storage Benefits:
- Database size reduction (Excel files typically 100KB-10MB each)
- Better backup/restore performance
- CDN integration for download acceleration
- Lifecycle management (auto-delete old results)
Stored Procedure Evaluation Summary
| Procedure | Decision | Rationale |
|---|---|---|
| usp_SubmitSearch | Keep | Atomic INSERT with OUTPUT parameter |
| usp_StartSearch | Keep (or migrate) | Simple UPDATE, but consistency with lifecycle |
| usp_CompleteSearch | Keep | Conditional logic, VARBINARY handling |
| usp_ResetPartialSearches | Keep | Bulk conditional UPDATE at startup |
All procedures can be called from .NET 10 via Dapper or EF Core. Migration to C# service layer is optional and would primarily benefit unit testing (mockable repository interfaces).
Current/Historical Table Pattern
The _Curr / _Hist suffix pattern partitions data based on work order status:
_Curr: Active work orders (typically status < 90)_Hist: Completed/closed work orders
Future Consideration: SQL Server table partitioning could replace the dual-table pattern:
- Single table with partition function on StatusCode or date
- Simplifies application code (no UNION ALL views)
- Enables partition switching for archive operations
- Requires SQL Server Enterprise Edition or Azure SQL
Index Coverage for Historical Tables
The following indexes should be verified on historical tables (matching current table patterns):
IX_WorkOrder_Hist_ParentWorkOrderNumberIX_WorkOrder_Hist_ItemNumberIX_WorkOrderStep_Hist_WorkCenterCodeIX_WorkOrderTime_Hist_LookupIX_WorkOrderTime_Hist_AddressNumberIX_WorkOrderComponent_Hist_WorkOrderNumberIX_WorkOrderComponent_Hist_LotLookupIX_LotUsage_Hist_WorkOrderNumberIX_LotUsage_Hist_LotLookup
Resolved Questions
Question 1: Setup.sql vs sqlproj scripts
Resolution: The sqlproj scripts in Tables/ folder are authoritative. Setup.sql appears to be an older bootstrapping script that predates the current _Curr/_Hist partitioning pattern. The migration should use the sqlproj-defined schema.
Question 2: WorkOrderScrap table conflict
Resolution: Use WorkOrderStep.ScrappedQuantity pattern (from sqlproj). The WorkOrderScrap table in Setup.sql is deprecated. The WorkOrderTotalScrap view should aggregate from WorkOrderStep, not a separate WorkOrderScrap table.
Question 3: LotLocation.LotNumber size (120 vs 30)
Resolution: Intentional design. LotLocation tracks physical locations which may use extended lot identifiers that concatenate base lot number with location-specific suffixes. The 120-character limit accommodates these extended identifiers.
Question 4: MisData.ObsoleteDate in domain model
Resolution: Include ObsoleteDate in domain model. This column tracks MIS revision obsolescence and is useful for audit/reporting purposes.
Question 5: FunctionCode.Description size
Resolution: Keep VARCHAR(MAX) for backward compatibility. Document as a future migration candidate to VARCHAR(2000) if query performance issues arise.
Question 6: No foreign keys
Resolution: Intentional for bulk load performance. The cache database receives large bulk inserts from JDE/CMS sync processes. Foreign key constraints would significantly slow these operations. Referential integrity is maintained at the application layer.
Question 7: Composite primary keys
Resolution: Keep existing composite keys. While surrogate keys would simplify EF Core mapping, the existing composite keys accurately represent the natural business keys and have worked reliably in production. The 7-column MisData key can be handled with [Keyless] mapping if needed.
Question 8: Data partitioning strategy
Resolution: Work orders are partitioned based on the WDSTEP (status) field from JDE. Status codes >= 90 indicate completed/closed work orders and are stored in _Hist tables. The sync process handles this partitioning during data refresh.
Question 9: Index coverage
Resolution: Historical tables should mirror current table indexes. See "Index Coverage for Historical Tables" section above.
Question 10: MisData cache refresh
Resolution: MisData uses full table refresh (TRUNCATE + bulk INSERT) rather than incremental sync. The CMS source system does not provide reliable change tracking, so full refresh is the only option. This is acceptable given the relatively small MisData row count.
Question 11: Results storage
Resolution: Keep VARBINARY(MAX) for initial .NET 10 migration. Plan for Azure Blob Storage migration as Phase 2 enhancement. See "Excel Results Storage Migration Path" section.
Question 12: Search.Criteria encryption
Resolution: Not required for initial migration. Search criteria contains filter values (dates, work order numbers) but no PII or sensitive data. Encryption at rest can be achieved via SQL Server Transparent Data Encryption (TDE) if needed at the database level.
Question 13: Audit columns
Resolution: Defer to future enhancement. Existing LastUpdateDT columns provide basic audit capability. Full audit (CreatedDT, CreatedBy, ModifiedDT, ModifiedBy) can be added as a Phase 2 enhancement if compliance requirements dictate.
Question 14: ScrappedQuantity default
Resolution: Add DEFAULT constraint in migration script. The application logic assumes default of 0, so the database should enforce this:
ALTER TABLE dbo.WorkOrderStep_Curr ADD CONSTRAINT DF_WorkOrderStep_Curr_ScrappedQuantity DEFAULT 0 FOR ScrappedQuantity;
ALTER TABLE dbo.WorkOrderStep_Hist ADD CONSTRAINT DF_WorkOrderStep_Hist_ScrappedQuantity DEFAULT 0 FOR ScrappedQuantity;
Codex Review Findings
This section documents findings from automated and manual code review.
Finding 1: Inconsistent timestamp columns
Some tables use DATETIME while the migration notes recommend DATETIME2(7). For backward compatibility, existing DATETIME columns will be preserved. New columns added during migration should use DATETIME2(7).
Finding 2: Missing indexes on historical tables
Historical tables (_Hist) may be missing indexes that exist on current tables (_Curr). Verify index parity during migration. See "Index Coverage for Historical Tables" section.
Finding 3: VARCHAR(MAX) usage
FunctionCode.Description uses VARCHAR(MAX). While functional, unbounded columns can impact query optimizer decisions. Monitor for performance issues; consider migration to VARCHAR(2000) if problems arise.