1use async_trait::async_trait;
14use sqlx::PgPool;
15use sqlx::QueryBuilder;
16use sqlx::postgres::PgPoolOptions;
17
18use crate::embedding::sparse::SparseVector;
19use crate::embedding::vector_index::SearchHit;
20use crate::error::{KernelError, Result};
21
22#[derive(sqlx::FromRow)]
24struct ScoreRow {
25 id: i64,
26 score: f64,
27}
28
29fn vec_literal(v: &[f32]) -> String {
32 let mut s = String::from("[");
33 for (i, f) in v.iter().enumerate() {
34 if i > 0 {
35 s.push(',');
36 }
37 s.push_str(&f.to_string());
38 }
39 s.push(']');
40 s
41}
42
43#[derive(Debug, Clone, Default)]
48pub struct PgVectorOpts {
49 pub half: bool,
51 pub hnsw_m: Option<u32>,
54 pub hnsw_ef_construction: Option<u32>,
57 pub hnsw_ef_search: Option<u32>,
61}
62
63pub struct PgVectorIndex {
69 pool: PgPool,
70 table: String,
71 dim: usize,
72 half: bool,
74 hnsw_m: Option<u32>,
75 hnsw_ef_construction: Option<u32>,
76}
77
78impl PgVectorIndex {
79 pub async fn new(url: &str, table: &str, dim: usize) -> Result<Self> {
87 Self::new_with_opts(url, table, dim, PgVectorOpts::default()).await
88 }
89
90 pub async fn new_halfvec(url: &str, table: &str, dim: usize) -> Result<Self> {
95 Self::new_with_opts(
96 url,
97 table,
98 dim,
99 PgVectorOpts {
100 half: true,
101 ..Default::default()
102 },
103 )
104 .await
105 }
106
107 pub async fn new_with_opts(
114 url: &str,
115 table: &str,
116 dim: usize,
117 opts: PgVectorOpts,
118 ) -> Result<Self> {
119 validate_table_name(table)?;
120 let mut pool_opts = PgPoolOptions::new().max_connections(8);
121 if let Some(ef) = opts.hnsw_ef_search {
122 pool_opts = pool_opts.after_connect(move |conn, _meta| {
124 Box::pin(async move {
125 sqlx::query(&format!("SET hnsw.ef_search = {ef}"))
126 .execute(conn)
127 .await?;
128 Ok(())
129 })
130 });
131 }
132 let pool = pool_opts
133 .connect(url)
134 .await
135 .map_err(|e| KernelError::Embedding(format!("pgvector connect: {e}")))?;
136 let idx = Self {
137 pool,
138 table: table.to_string(),
139 dim,
140 half: opts.half,
141 hnsw_m: opts.hnsw_m,
142 hnsw_ef_construction: opts.hnsw_ef_construction,
143 };
144 idx.init_schema().await?;
145 Ok(idx)
146 }
147
148 fn sql_type(&self) -> &'static str {
150 if self.half { "halfvec" } else { "vector" }
151 }
152
153 fn ops_class(&self) -> &'static str {
155 if self.half {
156 "halfvec_cosine_ops"
157 } else {
158 "vector_cosine_ops"
159 }
160 }
161
162 async fn init_schema(&self) -> Result<()> {
165 let ty = self.sql_type();
169 let ops = self.ops_class();
170 let with = hnsw_with_clause(self.hnsw_m, self.hnsw_ef_construction);
171 sqlx::query(&format!(
172 "CREATE TABLE IF NOT EXISTS {} (id BIGINT PRIMARY KEY, vec {}({}) NOT NULL)",
173 self.table, ty, self.dim
174 ))
175 .execute(&self.pool)
176 .await
177 .map_err(|e| KernelError::Embedding(format!("pgvector create table: {e}")))?;
178 sqlx::query(&format!(
179 "CREATE INDEX IF NOT EXISTS idx_{}_vec ON {} USING hnsw (vec {}){}",
180 self.table, self.table, ops, with
181 ))
182 .execute(&self.pool)
183 .await
184 .map_err(|e| KernelError::Embedding(format!("pgvector hnsw index: {e}")))?;
185 Ok(())
186 }
187
188 pub fn pool(&self) -> &PgPool {
196 &self.pool
197 }
198
199 pub async fn remove_in_tx(&self, tx: &mut sqlx::PgConnection, ids: &[u64]) -> Result<()> {
207 if ids.is_empty() {
208 return Ok(());
209 }
210 let ids: Vec<i64> = ids.iter().map(|&i| to_pg_id(i)).collect::<Result<_>>()?;
211 sqlx::query(&format!("DELETE FROM {} WHERE id = ANY($1)", self.table))
212 .bind(&ids)
213 .execute(tx)
214 .await
215 .map_err(|e| KernelError::Embedding(format!("pgvector remove_in_tx: {e}")))?;
216 Ok(())
217 }
218}
219
220#[async_trait]
221impl crate::embedding::AsyncVectorIndex for PgVectorIndex {
222 async fn add(&self, vectors: &[Vec<f32>], ids: &[u64]) -> Result<()> {
223 if vectors.len() != ids.len() {
224 return Err(KernelError::Embedding(format!(
225 "vectors.len() ({}) must equal ids.len() ({})",
226 vectors.len(),
227 ids.len()
228 )));
229 }
230 if vectors.is_empty() {
231 return Ok(());
232 }
233 let pg_ids: Vec<i64> = ids.iter().map(|&id| to_pg_id(id)).collect::<Result<_>>()?;
237
238 const ROWS_PER_CHUNK: usize = 16_000;
243 for chunk in vectors
244 .chunks(ROWS_PER_CHUNK)
245 .zip(pg_ids.chunks(ROWS_PER_CHUNK))
246 {
247 let (chunk_vecs, chunk_ids) = chunk;
248 let mut q = QueryBuilder::new("INSERT INTO ");
249 q.push(self.table.as_str());
250 q.push(" (id, vec) VALUES ");
251 for (i, (v, &id)) in chunk_vecs.iter().zip(chunk_ids.iter()).enumerate() {
255 if i > 0 {
256 q.push(", ");
257 }
258 q.push("(");
259 q.push_bind(id);
260 q.push(", ");
261 q.push_bind(vec_literal(v));
262 q.push("::");
263 q.push(self.sql_type());
264 q.push(")");
265 }
266 q.push(" ON CONFLICT (id) DO UPDATE SET vec = EXCLUDED.vec");
267 q.build()
268 .execute(&self.pool)
269 .await
270 .map_err(|e| KernelError::Embedding(format!("pgvector add: {e}")))?;
271 }
272 Ok(())
273 }
274
275 async fn remove(&self, ids: &[u64]) -> Result<()> {
276 if ids.is_empty() {
277 return Ok(());
278 }
279 let ids: Vec<i64> = ids.iter().map(|&i| to_pg_id(i)).collect::<Result<_>>()?;
280 sqlx::query(&format!("DELETE FROM {} WHERE id = ANY($1)", self.table))
281 .bind(&ids)
282 .execute(&self.pool)
283 .await
284 .map_err(|e| KernelError::Embedding(format!("pgvector remove: {e}")))?;
285 Ok(())
286 }
287
288 async fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchHit>> {
289 let q = vec_literal(query);
290 let ty = self.sql_type();
291 let rows: Vec<ScoreRow> = sqlx::query_as(&format!(
296 "SELECT id, 1 - (vec <=> $1::{ty}) AS score FROM {} \
297 ORDER BY vec <=> $1::{ty}, id LIMIT $2",
298 self.table
299 ))
300 .bind(q)
301 .bind(k as i64)
302 .fetch_all(&self.pool)
303 .await
304 .map_err(|e| KernelError::Embedding(format!("pgvector search: {e}")))?;
305 Ok(rows
306 .into_iter()
307 .map(|r| SearchHit {
308 id: r.id as u64,
309 score: r.score as f32,
310 })
311 .collect())
312 }
313
314 async fn search_filtered(
315 &self,
316 query: &[f32],
317 k: usize,
318 allowlist: &[u64],
319 ) -> Result<Vec<SearchHit>> {
320 if allowlist.is_empty() {
321 return Ok(Vec::new());
322 }
323 let q = vec_literal(query);
324 let ty = self.sql_type();
325 let allow: Vec<i64> = allowlist
326 .iter()
327 .map(|&i| to_pg_id(i))
328 .collect::<Result<_>>()?;
329 let rows: Vec<ScoreRow> = sqlx::query_as(&format!(
330 "SELECT id, 1 - (vec <=> $1::{ty}) AS score FROM {} WHERE id = ANY($2) \
331 ORDER BY vec <=> $1::{ty}, id LIMIT $3",
332 self.table
333 ))
334 .bind(q)
335 .bind(&allow)
336 .bind(k as i64)
337 .fetch_all(&self.pool)
338 .await
339 .map_err(|e| KernelError::Embedding(format!("pgvector search_filtered: {e}")))?;
340 Ok(rows
341 .into_iter()
342 .map(|r| SearchHit {
343 id: r.id as u64,
344 score: r.score as f32,
345 })
346 .collect())
347 }
348
349 async fn len(&self) -> Result<usize> {
350 let n: i64 = sqlx::query_scalar(&format!("SELECT count(*) FROM {}", self.table))
351 .fetch_one(&self.pool)
352 .await
353 .map_err(|e| KernelError::Embedding(format!("pgvector len: {e}")))?;
354 Ok(n as usize)
355 }
356
357 fn dim(&self) -> usize {
358 self.dim
359 }
360}
361
362fn sparse_literal(v: &SparseVector, dim: usize) -> String {
373 let mut s = String::from("{");
374 for (n, (idx, val)) in v.indices().iter().zip(v.values()).enumerate() {
375 if n > 0 {
376 s.push(',');
377 }
378 s.push_str(&(u64::from(*idx) + 1).to_string());
379 s.push(':');
380 s.push_str(&val.to_string());
381 }
382 s.push('}');
383 s.push('/');
384 s.push_str(&dim.to_string());
385 s
386}
387
388pub struct PgSparseVectorIndex {
401 pool: PgPool,
402 table: String,
403 dim: usize,
404}
405
406impl PgSparseVectorIndex {
407 pub async fn new(url: &str, table: &str, dim: usize) -> Result<Self> {
412 validate_table_name(table)?;
413 let pool = PgPoolOptions::new()
414 .max_connections(8)
415 .connect(url)
416 .await
417 .map_err(|e| KernelError::Embedding(format!("pgvector sparse connect: {e}")))?;
418 let idx = Self {
419 pool,
420 table: table.to_string(),
421 dim,
422 };
423 sqlx::query(&format!(
424 "CREATE TABLE IF NOT EXISTS {} (id BIGINT PRIMARY KEY, vec sparsevec({}) NOT NULL)",
425 idx.table, idx.dim
426 ))
427 .execute(&idx.pool)
428 .await
429 .map_err(|e| KernelError::Embedding(format!("pgvector sparse create table: {e}")))?;
430 sqlx::query(&format!(
431 "CREATE INDEX IF NOT EXISTS idx_{}_vec ON {} USING hnsw (vec sparsevec_ip_ops)",
432 idx.table, idx.table
433 ))
434 .execute(&idx.pool)
435 .await
436 .map_err(|e| KernelError::Embedding(format!("pgvector sparse hnsw index: {e}")))?;
437 Ok(idx)
438 }
439
440 pub fn dim(&self) -> usize {
442 self.dim
443 }
444
445 pub fn pool(&self) -> &PgPool {
447 &self.pool
448 }
449
450 pub async fn add(&self, vectors: &[SparseVector], ids: &[u64]) -> Result<()> {
452 if vectors.len() != ids.len() {
453 return Err(KernelError::Embedding(format!(
454 "vectors.len() ({}) must equal ids.len() ({})",
455 vectors.len(),
456 ids.len()
457 )));
458 }
459 if vectors.is_empty() {
460 return Ok(());
461 }
462 let pg_ids: Vec<i64> = ids.iter().map(|&id| to_pg_id(id)).collect::<Result<_>>()?;
463 let mut q = QueryBuilder::new("INSERT INTO ");
464 q.push(self.table.as_str());
465 q.push(" (id, vec) VALUES ");
466 for (i, (v, &id)) in vectors.iter().zip(pg_ids.iter()).enumerate() {
467 if i > 0 {
468 q.push(", ");
469 }
470 q.push("(");
471 q.push_bind(id);
472 q.push(", ");
473 q.push_bind(sparse_literal(v, self.dim));
474 q.push("::sparsevec)");
475 }
476 q.push(" ON CONFLICT (id) DO UPDATE SET vec = EXCLUDED.vec");
477 q.build()
478 .execute(&self.pool)
479 .await
480 .map_err(|e| KernelError::Embedding(format!("pgvector sparse add: {e}")))?;
481 Ok(())
482 }
483
484 pub async fn search(&self, query: &SparseVector, k: usize) -> Result<Vec<SearchHit>> {
487 if query.is_empty() {
488 return Ok(Vec::new());
489 }
490 let q = sparse_literal(query, self.dim);
491 let rows: Vec<ScoreRow> = sqlx::query_as(&format!(
495 "SELECT id, -(vec <#> $1::sparsevec) AS score FROM {} \
496 ORDER BY vec <#> $1::sparsevec, id LIMIT $2",
497 self.table
498 ))
499 .bind(q)
500 .bind(k as i64)
501 .fetch_all(&self.pool)
502 .await
503 .map_err(|e| KernelError::Embedding(format!("pgvector sparse search: {e}")))?;
504 Ok(rows
505 .into_iter()
506 .map(|r| SearchHit {
507 id: r.id as u64,
508 score: r.score as f32,
509 })
510 .collect())
511 }
512
513 pub async fn search_filtered(
515 &self,
516 query: &SparseVector,
517 k: usize,
518 allowlist: &[u64],
519 ) -> Result<Vec<SearchHit>> {
520 if query.is_empty() || allowlist.is_empty() {
521 return Ok(Vec::new());
522 }
523 let q = sparse_literal(query, self.dim);
524 let allow: Vec<i64> = allowlist
525 .iter()
526 .map(|&i| to_pg_id(i))
527 .collect::<Result<_>>()?;
528 let rows: Vec<ScoreRow> = sqlx::query_as(&format!(
529 "SELECT id, -(vec <#> $1::sparsevec) AS score FROM {} WHERE id = ANY($2) \
530 ORDER BY vec <#> $1::sparsevec, id LIMIT $3",
531 self.table
532 ))
533 .bind(q)
534 .bind(&allow)
535 .bind(k as i64)
536 .fetch_all(&self.pool)
537 .await
538 .map_err(|e| KernelError::Embedding(format!("pgvector sparse search_filtered: {e}")))?;
539 Ok(rows
540 .into_iter()
541 .map(|r| SearchHit {
542 id: r.id as u64,
543 score: r.score as f32,
544 })
545 .collect())
546 }
547
548 pub async fn remove(&self, ids: &[u64]) -> Result<()> {
550 if ids.is_empty() {
551 return Ok(());
552 }
553 let ids: Vec<i64> = ids.iter().map(|&i| to_pg_id(i)).collect::<Result<_>>()?;
554 sqlx::query(&format!("DELETE FROM {} WHERE id = ANY($1)", self.table))
555 .bind(&ids)
556 .execute(&self.pool)
557 .await
558 .map_err(|e| KernelError::Embedding(format!("pgvector sparse remove: {e}")))?;
559 Ok(())
560 }
561
562 pub async fn remove_in_tx(&self, tx: &mut sqlx::PgConnection, ids: &[u64]) -> Result<()> {
566 if ids.is_empty() {
567 return Ok(());
568 }
569 let ids: Vec<i64> = ids.iter().map(|&i| to_pg_id(i)).collect::<Result<_>>()?;
570 sqlx::query(&format!("DELETE FROM {} WHERE id = ANY($1)", self.table))
571 .bind(&ids)
572 .execute(tx)
573 .await
574 .map_err(|e| KernelError::Embedding(format!("pgvector sparse remove_in_tx: {e}")))?;
575 Ok(())
576 }
577
578 pub async fn len(&self) -> Result<usize> {
580 let n: i64 = sqlx::query_scalar(&format!("SELECT count(*) FROM {}", self.table))
581 .fetch_one(&self.pool)
582 .await
583 .map_err(|e| KernelError::Embedding(format!("pgvector sparse len: {e}")))?;
584 Ok(n as usize)
585 }
586
587 pub async fn is_empty(&self) -> Result<bool> {
589 Ok(self.len().await? == 0)
590 }
591}
592
593fn hnsw_with_clause(m: Option<u32>, ef_construction: Option<u32>) -> String {
597 let mut parts: Vec<String> = Vec::new();
598 if let Some(m) = m {
599 parts.push(format!("m = {m}"));
600 }
601 if let Some(ef) = ef_construction {
602 parts.push(format!("ef_construction = {ef}"));
603 }
604 if parts.is_empty() {
605 String::new()
606 } else {
607 format!(" WITH ({})", parts.join(", "))
608 }
609}
610
611fn to_pg_id(id: u64) -> Result<i64> {
614 i64::try_from(id).map_err(|_| KernelError::Embedding(format!("id {id} exceeds BIGINT range")))
615}
616
617fn validate_table_name(table: &str) -> Result<()> {
622 let mut chars = table.chars();
623 let first_ok = matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_');
624 let valid = first_ok && chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
625 if !valid {
626 return Err(KernelError::Embedding(format!(
627 "invalid table identifier: {table:?}"
628 )));
629 }
630 Ok(())
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::embedding::AsyncVectorIndex;
637
638 fn pg_url() -> Option<String> {
640 std::env::var("LLMKERNEL_PG_URL").ok()
641 }
642
643 #[tokio::test]
647 async fn roundtrip_add_search_remove() {
648 let Some(url) = pg_url() else {
649 eprintln!("skip pgvector test: LLMKERNEL_PG_URL unset");
650 return;
651 };
652 let table = format!("lk_test_{}", line!());
653 let idx = PgVectorIndex::new(&url, &table, 3).await.expect("new");
654
655 let vecs = vec![
656 vec![1.0, 0.0, 0.0],
657 vec![0.0, 1.0, 0.0],
658 vec![0.0, 0.0, 1.0],
659 ];
660 let ids = vec![10u64, 20, 30];
661 idx.add(&vecs, &ids).await.expect("add");
662 assert_eq!(idx.len().await.unwrap(), 3);
663
664 let hits = idx.search(&[1.0, 0.0, 0.0], 1).await.unwrap();
666 assert_eq!(hits.len(), 1);
667 assert_eq!(hits[0].id, 10);
668
669 let hits = idx
671 .search_filtered(&[1.0, 0.0, 0.0], 1, &[20, 30])
672 .await
673 .unwrap();
674 assert_eq!(hits.len(), 1);
675 assert_ne!(hits[0].id, 10);
676
677 idx.remove(&[10]).await.unwrap();
678 assert_eq!(idx.len().await.unwrap(), 2);
679
680 sqlx::query(&format!("DROP TABLE IF EXISTS {}", table))
682 .execute(&idx.pool)
683 .await
684 .ok();
685 }
686
687 #[tokio::test]
690 async fn roundtrip_halfvec() {
691 let Some(url) = pg_url() else {
692 eprintln!("skip pgvector halfvec test: LLMKERNEL_PG_URL unset");
693 return;
694 };
695 let table = format!("lk_test_hv_{}", line!());
696 let idx = PgVectorIndex::new_halfvec(&url, &table, 3)
697 .await
698 .expect("new_halfvec");
699
700 let vecs = vec![
701 vec![1.0, 0.0, 0.0],
702 vec![0.0, 1.0, 0.0],
703 vec![0.0, 0.0, 1.0],
704 ];
705 let ids = vec![10u64, 20, 30];
706 idx.add(&vecs, &ids).await.expect("add");
707 assert_eq!(idx.len().await.unwrap(), 3);
708
709 let hits = idx.search(&[1.0, 0.0, 0.0], 1).await.unwrap();
711 assert_eq!(hits.len(), 1);
712 assert_eq!(hits[0].id, 10);
713
714 idx.remove(&[10]).await.unwrap();
715 assert_eq!(idx.len().await.unwrap(), 2);
716
717 sqlx::query(&format!("DROP TABLE IF EXISTS {}", table))
719 .execute(&idx.pool)
720 .await
721 .ok();
722 }
723
724 #[tokio::test]
727 async fn remove_in_tx_atomic_delete() {
728 let Some(url) = pg_url() else {
729 eprintln!("skip pgvector test: LLMKERNEL_PG_URL unset");
730 return;
731 };
732 let table = format!("lk_txtx_{}", line!());
733 let idx = PgVectorIndex::new(&url, &table, 3).await.expect("new");
734 idx.add(&[vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]], &[11, 12])
735 .await
736 .expect("add");
737 assert_eq!(idx.len().await.unwrap(), 2);
738
739 let mut tx = idx.pool().begin().await.expect("begin tx");
741 idx.remove_in_tx(&mut tx, &[11])
742 .await
743 .expect("remove_in_tx");
744 tx.commit().await.expect("commit");
745 assert_eq!(idx.len().await.unwrap(), 1);
746
747 let mut tx2 = idx.pool().begin().await.expect("begin tx2");
749 idx.remove_in_tx(&mut tx2, &[12])
750 .await
751 .expect("remove_in_tx2");
752 tx2.rollback().await.expect("rollback");
753 assert_eq!(idx.len().await.unwrap(), 1);
754
755 sqlx::query(&format!("DROP TABLE IF EXISTS {}", table))
757 .execute(idx.pool())
758 .await
759 .ok();
760 }
761
762 #[test]
763 fn rejects_invalid_table_name() {
764 assert!(validate_table_name("lk_test_1").is_ok());
766 assert!(validate_table_name("_vec").is_ok());
767 assert!(validate_table_name("").is_err());
769 assert!(validate_table_name("1bad").is_err());
770 assert!(validate_table_name("rm; DROP").is_err());
771 assert!(validate_table_name("weird\"name").is_err());
772 assert!(validate_table_name("sch.tbl").is_err());
773 }
774
775 #[test]
777 fn sparse_literal_is_one_based() {
778 let sv = SparseVector::new(vec![0, 2], vec![0.5, 0.25]).unwrap();
779 assert_eq!(sparse_literal(&sv, 5), "{1:0.5,3:0.25}/5");
780 }
781
782 #[test]
783 fn sparse_literal_handles_empty() {
784 assert_eq!(sparse_literal(&SparseVector::default(), 5), "{}/5");
785 }
786
787 #[tokio::test]
790 async fn roundtrip_sparse() {
791 let Some(url) = pg_url() else {
792 eprintln!("skip pgvector sparse test: LLMKERNEL_PG_URL unset");
793 return;
794 };
795 let table = format!("lk_test_sp_{}", line!());
796 let idx = PgSparseVectorIndex::new(&url, &table, 10)
797 .await
798 .expect("new sparse");
799
800 let a = SparseVector::new(vec![0, 3], vec![1.0, 0.5]).unwrap();
801 let b = SparseVector::new(vec![3, 7], vec![0.5, 1.0]).unwrap();
802 idx.add(&[a.clone(), b], &[10, 20]).await.expect("add");
803 assert_eq!(idx.len().await.unwrap(), 2);
804
805 let hits = idx.search(&a, 1).await.unwrap();
807 assert_eq!(hits.len(), 1);
808 assert_eq!(hits[0].id, 10);
809
810 let empty = idx.search(&SparseVector::default(), 5).await.unwrap();
812 assert!(empty.is_empty());
813
814 idx.remove(&[10]).await.unwrap();
815 assert_eq!(idx.len().await.unwrap(), 1);
816
817 sqlx::query(&format!("DROP TABLE IF EXISTS {}", table))
818 .execute(idx.pool())
819 .await
820 .ok();
821 }
822
823 #[test]
824 fn hnsw_with_clause_variants() {
825 assert_eq!(hnsw_with_clause(None, None), "");
826 assert_eq!(hnsw_with_clause(Some(32), None), " WITH (m = 32)");
827 assert_eq!(
828 hnsw_with_clause(None, Some(200)),
829 " WITH (ef_construction = 200)"
830 );
831 assert_eq!(
832 hnsw_with_clause(Some(32), Some(200)),
833 " WITH (m = 32, ef_construction = 200)"
834 );
835 }
836
837 #[test]
840 fn default_opts_are_full_precision_and_untuned() {
841 let o = PgVectorOpts::default();
842 assert!(!o.half);
843 assert!(o.hnsw_m.is_none());
844 assert!(o.hnsw_ef_construction.is_none());
845 assert!(o.hnsw_ef_search.is_none());
846 }
847
848 #[test]
849 fn rejects_overflowing_id() {
850 assert_eq!(to_pg_id(0).unwrap(), 0);
851 assert_eq!(to_pg_id(42).unwrap(), 42);
852 assert_eq!(to_pg_id(i64::MAX as u64).unwrap(), i64::MAX);
853 assert!(to_pg_id((i64::MAX as u64) + 1).is_err());
854 assert!(to_pg_id(u64::MAX).is_err());
855 }
856}