Why Did a Pump Search Return the Wrong Equipment?
Clean and compare text under an explicit identifier policy, with literal pattern characters, NULL handling and honest Unicode limits.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Matching Text Required Rules Beyond Its Appearance
BOTIE
For imported identifiers, the useful lesson is to name the convention. We use ordinary surrounding spaces, ASCII letter case and a literal underscore. That limited rule is easy to demonstrate and test without pretending it solves every language’s text behaviour.
Sources for this history
That Underscore Is Part of the Name
The maintenance import contains “ PUMP_7 ”, “pump_9” and “pumpX8”. Botie searches for codes beginning with pump_ and unexpectedly includes pumpX8. In a LIKE pattern, the underscore has a special meaning: it matches any single character.
Before searching text, we need to decide how names should be cleaned and which characters in the search are literal. A query can run successfully while still applying the wrong naming policy.
The records are invented maintenance examples. The workshop exposes record operations; the SQL worksheet executes a real query in an isolated SQLite database.
Step 1 of 1
Distinguish a literal underscore from a wildcard
The desired prefix contains the actual underscore character.
| Raw code | After the stated cleanup | Keep? |
|---|---|---|
| PUMP_7 | pump_7 | Yes |
| pumpX8 | pumpx8 | No |
| pump_9 | pump_9 | Yes |
| NULL | NULL | No |
| PÜMP_2 | pÜmp_2 | No |
PUMP_7
- After the stated cleanup
- pump_7
- Keep?
- Yes
pumpX8
- After the stated cleanup
- pumpx8
- Keep?
- No
pump_9
- After the stated cleanup
- pump_9
- Keep?
- Yes
NULL
- After the stated cleanup
- NULL
- Keep?
- No
PÜMP_2
- After the stated cleanup
- pÜmp_2
- Keep?
- No
Text cleanup is an explicit policy
The cleanup component and literal-prefix component expose two separate decisions.
Normalise the code before applying a literal prefix
Trim surrounding ordinary spaces, lowercase ASCII letters, and retain codes beginning with the literal pump_ prefix.
BOTIE
Record IDs and normalised codes
[
{
"record_id": 1,
"normalised": "pump_7"
},
{
"record_id": 3,
"normalised": "pump_9"
}
]Follow the operations
1. Imported code records
[ { "record_id": 1, "code": " PUMP_7 " }, { "record_id": 2, "code": "pumpX8" }, { "record_id": 3, "code": "pump_9" }, { "record_id": 4, "code": null } ]2. Normalise an ASCII identifier
[ { "record_id": 1, "code": " PUMP_7 ", "normalised": "pump_7" }, { "record_id": 2, "code": "pumpX8", "normalised": "pumpx8" }, { "record_id": 3, "code": "pump_9", "normalised": "pump_9" }, { "record_id": 4, "code": null, "normalised": null } ]3. Literal prefix to select
pump_
4. Keep a literal text prefix
[ { "record_id": 1, "code": " PUMP_7 ", "normalised": "pump_7" }, { "record_id": 3, "code": "pump_9", "normalised": "pump_9" } ]5. Choose table columns
[ { "record_id": 1, "normalised": "pump_7" }, { "record_id": 3, "normalised": "pump_9" } ]6. Order rows by named fields
[ { "record_id": 1, "normalised": "pump_7" }, { "record_id": 3, "normalised": "pump_9" } ]7. Result
[ { "record_id": 1, "normalised": "pump_7" }, { "record_id": 3, "normalised": "pump_9" } ]
The workshop uses literal starts-with semantics, not a LIKE parser. It deliberately does not perform general Unicode normalisation.
1. Separate the original record from the comparison form
Keep the original imported code if it matters for audit or display. Add a derived normalised value for the specific matching policy. Silently overwriting an external identifier can make it difficult to reconcile later records.
Our rule removes ordinary spaces from both ends and lowercases ASCII letters. It leaves internal spaces and non-ASCII characters alone. Empty text remains different from NULL.
A cleanup rule should describe what changes and what remains meaningful.
2. Use trim and lower with their actual limits
SQLite trim(code) removes ordinary space characters at the ends by default. It does not promise to remove every Unicode whitespace character or internal spacing. Its default lower function converts ASCII letters.
A multilingual name-matching system needs a deliberate Unicode normalisation and case-folding policy, often with additional library support. Test that policy before using it in a unique key or deduplicating people.
A simple identifier lesson should not imply that all visually similar names are interchangeable.
Inspect cleanup without filtering anything
The unmodified unusual character and missing value remain visible.
| record_id | code |
|---|---|
| 1 | PUMP_7 |
| 2 | pumpX8 |
| 3 | pump_9 |
| 4 | NULL |
| 5 | PÜMP_2 |
Database setup for this example
CREATE TABLE codes(record_id INTEGER PRIMARY KEY, code TEXT);
INSERT INTO codes VALUES (1,' PUMP_7 '),(2,'pumpX8'),(3,'pump_9'),(4,NULL),(5,'PÜMP_2');SELECT record_id, lower(trim(code)) AS normalised FROM codes ORDER BY record_id;| record_id | normalised |
|---|---|
| 1 | pump_7 |
| 2 | pumpx8 |
| 3 | pump_9 |
| 4 | NULL |
| 5 | pÜmp_2 |
The output makes the rule inspectable before it influences which records are returned.
3. Escape characters that should be literal in a pattern
LIKE uses percent for any sequence of characters and underscore for one character. Our pattern must escape the underscore but retain the final percent wildcard. ESCAPE declares which character introduces that literal interpretation.
A literal prefix can also be expressed with an appropriate substring comparison. Parameter binding protects query structure, but it does not automatically turn pattern wildcards in a parameter into literal characters. Escape them according to the search contract.
SQL injection safety and wildcard interpretation are separate responsibilities.
Inspect both pattern interpretations
The first expression treats the underscore as a wildcard. The second uses it literally.
Database setup for this example
SELECT 'pumpX8' LIKE 'pump_%',
'pumpX8' LIKE 'pump\_%' ESCAPE '\',
'pump_8' LIKE 'pump\_%' ESCAPE '\';| 'pumpX8' LIKE 'pump_%' | 'pumpX8' LIKE 'pump\_%' ESCAPE '\' | 'pump_8' LIKE 'pump\_%' ESCAPE '\' |
|---|---|---|
| 1 | 0 | 1 |
The escaped pattern excludes pumpX8 while retaining pump_8. The challenge applies that rule to cleaned source records.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Why can it match?
Use the information below.
The LIKE pattern is pump_% without an ESCAPE clause. One input is pumpX8.4. Know which comparison is controlling equality
A collation determines how text is compared for operations such as ordering and equality. SQLite BINARY compares its encoded text representation; built-in NOCASE folds ASCII letter case rather than providing universal linguistic equality.
LIKE has its own documented matching behaviour, so attaching a collation is not a general way to rewrite its wildcard or Unicode rules. If case sensitivity matters, verify the exact operator and database configuration used by the query.
Do not assume one comparison rule governs every text operation in every database.
5. Inspect missing pieces before concatenating a label
The || operator concatenates text in SQLite. A NULL operand normally makes that concatenation NULL. Use COALESCE only when you have chosen a real fallback meaning, such as a displayed “unknown” marker.
substr can select a segment and instr can locate a substring. SQLite positions in these functions are one-based; instr returns zero when the substring is absent. Explicit examples help avoid importing Python’s zero-based indexing assumptions.
A display label is not a reliable replacement for separate structured identifier fields.
Locate the separator and preserve an unknown value
These small expressions isolate position and missing-value behaviour.
Database setup for this example
SELECT instr('pump_7', '_'), substr('pump_7', 6), 'pump_' || NULL;| instr('pump_7', '_') | substr('pump_7', 6) | 'pump_' || NULL |
|---|---|---|
| 5 | 7 | NULL |
The suffix starts at position 6. The unknown concatenated suffix does not silently become an empty string.
6. Do not let cleaning invent identity
Two different raw codes can become equal after cleanup. That is useful only if the domain says those forms represent the same equipment. Preserve a stable internal key and review collisions before enforcing uniqueness on the derived value.
The challenge deliberately returns record IDs beside cleaned text. If two matching records clean to the same code, both records remain. It is a selection exercise, not an unrequested deduplication policy.
Use examples with literal underscores, missing text and unfamiliar characters to test the policy you actually promised.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
Should the query automatically remove one?
Use the information below.
Two raw rows clean to pump_7, but their record_id values differ. This query selects matching imported records.Try it with Botie
Build with Botie
Write and run SQL