1use crate::error::{check_field_len, check_tag_count};
2use crate::limits::{MAX_TAG_KEY_LEN, MAX_TAG_VALUE_LEN};
3use crate::models::{BinaryTag, BinaryTagRow, Tag};
4use crate::{Db, ReadWrite, Result};
5use rusqlite::params;
6
7fn check_tag_lengths(key_len: i64, value_len: i64) -> Result<()> {
12 check_field_len("tags", "key", key_len, MAX_TAG_KEY_LEN)?;
13 check_field_len("tags", "value", value_len, MAX_TAG_VALUE_LEN)?;
14 Ok(())
15}
16
17impl<M> Db<M> {
18 pub fn get_tags(&self, track_id: i64) -> Result<Vec<Tag>> {
19 let mut stmt = self.conn.prepare(
20 "SELECT length(key), length(value), key, value, ordinal FROM tags \
21 WHERE track_id = ?1 AND value_blob IS NULL ORDER BY key, ordinal",
22 )?;
23 let mut rows = stmt.query(params![track_id])?;
24 let mut out = Vec::new();
25 while let Some(r) = rows.next()? {
26 check_tag_lengths(r.get(0)?, r.get(1)?)?;
27 out.push(Tag {
28 key: r.get(2)?,
29 value: r.get(3)?,
30 ordinal: r.get(4)?,
31 });
32 check_tag_count(track_id, out.len())?;
33 }
34 Ok(out)
35 }
36
37 pub fn tags_for_tracks(
38 &self,
39 track_ids: &[i64],
40 ) -> Result<std::collections::HashMap<i64, Vec<Tag>>> {
41 const CHUNK: usize = 900;
42 let mut out: std::collections::HashMap<i64, Vec<Tag>> = std::collections::HashMap::new();
43 for chunk in track_ids.chunks(CHUNK) {
44 let placeholders = vec!["?"; chunk.len()].join(",");
45 let sql = format!(
46 "SELECT track_id, length(key), length(value), key, value, ordinal FROM tags \
47 WHERE track_id IN ({placeholders}) AND value_blob IS NULL \
48 ORDER BY track_id, key, ordinal"
49 );
50 let mut stmt = self.conn.prepare(&sql)?;
51 let params = rusqlite::params_from_iter(chunk.iter());
52 let mut rows = stmt.query(params)?;
53 while let Some(r) = rows.next()? {
54 let track_id: i64 = r.get(0)?;
55 check_tag_lengths(r.get(1)?, r.get(2)?)?;
56 let entry = out.entry(track_id).or_default();
57 entry.push(Tag {
58 key: r.get(3)?,
59 value: r.get(4)?,
60 ordinal: r.get(5)?,
61 });
62 check_tag_count(track_id, entry.len())?;
63 }
64 }
65 Ok(out)
66 }
67
68 pub fn tags_grouped(&self) -> Result<std::collections::HashMap<i64, Vec<Tag>>> {
69 let mut stmt = self.conn.prepare(
70 "SELECT track_id, length(key), length(value), key, value, ordinal FROM tags \
71 WHERE value_blob IS NULL ORDER BY track_id, key, ordinal",
72 )?;
73 let mut rows = stmt.query([])?;
74 let mut out: std::collections::HashMap<i64, Vec<Tag>> = std::collections::HashMap::new();
75 while let Some(r) = rows.next()? {
76 let track_id: i64 = r.get(0)?;
77 check_tag_lengths(r.get(1)?, r.get(2)?)?;
78 let entry = out.entry(track_id).or_default();
79 entry.push(Tag {
80 key: r.get(3)?,
81 value: r.get(4)?,
82 ordinal: r.get(5)?,
83 });
84 check_tag_count(track_id, entry.len())?;
85 }
86 Ok(out)
87 }
88
89 pub fn tags_grouped_for_keys(
90 &self,
91 keys: &[&str],
92 ) -> Result<std::collections::HashMap<i64, Vec<Tag>>> {
93 const CHUNK: usize = 900;
94 let mut out: std::collections::HashMap<i64, Vec<Tag>> = std::collections::HashMap::new();
95 for chunk in keys.chunks(CHUNK) {
96 let lowered: Vec<String> = chunk.iter().map(|k| k.to_ascii_lowercase()).collect();
97 let placeholders = vec!["?"; lowered.len()].join(",");
98 let sql = format!(
99 "SELECT track_id, length(key), length(value), key, value, ordinal FROM tags \
100 WHERE value_blob IS NULL AND lower(key) IN ({placeholders}) \
101 ORDER BY track_id, key, ordinal"
102 );
103 let mut stmt = self.conn.prepare(&sql)?;
104 let params = rusqlite::params_from_iter(lowered.iter());
105 let mut rows = stmt.query(params)?;
106 while let Some(r) = rows.next()? {
107 let track_id: i64 = r.get(0)?;
108 check_tag_lengths(r.get(1)?, r.get(2)?)?;
109 let entry = out.entry(track_id).or_default();
110 entry.push(Tag {
111 key: r.get(3)?,
112 value: r.get(4)?,
113 ordinal: r.get(5)?,
114 });
115 check_tag_count(track_id, entry.len())?;
116 }
117 }
118 Ok(out)
119 }
120
121 pub fn get_binary_tags(&self, track_id: i64) -> Result<Vec<BinaryTagRow>> {
126 let mut stmt = self.conn.prepare(
127 "SELECT length(key), rowid, key, length(value_blob) FROM tags \
128 WHERE track_id = ?1 AND value_blob IS NOT NULL ORDER BY key, ordinal",
129 )?;
130 let mut rows = stmt.query(params![track_id])?;
131 let mut out = Vec::new();
132 while let Some(r) = rows.next()? {
133 check_field_len("tags", "key", r.get(0)?, MAX_TAG_KEY_LEN)?;
134 out.push(BinaryTagRow {
135 rowid: r.get(1)?,
136 key: r.get(2)?,
137 byte_len: r.get(3)?,
138 });
139 check_tag_count(track_id, out.len())?;
140 }
141 Ok(out)
142 }
143
144 pub fn read_binary_tag_chunk_into(
150 &self,
151 payload_id: i64,
152 offset: u64,
153 buf: &mut [u8],
154 ) -> Result<()> {
155 let blob = self
156 .conn
157 .blob_open("main", "tags", "value_blob", payload_id, true)?;
158 blob.read_at_exact(buf, crate::convert::usize_from(offset))?;
159 Ok(())
160 }
161
162 pub fn read_binary_tag_chunk(
165 &self,
166 payload_id: i64,
167 offset: u64,
168 len: usize,
169 ) -> Result<Vec<u8>> {
170 let mut buf = vec![0u8; len];
171 self.read_binary_tag_chunk_into(payload_id, offset, &mut buf)?;
172 Ok(buf)
173 }
174}
175
176impl Db<ReadWrite> {
177 pub fn replace_tags(&self, track_id: i64, tags: &[Tag]) -> Result<()> {
178 let tx = self.conn.unchecked_transaction()?;
179 tx.execute(
180 "DELETE FROM tags WHERE track_id = ?1 AND value_blob IS NULL",
181 params![track_id],
182 )?;
183 {
184 let mut stmt = tx.prepare(
185 "INSERT INTO tags (track_id, key, value, ordinal) VALUES (?1, ?2, ?3, ?4)",
186 )?;
187 for t in tags {
188 stmt.execute(params![track_id, t.key, t.value, t.ordinal])?;
189 }
190 }
191 tx.commit()?;
192 Ok(())
193 }
194
195 pub fn set_binary_tags(&self, track_id: i64, tags: &[BinaryTag]) -> Result<()> {
198 let tx = self.conn.unchecked_transaction()?;
199 tx.execute(
200 "DELETE FROM tags WHERE track_id = ?1 AND value_blob IS NOT NULL",
201 params![track_id],
202 )?;
203 {
204 let mut stmt = tx.prepare(
205 "INSERT INTO tags (track_id, key, value, value_blob, ordinal) \
206 VALUES (?1, ?2, '', ?3, ?4)",
207 )?;
208 for t in tags {
209 stmt.execute(params![track_id, t.key, t.payload, t.ordinal])?;
210 }
211 }
212 tx.commit()?;
213 Ok(())
214 }
215}
216
217#[cfg(test)]
218mod tags_for_tracks_tests {
219 use super::*;
220 use crate::{Format, NewTrack, Tag};
221
222 fn open_mem() -> Db {
223 Db::open_in_memory().unwrap()
224 }
225 fn new_track(path: &str) -> NewTrack {
226 NewTrack {
227 backing_path: path.into(),
228 format: Format::Flac,
229 audio_offset: 0,
230 audio_length: 1,
231 backing_size: 1,
232 backing_mtime_ns: 0,
233 backing_ctime_ns: 0,
234 }
235 }
236
237 #[test]
238 fn tags_for_tracks_returns_only_requested_ordered_by_key_ordinal() {
239 let db = open_mem();
240 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
241 let b = db.upsert_track(&new_track("/b.flac")).unwrap();
242 let c = db.upsert_track(&new_track("/c.flac")).unwrap();
243 db.replace_tags(
244 a,
245 &[
246 Tag::new("ARTIST", "second", 1),
247 Tag::new("ARTIST", "first", 0),
248 ],
249 )
250 .unwrap();
251 db.replace_tags(b, &[Tag::new("ARTIST", "bee", 0)]).unwrap();
252 db.replace_tags(c, &[Tag::new("ARTIST", "cee", 0)]).unwrap();
253
254 let got = db.tags_for_tracks(&[a, b]).unwrap();
255 assert_eq!(got.len(), 2, "c was not requested");
256 assert!(!got.contains_key(&c));
257 let a_tags = &got[&a];
258 assert_eq!(a_tags[0].value, "first");
259 assert_eq!(a_tags[1].value, "second");
260 }
261
262 #[test]
263 fn tags_for_tracks_chunks_beyond_sqlite_variable_limit() {
264 let db = open_mem();
265 let mut ids = Vec::new();
266 for i in 0..1500 {
267 let id = db.upsert_track(&new_track(&format!("/t{i}.flac"))).unwrap();
268 db.replace_tags(id, &[Tag::new("TITLE", &format!("t{i}"), 0)])
269 .unwrap();
270 ids.push(id);
271 }
272 let got = db.tags_for_tracks(&ids).unwrap();
273 assert_eq!(got.len(), 1500, "all chunks fetched");
274 }
275
276 #[test]
277 fn tags_for_tracks_empty_input_is_empty_map() {
278 let db = open_mem();
279 assert!(db.tags_for_tracks(&[]).unwrap().is_empty());
280 }
281
282 #[test]
283 fn text_queries_exclude_binary_rows() {
284 let db = open_mem();
285 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
286 db.replace_tags(a, &[Tag::new("artist", "Alice", 0)])
287 .unwrap();
288 db.conn
289 .execute(
290 "INSERT INTO tags (track_id, key, value, value_blob, ordinal) \
291 VALUES (?1, 'PRIV', '', X'DEADBEEF', 0)",
292 rusqlite::params![a],
293 )
294 .unwrap();
295
296 let got = db.get_tags(a).unwrap();
297 assert_eq!(got, vec![Tag::new("artist", "Alice", 0)]);
298 let grouped = db.tags_grouped().unwrap();
299 assert_eq!(grouped[&a], vec![Tag::new("artist", "Alice", 0)]);
300 let for_tracks = db.tags_for_tracks(&[a]).unwrap();
301 assert_eq!(for_tracks[&a], vec![Tag::new("artist", "Alice", 0)]);
302 }
303
304 #[test]
305 fn binary_tags_round_trip_and_are_independent_of_text() {
306 let db = open_mem();
307 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
308 db.replace_tags(a, &[Tag::new("artist", "Alice", 0)])
309 .unwrap();
310 db.set_binary_tags(
311 a,
312 &[
313 crate::BinaryTag {
314 key: "PRIV".into(),
315 payload: vec![1, 2, 3],
316 ordinal: 0,
317 },
318 crate::BinaryTag {
319 key: "PRIV".into(),
320 payload: vec![9, 9],
321 ordinal: 1,
322 },
323 crate::BinaryTag {
324 key: "GEOB".into(),
325 payload: vec![7],
326 ordinal: 0,
327 },
328 ],
329 )
330 .unwrap();
331
332 assert_eq!(
333 db.get_tags(a).unwrap(),
334 vec![Tag::new("artist", "Alice", 0)]
335 );
336
337 let rows = db.get_binary_tags(a).unwrap();
338 assert_eq!(rows.len(), 3);
339 assert_eq!(rows[0].key, "GEOB");
340 assert_eq!(rows[0].byte_len, 1);
341 assert_eq!(rows[1].key, "PRIV");
342 assert_eq!(rows[1].byte_len, 3);
343 assert_eq!(rows[2].byte_len, 2);
344
345 let full = db.read_binary_tag_chunk(rows[1].rowid, 0, 3).unwrap();
346 assert_eq!(full, vec![1, 2, 3]);
347 let mid = db.read_binary_tag_chunk(rows[1].rowid, 1, 2).unwrap();
348 assert_eq!(mid, vec![2, 3]);
349
350 db.set_binary_tags(a, &[]).unwrap();
351 assert!(db.get_binary_tags(a).unwrap().is_empty());
352 assert_eq!(
353 db.get_tags(a).unwrap(),
354 vec![Tag::new("artist", "Alice", 0)]
355 );
356 }
357
358 #[test]
359 fn tags_grouped_for_keys_filters_case_insensitively() {
360 let db = open_mem();
361 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
362 db.replace_tags(
363 a,
364 &[
365 Tag::new("ARTIST", "Pix", 0),
366 Tag::new("Title", "Song", 0),
367 Tag::new("LYRICS", "la la", 0),
368 ],
369 )
370 .unwrap();
371 let got = db.tags_grouped_for_keys(&["artist", "title"]).unwrap();
372 let tags = &got[&a];
373 assert!(tags.iter().any(|t| t.value == "Pix"), "ARTIST matched");
374 assert!(tags.iter().any(|t| t.value == "Song"), "Title matched");
375 assert!(!tags.iter().any(|t| t.value == "la la"), "LYRICS excluded");
376 }
377
378 #[test]
379 fn get_tags_rejects_oversize_value() {
380 let db = open_mem();
381 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
382 db.conn
383 .execute_batch("PRAGMA ignore_check_constraints=ON")
384 .unwrap();
385 let big = "v".repeat(262_145);
386 db.conn
387 .execute(
388 "INSERT INTO tags (track_id, key, value, ordinal) VALUES (?1, 'k', ?2, 0)",
389 rusqlite::params![a, big],
390 )
391 .unwrap();
392 let err = db.get_tags(a).unwrap_err();
393 assert!(
394 matches!(err, crate::DbError::FieldTooLarge { field: "value", .. }),
395 "{err:?}"
396 );
397 }
398
399 #[test]
400 fn get_tags_accepts_value_at_cap() {
401 let db = open_mem();
402 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
403 let at = "v".repeat(262_144);
404 db.conn
405 .execute(
406 "INSERT INTO tags (track_id, key, value, ordinal) VALUES (?1, 'k', ?2, 0)",
407 rusqlite::params![a, at],
408 )
409 .unwrap();
410 assert_eq!(db.get_tags(a).unwrap()[0].value.len(), 262_144);
411 }
412
413 #[test]
414 fn get_binary_tags_rejects_oversize_key() {
415 let db = open_mem();
416 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
417 db.conn
418 .execute_batch("PRAGMA ignore_check_constraints=ON")
419 .unwrap();
420 let key = "k".repeat(257);
421 db.conn
422 .execute(
423 "INSERT INTO tags (track_id, key, value, value_blob, ordinal) VALUES (?1, ?2, '', X'00', 0)",
424 rusqlite::params![a, key],
425 )
426 .unwrap();
427 let err = db.get_binary_tags(a).unwrap_err();
428 assert!(
429 matches!(
430 err,
431 crate::DbError::FieldTooLarge {
432 table: "tags",
433 field: "key",
434 ..
435 }
436 ),
437 "{err:?}"
438 );
439 }
440
441 #[test]
442 fn per_track_count_cap_text_and_binary() {
443 let db = open_mem();
444 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
445 {
447 let tx = db.conn.unchecked_transaction().unwrap();
448 let mut stmt = tx
449 .prepare(
450 "INSERT INTO tags (track_id, key, value, ordinal) VALUES (?1, 'k', 'v', ?2)",
451 )
452 .unwrap();
453 for i in 0..4097 {
454 stmt.execute(rusqlite::params![a, i]).unwrap();
455 }
456 drop(stmt);
457 tx.commit().unwrap();
458 }
459 let err = db.get_tags(a).unwrap_err();
460 assert!(
461 matches!(err, crate::DbError::TooManyValues { .. }),
462 "{err:?}"
463 );
464 }
465
466 #[test]
467 fn bulk_reader_rejects_one_oversized_track_in_batch() {
468 let db = open_mem();
469 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
470 let b = db.upsert_track(&new_track("/b.flac")).unwrap();
471 db.replace_tags(b, &[Tag::new("ok", "fine", 0)]).unwrap();
472 db.conn
473 .execute_batch("PRAGMA ignore_check_constraints=ON")
474 .unwrap();
475 let big = "v".repeat(262_145);
476 db.conn
477 .execute(
478 "INSERT INTO tags (track_id, key, value, ordinal) VALUES (?1, 'k', ?2, 0)",
479 rusqlite::params![a, big],
480 )
481 .unwrap();
482 let err = db.tags_for_tracks(&[a, b]).unwrap_err();
483 assert!(
484 matches!(err, crate::DbError::FieldTooLarge { field: "value", .. }),
485 "{err:?}"
486 );
487 }
488
489 #[test]
490 fn tags_grouped_for_keys_empty_keys_is_empty_map() {
491 let db = open_mem();
492 let a = db.upsert_track(&new_track("/a.flac")).unwrap();
493 db.replace_tags(a, &[Tag::new("ARTIST", "Pix", 0)]).unwrap();
494 let got = db.tags_grouped_for_keys(&[]).unwrap();
495 assert!(got.is_empty());
496 }
497
498 #[test]
499 fn replace_tags_rejects_floor_violating_keys() {
500 let db = open_mem();
501 let t = db.upsert_track(&new_track("/a.flac")).unwrap();
502 assert!(db.replace_tags(t, &[Tag::new("", "v", 0)]).is_err());
504 assert!(db.replace_tags(t, &[Tag::new("\u{7}", "v", 0)]).is_err());
505 db.replace_tags(t, &[Tag::new("a=b", "c", 0)]).unwrap();
507 let got = db.get_tags(t).unwrap();
508 assert_eq!(got.len(), 1);
509 assert_eq!(got[0].key, "a=b");
510 }
511
512 #[test]
513 fn replace_tags_rolls_back_a_mixed_valid_invalid_batch() {
514 let db = open_mem();
515 let t = db.upsert_track(&new_track("/a.flac")).unwrap();
516 db.replace_tags(t, &[Tag::new("artist", "Alice", 0)])
517 .unwrap();
518 assert!(
523 db.replace_tags(t, &[Tag::new("title", "ok", 0), Tag::new("", "bad", 0)])
524 .is_err()
525 );
526 let got = db.get_tags(t).unwrap();
527 assert_eq!(got.len(), 1);
528 assert_eq!(got[0].key, "artist");
529 assert_eq!(got[0].value, "Alice");
530 }
531}