Bulk 'import repeater contacts' imports nothing when a contact-type filter is used, and mis-types chat contacts #153

Closed
opened 2026-10-27 09:35:38 +01:00 by adam · 1 comment
Owner

Summary

"Import repeater contacts" imports nothing whenever a contact-type filter is supplied, and reports success. On a live repeater with 285 adverts, every one of the four permitted filter values selects 0 rows.

A second, related defect imports chat contacts with the wrong adv_type even when the import does work.

Cause

adverts.contact_type stores the display name, written via handler_helpers.discovery.NODE_TYPE_NAMES:

'Repeater'  'Chat Node'  'Room Server'  'Sensor'

companion_endpoints.import_repeater_contacts validates contact_types against MeshCore's names:

allowed = {"companion", "repeater", "room_server", "sensor"}

and companion_import_repeater_contacts compares the two directly:

query += f" AND contact_type IN ({placeholders})"

SQLite string comparison is case-sensitive, so "repeater" never matches "Repeater", "room_server" never matches "Room Server", and "companion" never matches "Chat Node" under any casing.

Measured on that repeater's own database, running the endpoint's exact query:

contact_types rows selected
["companion"] 0
["repeater"] 0
["room_server"] 0
["sensor"] 0
all four 0
(no filter) 285

Second defect, same cause. The adv_type assignment normalises the stored name but looks it up in the API-name table:

normalized_type = raw_type.lower().replace(" ", "_").strip()
adv_type = type_map.get(normalized_type, 0)   # type_map has "companion", not "chat_node"

"Chat Node" → "chat_node" → absent → adv_type = 0 instead of 1. Repeater/Room Server/Sensor happen to normalise onto their API names, so only chat contacts are affected. This one is silently wrong rather than empty, and needs a manual UPDATE companion_contacts SET adv_type=1 WHERE adv_type=0 to repair — I had already done exactly that by hand before finding the cause.

Reproduce

  1. Let the repeater collect adverts (select distinct contact_type from adverts → the display names above).
  2. POST /api/companion/import_repeater_contacts {"companion_name": "...", "contact_types": ["repeater"]}
  3. Response: {"imported": 0, "removed": 0}. Nothing is written; companion_contacts is unchanged.
  4. Repeat with no contact_types → imports fine, but chat contacts land with adv_type = 0.

Fix

PR to follow. The two sites derived the mapping independently, which is how they drifted; they now share one table keyed on the normalised stored form. The filter reverse-maps the requested API names onto stored keys and compares LOWER(REPLACE(TRIM(contact_type), ' ', '_')), so filtering and limiting stay in SQL. An unrecognised type still selects nothing rather than silently widening the query.

Includes 8 tests seeded with the exact stored display names; 6 fail on current dev.

Environment: Raspberry Pi 4, Python 3.13.5, dev at efc5616.

### Summary "Import repeater contacts" imports **nothing** whenever a contact-type filter is supplied, and reports success. On a live repeater with 285 adverts, every one of the four permitted filter values selects 0 rows. A second, related defect imports chat contacts with the wrong `adv_type` even when the import does work. ### Cause `adverts.contact_type` stores the **display** name, written via `handler_helpers.discovery.NODE_TYPE_NAMES`: ``` 'Repeater' 'Chat Node' 'Room Server' 'Sensor' ``` `companion_endpoints.import_repeater_contacts` validates `contact_types` against **MeshCore's** names: ```python allowed = {"companion", "repeater", "room_server", "sensor"} ``` and `companion_import_repeater_contacts` compares the two directly: ```python query += f" AND contact_type IN ({placeholders})" ``` SQLite string comparison is case-sensitive, so `"repeater"` never matches `"Repeater"`, `"room_server"` never matches `"Room Server"`, and `"companion"` never matches `"Chat Node"` under any casing. Measured on that repeater's own database, running the endpoint's exact query: | `contact_types` | rows selected | |---|---| | `["companion"]` | 0 | | `["repeater"]` | 0 | | `["room_server"]` | 0 | | `["sensor"]` | 0 | | all four | 0 | | *(no filter)* | **285** | **Second defect, same cause.** The `adv_type` assignment normalises the stored name but looks it up in the API-name table: ```python normalized_type = raw_type.lower().replace(" ", "_").strip() adv_type = type_map.get(normalized_type, 0) # type_map has "companion", not "chat_node" ``` `"Chat Node"` → `"chat_node"` → absent → **`adv_type = 0`** instead of 1. Repeater/Room Server/Sensor happen to normalise onto their API names, so only chat contacts are affected. This one is silently wrong rather than empty, and needs a manual `UPDATE companion_contacts SET adv_type=1 WHERE adv_type=0` to repair — I had already done exactly that by hand before finding the cause. ### Reproduce 1. Let the repeater collect adverts (`select distinct contact_type from adverts` → the display names above). 2. `POST /api/companion/import_repeater_contacts {"companion_name": "...", "contact_types": ["repeater"]}` 3. Response: `{"imported": 0, "removed": 0}`. Nothing is written; `companion_contacts` is unchanged. 4. Repeat with no `contact_types` → imports fine, but chat contacts land with `adv_type = 0`. ### Fix PR to follow. The two sites derived the mapping independently, which is how they drifted; they now share one table keyed on the normalised **stored** form. The filter reverse-maps the requested API names onto stored keys and compares `LOWER(REPLACE(TRIM(contact_type), ' ', '_'))`, so filtering and limiting stay in SQL. An unrecognised type still selects nothing rather than silently widening the query. Includes 8 tests seeded with the exact stored display names; 6 fail on current `dev`. Environment: Raspberry Pi 4, Python 3.13.5, `dev` at `efc5616`. <!-- github-sync-id:5338205469 -->
adam closed this issue 2026-10-27 09:35:38 +01:00
Author
Owner

Merged fix in dev

Merged fix in dev <!-- github-comment-id:5580224426 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/pyMC_Repeater#153