MOS Access Expert study materials
MOS Access Expert study resources

Nightly import keeps loading bad enrollment rows

MOS Access Expert · Put your knowledge to work in context.

All case studies

Nightly import keeps loading bad enrollment rows

Each night a CSV of course enrollments is appended to tblEnrollments in a training database. The table currently has EnrollmentID (AutoNumber, primary key), StudentEmail (Short Text, 255), CourseCode (Short Text, 10), StartDate (Short Text), Fee (Number, Double) and CompletionPct (Number, Double). Symptoms after last week's import: 340 rows duplicate an existing student/course pair; several rows have an empty StudentEmail; Fee values display as 1299.9999999 and one row holds -450; StartDate sorts as text, so October rows appear before February, and the query filtering a date range returns nothing; a few CourseCode values were silently truncated at 10 characters, losing the section suffix. Relationships already exist from EnrollmentID to two other tables. The registrar wants the database itself to reject bad rows instead of relying on a cleanup query run afterwards.

1. Enforce one enrollment per student per course without disturbing the AutoNumber primary key or the existing relationships.

Read the worked answer

Keep EnrollmentID as the primary key, because the two existing relationships depend on it and a composite primary key would require deleting and recreating them.

Add a composite unique index instead:

  1. Open the table in Design view and click Indexes on the Design tab.
  2. Add a row with Index Name uniqStudentCourse, Field Name StudentEmail.
  3. Add a second row with the same index name and Field Name CourseCode, and set Unique = Yes on the first row only. Leave Primary = No.

This permits many students on one course and many courses for one student, but only one row per pair.

Two traps. First, Access cannot create the index while the 340 duplicate pairs exist - deduplicate first (a Find Duplicates query, or a totals query keeping Max(EnrollmentID)). Second, Access does not enforce a unique index on records where an indexed field is Null, so a blank StudentEmail slips straight past the index. Set Required = Yes on both StudentEmail and CourseCode before saving the index, otherwise the constraint is decorative.

2. Specify the data type, property and validation changes for StartDate, Fee and CourseCode, and explain why each is needed.

Read the worked answer

StartDate is text, so comparisons are string comparisons: '10/01/2024' sorts before '2/01/2024', which is why October precedes February, and a date-range criterion in a query fails silently because Access compares a string to a date. Change the field to Date/Time. Convert existing values first, because Access cannot always parse mixed formats on a type change: add a temporary Date/Time field, populate it with CDate or DateSerial built from the pieces, verify, then swap the fields. Set Format to Short Date and add an index - date fields are routinely used in criteria and sorting.

Fee as Double explains 1299.9999999: binary floating point cannot represent decimal currency. Change it to Currency (four decimal places internally, two displayed) and set Decimal Places = 2. Add a table-level Validation Rule of >=0 with Validation Text naming the field, so the -450 row is rejected at insert time. If fractional cents ever matter, use Decimal with a defined scale instead.

CourseCode needs Field Size raised to 12 or more. Field Size is a hard ceiling enforced on every append, which is why the import truncated rather than errored. For the email, add a Validation Rule such as Like '*?@?*.?*' plus Required = Yes; a Validation Rule is enforced by the engine on any insert path, including imports and append queries, which an input mask on a form is not.

3. Design the import so bad rows are rejected rather than loaded and cleaned up later, and state the limits of that approach.

Read the worked answer

Point the import at a staging table with the same columns typed correctly - Date/Time for StartDate, Currency for Fee, no validation rules yet - so raw text never lands in the production table. Then append production rows from the staging table with an append query. Every constraint you defined fires during that append: Required fields, Validation Rules, Field Size, and the composite unique index.

Access does not stop the whole batch when a row fails. It appends the good rows and writes the rejected ones, with the error text, to the import errors table named after the destination (for example tblEnrollments_Paste Errors). The workflow is therefore: import to staging, run the append, then read the errors table, correct the source file, and re-run. Keep the staging table and the append query as saved objects so the nightly job is one macro or one saved import specification plus one query.

Limits to state honestly: table-level rules catch format, range, required and duplicate problems, but they cannot catch a valid-looking value with the wrong meaning (a CourseCode that exists but is retired). That needs a lookup to tblCourses or an unmatched query against the reference table, run on the staging table before the append.