The better_labels Lookup Tables
Many FIT fields (sport type, event type, dive alarm type, and dozens more) are stored as raw numeric codes, not text. Cordelia's own UI shows you a human-readable, localized name for these; a report script querying the SQLite database directly gets the raw code and needs to resolve it itself. Throughout the Data Dictionary, Full Field Reference, any column that needs this is marked: "The localized text equivalent of this value can be obtained by lookup into better_labels_lookup." This page explains what that lookup actually looks like.
Why the codes stay raw
Cordelia does not bake resolved, localized text into your activity tables at import time. The raw code is the Garmin-documented identifier: comparable across any FIT-based tool, and stable whatever locale Cordelia was running in when the file was imported. Translated text instead lives in two small, normalized tables that every activity table's code columns point into.
The two tables
better_labels_definitions assigns a stable, database-internal
better_labels_id to each (domain, garmin_key) pair.
domain names a family of related codes (sport,
event_type, dive_alarm_type, and so on), and
garmin_key is the raw code as it appears in that family's column, stored as
text. better_labels_lookup then holds the actual display text: one row per
better_labels_id per locale.
CREATE TABLE better_labels_definitions (
better_labels_id INTEGER PRIMARY KEY,
domain TEXT NOT NULL,
garmin_key TEXT NOT NULL,
UNIQUE (domain, garmin_key)
);
CREATE TABLE better_labels_lookup (
better_labels_id INTEGER NOT NULL REFERENCES better_labels_definitions(better_labels_id),
locale TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (better_labels_id, locale)
);
The two statements above are an excerpt, re-aligned for readability, of the schema published in full at The schema as SQL. That download is the authoritative text; if the two ever disagree, the download is right.
locale is one of en, fr, es,
de, it, ptbr: the same six languages Cordelia's UI
supports. Both tables are seeded once, when Cordelia creates the database, and cover every
code Cordelia currently recognizes, not just the ones your imported files happen to use.
Finding the domain for a column
The Data Dictionary's field-remark tells you a column needs resolving, but not which
domain string to join against; that isn't always the column name. For
example, Session.sport_id resolves against domain sport and
Session.sub_sport_id against sub_sport (the _id
suffix is dropped), but Session.trigger resolves against
session_trigger (the table name is prefixed on instead). Rather than guess,
list what's actually in your database:
SELECT DISTINCT domain FROM better_labels_definitions ORDER BY domain;
domain
---------
sport
sub_sport
...
A real database has around sixty domains. Once you know the domain name, a quick look at a few of its rows confirms which raw values it covers:
SELECT garmin_key, better_labels_id FROM better_labels_definitions
WHERE domain = 'sport' ORDER BY CAST(garmin_key AS INTEGER) LIMIT 5;
A basic JOIN
Resolving one column is a two-table join: better_labels_definitions to
turn the raw code into a better_labels_id, then
better_labels_lookup to turn that into text for the locale you want.
SELECT
s.RecordNumber,
s.sport_id,
l.value AS sport_name
FROM Session s
LEFT JOIN better_labels_definitions d
ON d.domain = 'sport' AND d.garmin_key = CAST(s.sport_id AS TEXT)
LEFT JOIN better_labels_lookup l
ON l.better_labels_id = d.better_labels_id AND l.locale = 'en';
RecordNumber sport_id sport_name
------------ -------- ----------
1 1 Running
2 2 Cycling
3 99
garmin_key is TEXT while a code column like
sport_id is usually INTEGER; the explicit
CAST(... AS TEXT) makes the comparison unambiguous rather than relying on
SQLite's column-affinity rules to coerce it implicitly.
LEFT JOIN, not INNER JOIN. A raw code
that isn't yet in better_labels_definitions, an unrecognized or newly-added
FIT value, has no matching row. An INNER JOIN would silently drop that
activity's row from your results entirely; a LEFT JOIN keeps it, with
sport_name simply NULL (as row 3, sport_id 99,
shows above).More than one locale, or more than one column, at once
Each additional locale is another join against better_labels_lookup under
its own alias:
SELECT
s.RecordNumber,
s.sport_id,
en.value AS sport_en,
fr.value AS sport_fr,
es.value AS sport_es
FROM Session s
LEFT JOIN better_labels_definitions d
ON d.domain = 'sport' AND d.garmin_key = CAST(s.sport_id AS TEXT)
LEFT JOIN better_labels_lookup en ON en.better_labels_id = d.better_labels_id AND en.locale = 'en'
LEFT JOIN better_labels_lookup fr ON fr.better_labels_id = d.better_labels_id AND fr.locale = 'fr'
LEFT JOIN better_labels_lookup es ON es.better_labels_id = d.better_labels_id AND es.locale = 'es';
RecordNumber sport_id sport_en sport_fr sport_es
------------ -------- -------- ------------- --------
1 1 Running Course à pied Correr
2 2 Cycling Cyclisme Ciclismo
3 99
And each additional code column is another definitions/lookup
pair, joined under its own aliases:
SELECT
s.RecordNumber,
sport.value AS sport,
sub_sport.value AS sub_sport,
s.total_distance
FROM Session s
LEFT JOIN better_labels_definitions sport_def
ON sport_def.domain = 'sport' AND sport_def.garmin_key = CAST(s.sport_id AS TEXT)
LEFT JOIN better_labels_lookup sport
ON sport.better_labels_id = sport_def.better_labels_id AND sport.locale = 'en'
LEFT JOIN better_labels_definitions sub_sport_def
ON sub_sport_def.domain = 'sub_sport' AND sub_sport_def.garmin_key = CAST(s.sub_sport_id AS TEXT)
LEFT JOIN better_labels_lookup sub_sport
ON sub_sport.better_labels_id = sub_sport_def.better_labels_id AND sub_sport.locale = 'en';
RecordNumber sport sub_sport total_distance
------------ ------- ------------- --------------
1 Running Trail Running 10450.2
2 Cycling 32000.0
3 5000.0
This pattern, one definitions/lookup join pair per code
column, one extra lookup alias per locale, covers every table in the
Data Dictionary marked as needing resolution;
only the domain string and the column being joined change.
For worked examples against a real Cordelia database, see antonio, or the Example Cordelia Databases to try these queries yourself. See also The Cordelia Generated SQLite Database and Editing the Data with Other Apps for opening the database with a general SQLite tool.