# Migration Plan: Valueset VID → VValue ## Overview Transition from using database `valueset` table (VID as primary key) to the new `App\Libraries\ValueSet` library (VValue as key). This eliminates database joins for lookup values and uses JSON-based static lookup files. ## Current State - Database `valueset` table with columns: `VID` (PK, INT), `VValue` (VARCHAR), `VDesc` (VARCHAR) - 30+ places using `->join('valueset', 'valueset.VID = ...')` - Selects use `valueset.VValue` and `valueset.VDesc` for display text ## Target State - Use `App\Libraries\ValueSet` library with `getLabel(lookupName, key)` method - Lookup names use table-prefixed PascalCase (e.g., `patient_Sex`, `test_TestType`, `container_ContainerCapColor`) - All fields store `VValue` codes directly (e.g., '1', '2', 'M', 'F', 'TEST') - Remove all `valueset` table joins from queries - Keep raw field values for codes; use `ValueSet::getLabel()` for display text - JSON files in `app/Libraries/Data/valuesets/` are already populated --- ## Phase 1: JSON Files Rename Rename all JSON files in `app/Libraries/Data/valuesets/` to use table-prefixed PascalCase format: | Old Name | New Name | Source Table | Field | |----------|----------|--------------|-------| | `gender.json` | `patient_Sex.json` | patient | Gender | | `country.json` | `patient_Country.json` | patient | Country | | `race.json` | `patient_Race.json` | patient | Race | | `religion.json` | `patient_Religion.json` | patient | Religion | | `ethnic.json` | `patient_Ethnic.json` | patient | Ethnic | | `marital_status.json` | `patient_MaritalStatus.json` | patient | MaritalStatus | | `death_indicator.json` | `patient_DeathIndicator.json` | patient | DeathIndicator | | `test_type.json` | `test_TestType.json` | testdefsite | TestType | | `container_cap_color.json` | `container_ContainerCapColor.json` | containerdef | Color | | `container_class.json` | `container_ContainerClass.json` | containerdef | ConClass | | `additive.json` | `container_Additive.json` | containerdef | Additive | | `location_type.json` | `location_LocationType.json` | location | LocType | | `ws_type.json` | `organization_WorkstationType.json` | workstation | Type | | `enable_disable.json` | `organization_EnableDisable.json` | workstation | Enable | | `site_type.json` | `organization_SiteType.json` | site | SiteTypeID | | `site_class.json` | `organization_SiteClass.json` | site | SiteClassID | | `numeric_ref_type.json` | `ref_NumericRefType.json` | refnum | NumRefType | | `range_type.json` | `ref_RangeType.json` | refnum | RangeType | | `text_ref_type.json` | `ref_TextRefType.json` | reftxt | TxtRefType | | `reference_type.json` | `test_ReferenceType.json` | testdeftech | RefType | | `math_sign.json` | `ref_MathSign.json` | refnum | LowSign, HighSign | | `country.json` | `account_Country.json` | account | Country | | ... | ... | ... | ... | All lookup names use `{table}_{Field}` format for clarity and namespace isolation. --- ## Phase 2: Database Schema Migration ### Files to DELETE | File | Action | |------|--------| | `app\Database\Seeds\ValueSetSeeder.php` | DELETE | | `app\Database\Seeds\ValueSetCountrySeeder.php` | DELETE | | `app\Database\Seeds\MinimalMasterDataSeeder.php` | DELETE | | `app\Database\Seeds\PatientSeeder.php` | DELETE | | `app\Database\Migrations\2025-09-15-130122_ValueSet.php` | DELETE | ### Migration: Modify Columns INT → VARCHAR(10) **File:** `app\Database\Migrations\2026-01-12-000001_ValuesetVidToVvalue.php` ```php forge->modifyColumn('patient', [ 'Gender' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'Race' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'Religion' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'Ethnic' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'MaritalStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'DeathIndicator' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // testdefsite table $this->forge->modifyColumn('testdefsite', [ 'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false], ]); // containerdef table $this->forge->modifyColumn('containerdef', [ 'Additive' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'ConClass' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'Color' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // location table $this->forge->modifyColumn('location', [ 'LocType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // workstation table $this->forge->modifyColumn('workstation', [ 'Type' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'Enable' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // site table $this->forge->modifyColumn('site', [ 'SiteTypeID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'SiteClassID' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // account table $this->forge->modifyColumn('account', [ 'Country' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // refnum table $this->forge->modifyColumn('refnum', [ 'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'NumRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'RangeType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'LowSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'HighSign' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // reftxt table $this->forge->modifyColumn('reftxt', [ 'Sex' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], 'TxtRefType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => true], ]); // orderstatus table $this->forge->modifyColumn('orderstatus', [ 'OrderStatus' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false], ]); } public function down() { // Revert to INT $this->forge->modifyColumn('patient', [ 'Gender' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'Race' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'Religion' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'Ethnic' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'MaritalStatus' => ['type' => 'TINYINT', 'null' => true], 'DeathIndicator' => ['type' => 'INT', 'constraint' => 11, 'null' => true], ]); $this->forge->modifyColumn('testdefsite', [ 'TestType' => ['type' => 'INT', 'null' => false], ]); $this->forge->modifyColumn('containerdef', [ 'Additive' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'ConClass' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'Color' => ['type' => 'INT', 'constraint' => 11, 'null' => true], ]); $this->forge->modifyColumn('location', [ 'LocType' => ['type' => 'INT', 'null' => true], ]); $this->forge->modifyColumn('workstation', [ 'Type' => ['type' => 'TINYINT', 'null' => true], 'Enable' => ['type' => 'INT', 'null' => true], ]); $this->forge->modifyColumn('site', [ 'SiteTypeID' => ['type' => 'INT', 'null' => true], 'SiteClassID' => ['type' => 'INT', 'null' => true], ]); $this->forge->modifyColumn('account', [ 'Country' => ['type' => 'INT', 'constraint' => 11, 'null' => true], ]); $this->forge->modifyColumn('refnum', [ 'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'NumRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'RangeType' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'LowSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'HighSign' => ['type' => 'INT', 'constraint' => 11, 'null' => true], ]); $this->forge->modifyColumn('reftxt', [ 'Sex' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'TxtRefType' => ['type' => 'INT', 'constraint' => 11, 'null' => true], ]); $this->forge->modifyColumn('orderstatus', [ 'OrderStatus' => ['type' => 'INT', 'null' => false], ]); } } ``` **Note:** No data migration needed - dummy data will be lost. This is acceptable for development/testing environments. --- ## Phase 3: Library & Model Updates ### ValueSet Library - Update to read from JSON files **File:** `app/Libraries/ValueSet.php` Ensure the library reads from JSON files in `app/Libraries/Data/valuesets/`: ```php $item['key'] ?? '', 'value' => $item['value'] ?? $item['label'] ?? '', ]; }, $values); } public static function transformLabels(array $data, array $fieldMappings): array { foreach ($data as &$row) { foreach ($fieldMappings as $field => $lookupName) { if (isset($row[$field]) && $row[$field] !== null) { $row[$field . 'Text'] = self::getLabel($lookupName, $row[$field]) ?? ''; } } } return $data; } } ``` ### ValueSetModel - Deprecate or Repurpose **File:** `app/Models/ValueSet/ValueSetModel.php` Options: 1. **Deprecate entirely** - No longer needed after migration 2. **Repurpose for JSON file management** - Read/write to JSON files 3. **Keep as-is for backward compatibility** - If database valuesets are still needed Recommended: Deprecate and remove references after migration. --- ## Phase 4: Model Changes ### Pattern for Model Updates **Before:** ```php $this->select("..., gender.VValue as Gender, gender.VDesc as GenderText") ->join('valueset gender', 'gender.VID = patient.Gender', 'left') ``` **After:** ```php use App\Libraries\ValueSet; $this->select("..., patient.Gender"); // After fetching: $rows = ValueSet::transformLabels($rows, [ 'Gender' => 'patient_Sex', 'Country' => 'patient_Country', 'Race' => 'patient_Race', 'Religion' => 'patient_Religion', 'Ethnic' => 'patient_Ethnic', 'DeathIndicator' => 'patient_DeathIndicator', 'MaritalStatus' => 'patient_MaritalStatus', ]); ``` ### Models to Modify (8 files) #### 1. `app/Models/Patient/PatientModel.php` **Remove:** - Line 27: `$this->join('valueset vs', 'vs.vid = Gender', 'left');` - Lines 52-64: All `*.VID as *VID` aliases - Lines 75-81: All `valueset.*` joins **Add transformation in `getPatient()`:** ```php $patient = ValueSet::transformLabels([$patient], [ 'Gender' => 'patient_gender', 'Country' => 'patient_country', 'Race' => 'patient_race', 'Religion' => 'patient_religion', 'Ethnic' => 'patient_ethnic', 'DeathIndicator' => 'patient_death_indicator', 'MaritalStatus' => 'patient_marital_status', ])[0]; ``` #### 2. `app/Models/Location/LocationModel.php` **Remove:** - Lines 18, 30: `->join("valueset v", "v.VID=location.loctype", ...)` **Add transformation:** ```php $rows = ValueSet::transformLabels($rows, [ 'LocType' => 'location_LocationType', ]); ``` #### 3. `app/Models/Test/TestDefSiteModel.php` **Remove:** - Lines 42, 75, 103: `->join("valueset", "valueset.VID=...")` **Add transformation:** ```php $rows = ValueSet::transformLabels($rows, [ 'TestType' => 'test_TestType', ]); ``` #### 4. `app/Models/Test/TestDefGrpModel.php` **Remove:** - Line 32: `->join('valueset vs', 'vs.VID=t.TestType', 'left')` #### 5. `app/Models/Specimen/ContainerDefModel.php` **Remove:** - Lines 20-22, 37-39: All 6 `valueset.*` joins **Add transformation:** ```php $rows = ValueSet::transformLabels($rows, [ 'Color' => 'container_ContainerCapColor', 'ConClass' => 'container_ContainerClass', 'Additive' => 'container_Additive', ]); ``` #### 6. `app/Models/Organization/SiteModel.php` **Remove:** - Lines 38-39: `->join('valueset sitetype'...)` and `->join('valueset siteclass'...)` **Add transformation:** ```php $row = ValueSet::transformLabels([$row], [ 'SiteTypeID' => 'organization_SiteType', 'SiteClassID' => 'organization_SiteClass', ])[0]; ``` #### 7. `app/Models/Organization/AccountModel.php` **Remove:** - Line 41: `->join('valueset country'...)` **Remove from select:** - Line 36: `country.VID as country` **Add transformation in controller if needed:** ```php $rows = ValueSet::transformLabels($rows, [ 'Country' => 'account_Country', ]); ``` #### 8. `app/Models/Organization/WorkstationModel.php` **Remove:** - Lines 36-37: `->join('valueset wstype'...)` and `->join('valueset enable'...)` **Add transformation:** ```php $row = ValueSet::transformLabels([$row], [ 'Type' => 'organization_WorkstationType', 'Enable' => 'organization_EnableDisable', ])[0]; ``` --- ## Phase 5: Controller Changes ### `app/Controllers/TestsController.php` **Remove:** - Line 69: `->join("valueset", "valueset.VID=testdefsite.TestType", "left")` - Line 111: `->join("valueset", "valueset.VID=testdefsite.TestType", "left")` - Line 140: `->join('valueset vs', 'vs.VID=t.TestType', 'left')` **Replace `getVValue()` method:** ```php private function getVValue($vsetID, $vid) { // DEPRECATED - Use ValueSet::getLabel() instead return null; } ``` **Update references from `getVValue()` to `ValueSet::getLabel()`:** ```php // Before: 'NumRefTypeVValue' => $this->getVValue(46, $r['NumRefType']), // After: 'NumRefTypeVValue' => \App\Libraries\ValueSet::getLabel('ref_NumericRefType', $r['NumRefType']), ``` **VSetID to Lookup Name Mapping:** | VSetID | Constant | Lookup Name | |--------|----------|-------------| | 44 | `VALUESET_REF_TYPE` | `test_ReferenceType` | | 45 | `VALUESET_RANGE_TYPE` | `ref_RangeType` | | 46 | `VALUESET_NUM_REF_TYPE` | `ref_NumericRefType` | | 47 | `VALUESET_TXT_REF_TYPE` | `ref_TextRefType` | | 3 | `VALUESET_SEX` | `patient_Sex` | | 41 | `VALUESET_MATH_SIGN` | `ref_MathSign` | **Update `getValuesetOptions()` to use JSON:** ```php private function getValuesetOptions($lookupName) { return \App\Libraries\ValueSet::getOptions($lookupName); } ``` --- ## Phase 6: API Endpoints - Replace with JSON-based endpoints ### New API Controller: `app/Controllers/ValueSetApiController.php` ```php respond([ 'status' => 'success', 'data' => $data ], 200); } public function all() { $dir = APPPATH . 'Libraries/Data/valuesets/'; $files = glob($dir . '*.json'); $result = []; foreach ($files as $file) { $name = basename($file, '.json'); $result[] = [ 'name' => $name, 'options' => ValueSet::getOptions($name) ]; } return $this->respond([ 'status' => 'success', 'data' => $result ], 200); } } ``` ### Update Routes: `app/Config/Routes.php` ```php $routes->group('api', function ($routes) { $routes->get('valueset/(:segment)', 'ValueSetApiController::index/$1'); $routes->get('valueset', 'ValueSetApiController::all'); }); ``` --- ## Phase 7: View Updates ### 1. `app/Views/v2/master/valuesets/valuesets_index.php` Repurpose to manage JSON-based valuesets instead of database table. | Before | After | |--------|-------| | `fetch(...api/valueset...)` | `fetch(...api/valueset/lookupName...)` | | Database CRUD operations | File-based CRUD operations | ### 2. `app/Views/v2/master/valuesets/valueset_nested_crud.php` Repurpose for JSON file management. ### 3. `app/Views/v2/master/valuesets/valueset_dialog.php` Update for JSON file format. ### 4. `app/Views/v2/master/tests/tests_index.php` | Before | After | |--------|-------| | `type?.VID` | `type?.key` | | `type?.VValue` | `type?.value` | | `type?.VDesc` | `type?.label` | | `{ VID: 1, VValue: 'TEST', ... }` | `{ key: 'TEST', value: 'Test', ... }` | | `getTypeName(vid)` | `getTypeName(value)` | | `api/valuesetdef/27` | `api/valueset/test_TestType` | | Hardcoded fallback: `{ VID: 1, VValue: 'TEST', VDesc: 'Test' }` | `{ key: 'TEST', value: 'Test' }` | ### 5. Additional Views to Update | View File | Fields to Update | Lookup Name | |-----------|------------------|-------------| | `app/Views/v2/patients/patients_index.php` | Gender, Country, Race, Religion, Ethnic, MaritalStatus, DeathIndicator | `patient_*` | | `app/Views/v2/master/specimen/containers_index.php` | Color, ConClass, Additive | `container_*` | | `app/Views/v2/master/organization/sites_index.php` | SiteTypeID, SiteClassID | `organization_*` | | `app/Views/v2/master/organization/workstations_index.php` | Type, Enable | `organization_*` | | `app/Views/v2/master/organization/accounts_index.php` | Country | `account_Country` | | `app/Views/v2/master/organization/locations_index.php` | LocType | `location_LocationType` | --- ## Phase 8: Test Files Update ### `tests/feature/ValueSet/ValueSetApiControllerTest.php` Update tests to use new JSON-based API endpoints. ### `tests/_support/v2/MasterTestCase.php` Update any valueset-related test data setup. --- ## Testing Checklist 1. **Unit Tests** - Test `ValueSet::getLabel('patient_Sex', '1')` returns 'Female' - Test `ValueSet::getLabel('test_TestType', 'TEST')` returns 'Test' - Test `ValueSet::getOptions('container_ContainerCapColor')` returns correct format - Test `ValueSet::transformLabels()` with table-prefixed field mappings 2. **Integration Tests** - Patient CRUD (create, read, update, delete) - Test definition CRUD - Location CRUD - Container definition CRUD - Organization (site, account, workstation) CRUD 3. **Manual Testing** - Verify all dropdowns display correct labels - Verify filtering by valueset fields works - Verify form submissions save correct VValue codes --- ## Rollback Plan 1. Run migration `down()` to revert column types 2. Restore deleted seeders from git if needed 3. Restore deleted migration file from git --- ## Files Summary | Phase | Action | Files | |-------|--------|-------| | 1 | RENAME | ~50 JSON files in `app/Libraries/Data/valuesets/` | | 2 | DELETE | 5 seeders, 1 migration | | 2 | CREATE | 1 migration (column changes) | | 3 | UPDATE | 1 library (ValueSet.php), ValueSetModel.php deprecation | | 4 | UPDATE | 8 Model files | | 5 | UPDATE | 1 Controller, Routes | | 6 | CREATE | 1 Controller (ValueSetApiController.php) | | 6 | UPDATE | ~6+ View files | | 8 | UPDATE | 2+ Test files | --- ## Estimated Effort - Phase 1 (JSON Rename): 15 minutes - Phase 2 (Migration): 30 minutes - Phase 3 (Library + Model): 30 minutes - Phase 4 (Models): 1.5 hours - Phase 5 (Controller): 30 minutes - Phase 6 (API + Views): 2 hours - Phase 8 (Tests): 30 minutes - Testing: 1 hour **Total Estimated Time: ~7 hours**