LensSerious 0.1
Lens-correction mathematics as data, not as a library of callbacks
Loading...
Searching...
No Matches
schema.sql
Go to the documentation of this file.
1-- LensSerious — the lens database as a table of numbers.
2--
3-- Copyright (C) 2026 Aurélien PIERRE. License: LGPL-3.0-or-later.
4--
5-- Written once by tools/import_lensfun_xml.c from the upstream XML, then only ever read.
6-- The reader opens it with immutable=1, which is what lets it skip file locking entirely
7-- (see include/lensserious_db.h): a file this schema describes is never updated in place.
8-- A rebuild writes a new file and renames it over the old one.
9--
10-- Calibration rows store the model as the LS_DIST_*/LS_TCA_*/LS_VIG_* integer and the
11-- terms in the order ls_calib_*_t declares them, so a lookup is a copy rather than a
12-- translation. That is deliberate: every conversion between the XML's vocabulary and the
13-- evaluator's happens once, offline, where it can be checked, rather than per render.
14
15PRAGMA user_version = 4;
16
17CREATE TABLE meta (
18 key TEXT PRIMARY KEY,
19 value TEXT NOT NULL
20) WITHOUT ROWID;
21
22CREATE TABLE mount (
23 id INTEGER PRIMARY KEY,
24 name TEXT NOT NULL UNIQUE
25);
26
27-- Directed: upstream lists compatibility per mount and does not assume symmetry.
28CREATE TABLE mount_compat (
29 mount_id INTEGER NOT NULL REFERENCES mount(id),
30 compat_id INTEGER NOT NULL REFERENCES mount(id),
31 PRIMARY KEY (mount_id, compat_id)
32) WITHOUT ROWID;
33
34CREATE TABLE camera (
35 id INTEGER PRIMARY KEY,
36 maker TEXT NOT NULL, -- the untranslated <maker>, as matched against EXIF
37 model TEXT NOT NULL,
38 variant TEXT,
39 mount_id INTEGER REFERENCES mount(id),
40 crop_factor REAL NOT NULL
41);
42
43CREATE TABLE lens (
44 id INTEGER PRIMARY KEY,
45 maker TEXT NOT NULL,
46 model TEXT NOT NULL,
47 type INTEGER NOT NULL, -- ls_lens_type_t
48 crop_factor REAL NOT NULL, -- of the CALIBRATION sensor
49 aspect_ratio REAL NOT NULL,
50 center_x REAL NOT NULL,
51 center_y REAL NOT NULL,
52 min_focal REAL NOT NULL, -- the vignetting IDW metric needs both, and most
53 max_focal REAL NOT NULL, -- lenses only carry them in their model string
54 min_aperture REAL,
55 max_aperture REAL
56);
57
58CREATE TABLE lens_mount (
59 lens_id INTEGER NOT NULL REFERENCES lens(id),
60 mount_id INTEGER NOT NULL REFERENCES mount(id),
61 PRIMARY KEY (lens_id, mount_id)
62) WITHOUT ROWID;
63
64-- Every spelling a lens or camera answers to: the untranslated name plus each <... lang="">
65-- variant. The matcher needs all of them, and `norm` is the case-folded, punctuation-free
66-- form it compares on, computed once here rather than per lookup.
67CREATE TABLE lens_name (
68 lens_id INTEGER NOT NULL REFERENCES lens(id),
69 kind TEXT NOT NULL, -- 'maker' | 'model'
70 lang TEXT, -- NULL for the untranslated name
71 value TEXT NOT NULL,
72 norm TEXT NOT NULL,
73 -- The tokens of `norm`, as a digest the matcher can score without parsing anything:
74 -- uint16 n, then n x uint32 FNV-1a hash, then n x uint8 length.
75 -- Little-endian, written and read by the same two functions in this repository, so the
76 -- database is not portable across endiannesses -- it is a build artefact of the machine
77 -- that imported it, like the rest of the file's page layout.
78 --
79 -- Scoring compares hash and length only. The prefix rule this replaces was measured to
80 -- contribute nothing: removing it left agreement at 99.0%, shape for shape.
81 tokens BLOB NOT NULL
82);
83
84CREATE TABLE camera_name (
85 camera_id INTEGER NOT NULL REFERENCES camera(id),
86 kind TEXT NOT NULL,
87 lang TEXT,
88 value TEXT NOT NULL,
89 norm TEXT NOT NULL
90);
91
92-- Terms are packed exactly as ls_calib_dist_t/ls_calib_tca_t/ls_calib_vig_t declare them.
93--
94-- WITHOUT ROWID, keyed on (lens_id, ord). The table IS its primary-key b-tree, so one
95-- lens's calibration is physically contiguous and reading it never leaves that b-tree --
96-- where a plain table plus an index on lens_id costs a separate table seek for every row
97-- the index finds.
98--
99-- `ord` is the position within the lens as upstream listed it, and it turns a convention
100-- into a guarantee: the reader fills ls_lens_t's arrays in row order, which under a rowid
101-- table was insertion order only by habit, and is now the key order the storage engine
102-- promises.
103--
104-- Measured on a 70-row fetch, all three against the same surrounding schema:
105-- WITHOUT ROWID (lens_id, ord) 0.0385 ms 3.76 MB <- this
106-- plain table + index(lens_id) 0.0436 ms 4.19 MB
107-- plain table + index(lens_id,ord) 0.0474 ms 4.35 MB -- the column AND a wider index
108-- Faster and smaller: such a table repeats its key in every row, but dropping the three
109-- lens_id indexes more than pays for that. An earlier round rejected this form on size,
110-- having compared it against a baseline taken before lens_token and token_df existed --
111-- which is what a comparison between two different schemas is worth.
112CREATE TABLE calib_distortion (
113 lens_id INTEGER NOT NULL REFERENCES lens(id),
114 ord INTEGER NOT NULL, -- position within this lens, as upstream listed it
115 model INTEGER NOT NULL, -- ls_dist_model_t
116 focal REAL NOT NULL,
117 t0 REAL NOT NULL, t1 REAL NOT NULL, t2 REAL NOT NULL
118,
119 PRIMARY KEY (lens_id, ord)
120) WITHOUT ROWID;
121
122CREATE TABLE calib_tca (
123 lens_id INTEGER NOT NULL REFERENCES lens(id),
124 ord INTEGER NOT NULL, -- position within this lens, as upstream listed it
125 model INTEGER NOT NULL, -- ls_tca_model_t
126 focal REAL NOT NULL,
127 t0 REAL NOT NULL, t1 REAL NOT NULL, t2 REAL NOT NULL,
128 t3 REAL NOT NULL, t4 REAL NOT NULL, t5 REAL NOT NULL
129,
130 PRIMARY KEY (lens_id, ord)
131) WITHOUT ROWID;
132
133CREATE TABLE calib_vignetting (
134 lens_id INTEGER NOT NULL REFERENCES lens(id),
135 ord INTEGER NOT NULL, -- position within this lens, as upstream listed it
136 model INTEGER NOT NULL, -- ls_vig_model_t
137 focal REAL NOT NULL,
138 aperture REAL NOT NULL,
139 distance REAL NOT NULL,
140 t0 REAL NOT NULL, t1 REAL NOT NULL, t2 REAL NOT NULL
141,
142 PRIMARY KEY (lens_id, ord)
143) WITHOUT ROWID;
144
145-- The inverted index the fuzzy matcher prunes with. One row per (name, token).
146--
147-- It is NOT used to gather every candidate that shares any token -- that was tried and
148-- measured, and it is no better than a full scan, because the common tokens ("mm", "f",
149-- "ed", "vr", the focal digits) each appear in a large fraction of the catalogue. What the
150-- matcher does instead is ask this table which of the QUERY's tokens is rarest, and gather
151-- only the lenses carrying that one. See ls_db_match_lens().
152CREATE TABLE lens_token (
153 lens_id INTEGER NOT NULL REFERENCES lens(id),
154 kind TEXT NOT NULL, -- 'maker' | 'model'
155 token TEXT NOT NULL
156);
157
158-- How many lenses each token appears in, so the matcher can find its query's rarest token
159-- with one point lookup per token instead of counting index rows. Counting is what made the
160-- first version of that lookup slow: "mm" appears in thousands of names, and COUNT(*) has
161-- to walk every one of them.
162CREATE TABLE token_df (
163 kind TEXT NOT NULL, -- 'maker' | 'model'
164 token TEXT NOT NULL,
165 df INTEGER NOT NULL,
166 PRIMARY KEY (kind, token)
167) WITHOUT ROWID;
168
169-- <real-focal-length>: the focal the PROJECTION stage runs on, which is not the one
170-- engraved on the barrel. Only a few dozen lenses carry it and it is load-bearing where
171-- they do -- 0.47x nominal on the Sigma 4.5mm circular fisheye, which is 28 px at the
172-- centre of the frame if the nominal focal is used instead.
173--
174-- WITHOUT ROWID and keyed by (lens_id, focal): the reader fetches a whole lens's points
175-- ordered by focal, which is exactly this key, so the fetch is one contiguous range scan
176-- and needs no separate index. Same reasoning as the calib_* tables.
177CREATE TABLE lens_real_focal (
178 lens_id INTEGER NOT NULL REFERENCES lens(id),
179 focal REAL NOT NULL,
180 real_focal REAL NOT NULL,
181 PRIMARY KEY (lens_id, focal)
182) WITHOUT ROWID;
183
184-- Lookups are by name, and the calibration fetch is by lens_id. Nothing else is indexed:
185-- an index the reader never uses is bytes every reader pays to mmap.
186CREATE INDEX idx_lens_name_norm ON lens_name(norm);
187-- Every name of one lens, for the fuzzy matcher's second phase: without it the query that
188-- gathers the pruned candidates' names has no way in and scans the whole table, which is
189-- the scan the pruning existed to avoid.
190--
191-- COVERING: kind and the token digest are in the index, so the search never touches
192-- the table. An index that only carries the search key still costs one table seek per row
193-- found, which is what `SEARCH ... USING INDEX` hides -- the row it lands on is an index
194-- entry, and every column outside the index is another b-tree descent.
195CREATE INDEX idx_lens_name_lens ON lens_name(lens_id, kind, tokens);
196-- COVERING for the same reason: the pruning subquery wants lens_id, so carrying it in the
197-- index means the token lookup never descends into lens_token itself.
198CREATE INDEX idx_lens_token ON lens_token(kind, token, lens_id);
199CREATE INDEX idx_camera_name_norm ON camera_name(norm);
200CREATE INDEX idx_lens_mount_mount ON lens_mount(mount_id);