1mod a2a;
4mod accounts_payable;
5mod accounts_receivable;
6mod activity_logs;
7mod agent_cards;
8mod agent_identities;
9mod agent_reputation;
10mod agent_validation;
11mod analytics;
12mod backorder;
13mod bom;
14mod carts;
15mod channels;
16mod companies;
17mod cost_accounting;
18mod credit;
19mod currency;
20mod custom_objects;
21mod customers;
22mod edi_documents;
23mod fixed_assets;
24mod fraud;
25mod fulfillment;
26mod general_ledger;
27mod gift_cards;
28mod http_idempotency;
29mod inbound_shipments;
30mod integration_field_mappings;
31mod integration_mappings;
32mod inventory;
33mod invoices;
34mod lots;
35mod loyalty;
36mod money_agg;
37mod orders;
38pub(crate) mod parse_helpers;
39mod payment_obligations;
40mod payments;
41mod prepayments;
42mod price_levels;
43mod price_schedules;
44mod print_stations;
45mod production_batches;
46mod products;
47mod promotions;
48mod purchase_orders;
49mod purgatory;
50mod quality;
51mod receiving;
52mod returns;
53mod revenue_recognition;
54mod reviews;
55mod rewards;
56mod search_configs;
57mod segments;
58mod serials;
59mod shipments;
60mod shipping_zones;
61mod stock_snapshots;
62mod store_credits;
63mod subscriptions;
64mod supplier_skus;
65mod tax;
66mod topology_snapshots;
67mod transfer_orders;
68mod units_of_measure;
69mod vendor_credits;
70mod vendor_returns;
71mod warehouse;
72mod warranties;
73mod wishlists;
74mod work_orders;
75mod x402_credits;
76mod x402_payment_intents;
77mod zone_shipping_methods;
78
79#[cfg(feature = "vector")]
80mod vector;
81
82pub use a2a::*;
83pub use accounts_payable::*;
84pub use accounts_receivable::*;
85pub use activity_logs::*;
86pub use agent_cards::*;
87pub use agent_identities::*;
88pub use agent_reputation::*;
89pub use agent_validation::*;
90pub use analytics::*;
91pub use backorder::*;
92pub use bom::*;
93pub use carts::*;
94pub use channels::*;
95pub use companies::*;
96pub use cost_accounting::*;
97pub use credit::*;
98pub use currency::*;
99pub use custom_objects::*;
100pub use customers::*;
101pub use edi_documents::*;
102pub use fixed_assets::*;
103pub use fraud::*;
104pub use fulfillment::*;
105pub use general_ledger::*;
106pub use gift_cards::*;
107pub use http_idempotency::*;
108pub use inbound_shipments::*;
109pub use integration_field_mappings::*;
110pub use integration_mappings::*;
111pub use inventory::*;
112pub use invoices::*;
113pub use lots::*;
114pub use loyalty::*;
115pub use orders::*;
116pub use payment_obligations::*;
117pub use payments::*;
118pub use prepayments::*;
119pub use price_levels::*;
120pub use price_schedules::*;
121pub use print_stations::*;
122pub use production_batches::*;
123pub use products::*;
124pub use promotions::*;
125pub use purchase_orders::*;
126pub use purgatory::*;
127pub use quality::*;
128pub use receiving::*;
129pub use returns::*;
130pub use revenue_recognition::*;
131pub use reviews::*;
132pub use rewards::*;
133pub use search_configs::*;
134pub use segments::*;
135pub use serials::*;
136pub use shipments::*;
137pub use shipping_zones::*;
138pub use stock_snapshots::*;
139pub use store_credits::*;
140pub use subscriptions::*;
141pub use supplier_skus::*;
142pub use tax::*;
143pub use topology_snapshots::*;
144pub use transfer_orders::*;
145pub use units_of_measure::*;
146#[cfg(feature = "vector")]
147pub use vector::*;
148pub use vendor_credits::*;
149pub use vendor_returns::*;
150pub use warehouse::*;
151pub use warranties::*;
152pub use wishlists::*;
153pub use work_orders::*;
154pub use x402_credits::*;
155pub use x402_payment_intents::*;
156pub use zone_shipping_methods::*;
157
158use crate::DatabaseConfig;
159use crate::migrations;
160use r2d2::{Pool, PooledConnection};
161use r2d2_sqlite::SqliteConnectionManager;
162use rusqlite::OpenFlags;
163use rust_decimal::Decimal;
164use stateset_core::CommerceError;
165use std::panic::{self, AssertUnwindSafe};
166use std::thread;
167use std::time::Duration;
168
169#[derive(Debug)]
171pub struct SqliteDatabase {
172 pool: Pool<SqliteConnectionManager>,
173}
174
175#[derive(Debug)]
178struct EphemeralDbGuard {
179 path: std::path::PathBuf,
180}
181
182impl Drop for EphemeralDbGuard {
183 fn drop(&mut self) {
184 for suffix in ["", "-wal", "-shm"] {
185 let mut p = self.path.as_os_str().to_owned();
186 p.push(suffix);
187 let _ = std::fs::remove_file(std::path::PathBuf::from(p));
188 }
189 }
190}
191
192#[derive(Debug, Clone, serde::Serialize)]
194pub struct DbHealthStatus {
195 pub latency_ms: u64,
197 pub pool_size: u32,
199 pub pool_idle: u32,
201 pub db_size_bytes: u64,
203 pub freelist_pages: u64,
205}
206
207#[derive(Debug)]
208struct PragmaScope {
209 conn: PooledConnection<SqliteConnectionManager>,
210 previous_timeout: u64,
211 previous_read_uncommitted: Option<bool>,
212}
213
214impl PragmaScope {
215 fn new(
216 conn: PooledConnection<SqliteConnectionManager>,
217 timeout_ms: u64,
218 set_read_uncommitted: bool,
219 fallback_timeout_ms: u64,
220 ) -> Result<Self, rusqlite::Error> {
221 let previous_timeout: u64 = conn
222 .query_row("PRAGMA busy_timeout", [], |row| row.get::<_, u64>(0))
223 .unwrap_or(fallback_timeout_ms);
224
225 conn.execute_batch(&format!("PRAGMA busy_timeout = {timeout_ms}"))?;
226
227 let previous_read_uncommitted = if set_read_uncommitted {
228 let previous: i64 = conn
229 .query_row("PRAGMA read_uncommitted", [], |row| row.get::<_, i64>(0))
230 .unwrap_or(0);
231 let previous_read_uncommitted = previous == 1;
232 conn.execute_batch("PRAGMA read_uncommitted = true")?;
233 Some(previous_read_uncommitted)
234 } else {
235 None
236 };
237
238 Ok(Self { conn, previous_timeout, previous_read_uncommitted })
239 }
240}
241
242impl std::ops::Deref for PragmaScope {
243 type Target = PooledConnection<SqliteConnectionManager>;
244
245 fn deref(&self) -> &Self::Target {
246 &self.conn
247 }
248}
249
250impl std::ops::DerefMut for PragmaScope {
251 fn deref_mut(&mut self) -> &mut Self::Target {
252 &mut self.conn
253 }
254}
255
256impl Drop for PragmaScope {
257 fn drop(&mut self) {
258 let _ =
259 self.conn.execute_batch(&format!("PRAGMA busy_timeout = {}", self.previous_timeout));
260 if let Some(previous_read_uncommitted) = self.previous_read_uncommitted {
261 let _ = self.conn.execute_batch(&format!(
262 "PRAGMA read_uncommitted = {}",
263 if previous_read_uncommitted { "true" } else { "false" }
264 ));
265 }
266 }
267}
268
269impl SqliteDatabase {
270 pub fn new(config: &DatabaseConfig) -> Result<Self, CommerceError> {
272 use std::sync::atomic::{AtomicU64, Ordering};
273 use std::time::Duration;
274
275 static MEMORY_DB_COUNTER: AtomicU64 = AtomicU64::new(0);
277
278 let is_memory = config.url == ":memory:";
288 let mut ephemeral = None;
289 let (manager, max_connections) = if is_memory {
290 let db_id = MEMORY_DB_COUNTER.fetch_add(1, Ordering::SeqCst);
291 let path = std::env::temp_dir()
292 .join(format!("stateset_memdb_{}_{db_id}.db", std::process::id()));
293 ephemeral = Some(std::sync::Arc::new(EphemeralDbGuard { path: path.clone() }));
294 let manager = SqliteConnectionManager::file(&path).with_flags(
295 OpenFlags::SQLITE_OPEN_READ_WRITE
296 | OpenFlags::SQLITE_OPEN_CREATE
297 | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
298 );
299 (manager, config.max_connections.max(1))
300 } else {
301 let manager = SqliteConnectionManager::file(&config.url).with_flags(
302 OpenFlags::SQLITE_OPEN_READ_WRITE
303 | OpenFlags::SQLITE_OPEN_CREATE
304 | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
305 );
306 (manager, config.max_connections)
307 };
308 let manager = manager.with_init(move |conn| {
309 let _keep_ephemeral_alive = &ephemeral;
315 conn.execute_batch(&format!(
317 "PRAGMA foreign_keys = ON; PRAGMA busy_timeout = {};",
318 crate::DEFAULT_TRANSACTION_TIMEOUT_MS
319 ))?;
320 conn.execute_batch("PRAGMA journal_mode = WAL;")?;
321 conn.execute_batch(
323 "PRAGMA synchronous = NORMAL;\
324 PRAGMA cache_size = -16000;\
325 PRAGMA temp_store = MEMORY;\
326 PRAGMA mmap_size = 268435456;\
327 PRAGMA wal_autocheckpoint = 10000;",
328 )?;
329 money_agg::register(conn)?;
332 Ok(())
333 });
334
335 let pool = Pool::builder()
336 .max_size(max_connections)
337 .connection_timeout(Duration::from_secs(30))
338 .build(manager)
339 .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
340
341 let mut conn = pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
343
344 migrations::run_migrations(&mut conn)
346 .map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
347
348 Ok(Self { pool })
349 }
350
351 pub fn in_memory() -> Result<Self, CommerceError> {
353 Self::new(&DatabaseConfig::in_memory())
354 }
355
356 pub fn conn(&self) -> Result<PooledConnection<SqliteConnectionManager>, CommerceError> {
358 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
359 }
360
361 pub fn health_check(&self) -> Result<DbHealthStatus, CommerceError> {
363 let start = std::time::Instant::now();
364 let conn = self.conn()?;
365 let _: i64 = conn
367 .query_row("SELECT 1", [], |row| row.get(0))
368 .map_err(|e| CommerceError::DatabaseError(format!("Health check query failed: {e}")))?;
369
370 let page_count: i64 =
372 conn.query_row("PRAGMA page_count", [], |row| row.get(0)).unwrap_or(0);
373 let page_size: i64 =
374 conn.query_row("PRAGMA page_size", [], |row| row.get(0)).unwrap_or(4096);
375 let freelist_count: i64 =
376 conn.query_row("PRAGMA freelist_count", [], |row| row.get(0)).unwrap_or(0);
377
378 let pool_state = self.pool.state();
379
380 Ok(DbHealthStatus {
381 latency_ms: start.elapsed().as_millis() as u64,
382 pool_size: pool_state.connections,
383 pool_idle: pool_state.idle_connections,
384 db_size_bytes: (page_count * page_size) as u64,
385 freelist_pages: freelist_count as u64,
386 })
387 }
388
389 pub fn shutdown(&self) -> Result<(), CommerceError> {
394 let conn = self.conn()?;
395 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")
397 .map_err(|e| CommerceError::DatabaseError(format!("WAL checkpoint failed: {e}")))?;
398 conn.execute_batch("PRAGMA optimize")
400 .map_err(|e| CommerceError::DatabaseError(format!("PRAGMA optimize failed: {e}")))?;
401 tracing::info!("Database shutdown: WAL checkpointed and optimized");
402 Ok(())
403 }
404
405 #[must_use]
407 pub fn orders(&self) -> SqliteOrderRepository {
408 SqliteOrderRepository::new(self.pool.clone())
409 }
410
411 #[must_use]
413 pub fn inventory(&self) -> SqliteInventoryRepository {
414 SqliteInventoryRepository::new(self.pool.clone())
415 }
416
417 #[must_use]
419 pub fn customers(&self) -> SqliteCustomerRepository {
420 SqliteCustomerRepository::new(self.pool.clone())
421 }
422
423 #[must_use]
425 pub fn products(&self) -> SqliteProductRepository {
426 SqliteProductRepository::new(self.pool.clone())
427 }
428
429 #[must_use]
431 pub fn custom_objects(&self) -> SqliteCustomObjectRepository {
432 SqliteCustomObjectRepository::new(self.pool.clone())
433 }
434
435 #[must_use]
437 pub fn returns(&self) -> SqliteReturnRepository {
438 SqliteReturnRepository::new(self.pool.clone())
439 }
440
441 #[must_use]
443 pub fn bom(&self) -> SqliteBomRepository {
444 SqliteBomRepository::new(self.pool.clone())
445 }
446
447 #[must_use]
449 pub fn work_orders(&self) -> SqliteWorkOrderRepository {
450 SqliteWorkOrderRepository::new(self.pool.clone())
451 }
452
453 #[must_use]
455 pub fn shipments(&self) -> SqliteShipmentRepository {
456 SqliteShipmentRepository::new(self.pool.clone())
457 }
458
459 #[must_use]
461 pub fn payments(&self) -> SqlitePaymentRepository {
462 SqlitePaymentRepository::new(self.pool.clone())
463 }
464
465 #[must_use]
467 pub fn warranties(&self) -> SqliteWarrantyRepository {
468 SqliteWarrantyRepository::new(self.pool.clone())
469 }
470
471 #[must_use]
473 pub fn purchase_orders(&self) -> SqlitePurchaseOrderRepository {
474 SqlitePurchaseOrderRepository::new(self.pool.clone())
475 }
476
477 #[must_use]
479 pub fn invoices(&self) -> SqliteInvoiceRepository {
480 SqliteInvoiceRepository::new(self.pool.clone())
481 }
482
483 #[must_use]
485 pub fn carts(&self) -> SqliteCartRepository {
486 SqliteCartRepository::new(self.pool.clone())
487 }
488
489 #[must_use]
491 pub fn analytics(&self) -> SqliteAnalyticsRepository {
492 SqliteAnalyticsRepository::new(self.pool.clone())
493 }
494
495 #[must_use]
497 pub fn currency(&self) -> SqliteCurrencyRepository {
498 SqliteCurrencyRepository::new(self.pool.clone())
499 }
500
501 #[must_use]
503 pub fn tax(&self) -> SqliteTaxRepository {
504 SqliteTaxRepository::new(self.pool.clone())
505 }
506
507 #[must_use]
509 pub fn promotions(&self) -> SqlitePromotionRepository {
510 SqlitePromotionRepository::new(self.pool.clone())
511 }
512
513 #[must_use]
515 pub fn subscriptions(&self) -> SqliteSubscriptionRepository {
516 SqliteSubscriptionRepository::new(self.pool.clone())
517 }
518
519 #[must_use]
521 pub fn quality(&self) -> SqliteQualityRepository {
522 SqliteQualityRepository::new(self.pool.clone())
523 }
524
525 #[must_use]
527 pub fn lots(&self) -> SqliteLotRepository {
528 SqliteLotRepository::new(self.pool.clone())
529 }
530
531 #[must_use]
533 pub fn serials(&self) -> SqliteSerialRepository {
534 SqliteSerialRepository::new(self.pool.clone())
535 }
536
537 #[must_use]
539 pub fn warehouse(&self) -> SqliteWarehouseRepository {
540 SqliteWarehouseRepository::new(self.pool.clone())
541 }
542
543 #[must_use]
545 pub fn receiving(&self) -> SqliteReceivingRepository {
546 SqliteReceivingRepository::new(self.pool.clone())
547 }
548
549 #[must_use]
551 pub fn fulfillment(&self) -> SqliteFulfillmentRepository {
552 SqliteFulfillmentRepository::new(self.pool.clone())
553 }
554
555 #[must_use]
557 pub fn accounts_payable(&self) -> SqliteAccountsPayableRepository {
558 SqliteAccountsPayableRepository::new(self.pool.clone())
559 }
560
561 #[must_use]
563 pub fn cost_accounting(&self) -> SqliteCostAccountingRepository {
564 SqliteCostAccountingRepository::new(self.pool.clone())
565 }
566
567 #[must_use]
569 pub fn credit(&self) -> SqliteCreditRepository {
570 SqliteCreditRepository::new(self.pool.clone())
571 }
572
573 #[must_use]
575 pub fn backorder(&self) -> SqliteBackorderRepository {
576 SqliteBackorderRepository::new(self.pool.clone())
577 }
578
579 #[must_use]
581 pub fn accounts_receivable(&self) -> SqliteAccountsReceivableRepository {
582 SqliteAccountsReceivableRepository::new(self.pool.clone())
583 }
584
585 #[must_use]
587 pub fn general_ledger(&self) -> SqliteGeneralLedgerRepository {
588 SqliteGeneralLedgerRepository::new(self.pool.clone())
589 }
590
591 #[cfg(feature = "vector")]
593 pub fn vector(&self) -> SqliteVectorRepository {
594 SqliteVectorRepository::new(self.pool.clone())
595 }
596
597 #[must_use]
599 pub fn x402_payment_intents(&self) -> SqliteX402PaymentIntentRepository {
600 SqliteX402PaymentIntentRepository::new(self.pool.clone())
601 }
602
603 #[must_use]
605 pub fn x402_credits(&self) -> SqliteX402CreditRepository {
606 SqliteX402CreditRepository::new(self.pool.clone())
607 }
608
609 #[must_use]
611 pub fn a2a_quotes(&self) -> SqliteA2ARepository {
612 SqliteA2ARepository::new(self.pool.clone())
613 }
614
615 #[must_use]
617 pub fn a2a_purchases(&self) -> SqliteA2ARepository {
618 SqliteA2ARepository::new(self.pool.clone())
619 }
620
621 #[must_use]
623 pub fn agent_cards(&self) -> SqliteAgentCardRepository {
624 SqliteAgentCardRepository::new(self.pool.clone())
625 }
626
627 #[must_use]
629 pub fn agent_identities(&self) -> SqliteAgentIdentityRepository {
630 SqliteAgentIdentityRepository::new(self.pool.clone())
631 }
632
633 #[must_use]
635 pub fn agent_reputation(&self) -> SqliteAgentReputationRepository {
636 SqliteAgentReputationRepository::new(self.pool.clone())
637 }
638
639 #[must_use]
641 pub fn agent_validation(&self) -> SqliteAgentValidationRepository {
642 SqliteAgentValidationRepository::new(self.pool.clone())
643 }
644
645 #[must_use]
647 pub fn gift_cards(&self) -> SqliteGiftCardRepository {
648 SqliteGiftCardRepository::new(self.pool.clone())
649 }
650
651 #[must_use]
653 pub fn store_credits(&self) -> SqliteStoreCreditRepository {
654 SqliteStoreCreditRepository::new(self.pool.clone())
655 }
656
657 #[must_use]
659 pub fn segments(&self) -> SqliteSegmentRepository {
660 SqliteSegmentRepository::new(self.pool.clone())
661 }
662
663 #[must_use]
665 pub fn shipping_zones(&self) -> SqliteShippingZoneRepository {
666 SqliteShippingZoneRepository::new(self.pool.clone())
667 }
668
669 #[must_use]
671 pub fn channels(&self) -> SqliteChannelRepository {
672 SqliteChannelRepository::new(self.pool.clone())
673 }
674
675 #[must_use]
677 pub fn companies(&self) -> SqliteCompanyRepository {
678 SqliteCompanyRepository::new(self.pool.clone())
679 }
680
681 #[must_use]
683 pub fn transfer_orders(&self) -> SqliteTransferOrderRepository {
684 SqliteTransferOrderRepository::new(self.pool.clone())
685 }
686
687 #[must_use]
689 pub fn units_of_measure(&self) -> SqliteUnitOfMeasureRepository {
690 SqliteUnitOfMeasureRepository::new(self.pool.clone())
691 }
692
693 #[must_use]
695 pub fn production_batches(&self) -> SqliteProductionBatchRepository {
696 SqliteProductionBatchRepository::new(self.pool.clone())
697 }
698
699 #[must_use]
701 pub fn supplier_skus(&self) -> SqliteSupplierSkuRepository {
702 SqliteSupplierSkuRepository::new(self.pool.clone())
703 }
704
705 #[must_use]
707 pub fn fixed_assets(&self) -> SqliteFixedAssetRepository {
708 SqliteFixedAssetRepository::new(self.pool.clone())
709 }
710
711 #[must_use]
713 pub fn revenue_recognition(&self) -> SqliteRevenueRecognitionRepository {
714 SqliteRevenueRecognitionRepository::new(self.pool.clone())
715 }
716
717 #[must_use]
719 pub fn vendor_returns(&self) -> SqliteVendorReturnRepository {
720 SqliteVendorReturnRepository::new(self.pool.clone())
721 }
722
723 #[must_use]
725 pub fn vendor_credits(&self) -> SqliteVendorCreditRepository {
726 SqliteVendorCreditRepository::new(self.pool.clone())
727 }
728
729 #[must_use]
731 pub fn payment_obligations(&self) -> SqlitePaymentObligationRepository {
732 SqlitePaymentObligationRepository::new(self.pool.clone())
733 }
734
735 #[must_use]
737 pub fn price_levels(&self) -> SqlitePriceLevelRepository {
738 SqlitePriceLevelRepository::new(self.pool.clone())
739 }
740
741 #[must_use]
743 pub fn prepayments(&self) -> SqlitePrepaymentRepository {
744 SqlitePrepaymentRepository::new(self.pool.clone())
745 }
746
747 #[must_use]
749 pub fn price_schedules(&self) -> SqlitePriceScheduleRepository {
750 SqlitePriceScheduleRepository::new(self.pool.clone())
751 }
752
753 #[must_use]
755 pub fn http_idempotency(&self) -> SqliteHttpIdempotencyRepository {
756 SqliteHttpIdempotencyRepository::new(self.pool.clone())
757 }
758
759 #[must_use]
761 pub fn activity_logs(&self) -> SqliteActivityLogRepository {
762 SqliteActivityLogRepository::new(self.pool.clone())
763 }
764
765 #[must_use]
767 pub fn integration_mappings(&self) -> SqliteIntegrationMappingRepository {
768 SqliteIntegrationMappingRepository::new(self.pool.clone())
769 }
770
771 #[must_use]
773 pub fn inbound_shipments(&self) -> SqliteInboundShipmentRepository {
774 SqliteInboundShipmentRepository::new(self.pool.clone())
775 }
776
777 #[must_use]
779 pub fn purgatory(&self) -> SqlitePurgatoryRepository {
780 SqlitePurgatoryRepository::new(self.pool.clone())
781 }
782
783 #[must_use]
785 pub fn print_stations(&self) -> SqlitePrintStationRepository {
786 SqlitePrintStationRepository::new(self.pool.clone())
787 }
788
789 #[must_use]
791 pub fn edi_documents(&self) -> SqliteEdiDocumentRepository {
792 SqliteEdiDocumentRepository::new(self.pool.clone())
793 }
794
795 #[must_use]
797 pub fn integration_field_mappings(&self) -> SqliteIntegrationFieldMappingRepository {
798 SqliteIntegrationFieldMappingRepository::new(self.pool.clone())
799 }
800
801 #[must_use]
803 pub fn topology_snapshots(&self) -> SqliteTopologySnapshotRepository {
804 SqliteTopologySnapshotRepository::new(self.pool.clone())
805 }
806
807 #[must_use]
809 pub fn stock_snapshots(&self) -> SqliteStockSnapshotRepository {
810 SqliteStockSnapshotRepository::new(self.pool.clone())
811 }
812
813 #[must_use]
815 pub fn zone_shipping_methods(&self) -> SqliteZoneShippingMethodRepository {
816 SqliteZoneShippingMethodRepository::new(self.pool.clone())
817 }
818
819 #[must_use]
821 pub fn reviews(&self) -> SqliteReviewRepository {
822 SqliteReviewRepository::new(self.pool.clone())
823 }
824
825 #[must_use]
827 pub fn wishlists(&self) -> SqliteWishlistRepository {
828 SqliteWishlistRepository::new(self.pool.clone())
829 }
830
831 #[must_use]
833 pub fn loyalty_programs(&self) -> SqliteLoyaltyProgramRepository {
834 SqliteLoyaltyProgramRepository::new(self.pool.clone())
835 }
836
837 #[must_use]
839 pub fn rewards(&self) -> SqliteRewardRepository {
840 SqliteRewardRepository::new(self.pool.clone())
841 }
842
843 #[must_use]
845 pub fn fraud(&self) -> SqliteFraudRepository {
846 SqliteFraudRepository::new(self.pool.clone())
847 }
848
849 #[must_use]
851 pub fn search_configs(&self) -> SqliteSearchConfigRepository {
852 SqliteSearchConfigRepository::new(self.pool.clone())
853 }
854
855 #[must_use]
857 pub const fn pool(&self) -> &Pool<SqliteConnectionManager> {
858 &self.pool
859 }
860}
861
862pub(crate) fn map_db_error(e: rusqlite::Error) -> CommerceError {
864 match e {
865 rusqlite::Error::QueryReturnedNoRows => CommerceError::NotFound,
866 rusqlite::Error::ToSqlConversionFailure(boxed) => {
867 match boxed.downcast::<CommerceError>() {
869 Ok(commerce_error) => *commerce_error,
870 Err(other) => CommerceError::DatabaseError(other.to_string()),
871 }
872 }
873 rusqlite::Error::SqliteFailure(err, _) => {
874 match err.code {
875 rusqlite::ErrorCode::ConstraintViolation => {
877 let msg = e.to_string();
878 if msg.contains("UNIQUE") {
879 CommerceError::Conflict(msg)
880 } else {
881 CommerceError::ValidationError(msg)
882 }
883 }
884 _ => {
885 if err.extended_code == rusqlite::ffi::SQLITE_FULL {
887 CommerceError::Internal("Storage full: database disk is full".into())
888 } else {
889 CommerceError::DatabaseError(e.to_string())
890 }
891 }
892 }
893 }
894 _ => CommerceError::DatabaseError(e.to_string()),
895 }
896}
897
898pub(crate) use parse_helpers::{
900 parse_date_row,
901 parse_datetime,
902 parse_datetime_opt,
903 parse_datetime_opt_row,
904 parse_datetime_row,
905 parse_decimal as parse_decimal_strict,
906 parse_decimal_opt,
907 parse_decimal_opt_row,
908 parse_decimal_row,
909 parse_enum,
910 parse_enum_row,
911 parse_json_opt_row,
912 parse_json_row,
913 parse_uuid,
915 parse_uuid_opt,
916 parse_uuid_opt_row,
917 parse_uuid_row,
918};
919
920#[must_use]
925pub(crate) fn escape_like(input: &str) -> String {
926 let mut out = String::with_capacity(input.len());
927 for ch in input.chars() {
928 match ch {
929 '%' | '_' | '\\' => {
930 out.push('\\');
931 out.push(ch);
932 }
933 _ => out.push(ch),
934 }
935 }
936 out
937}
938
939pub(crate) fn build_in_clause(count: usize) -> String {
946 if count == 0 {
947 return "NULL".to_string();
948 }
949
950 std::iter::repeat_n("?", count).collect::<Vec<_>>().join(", ")
951}
952
953pub(crate) const DEFAULT_LIST_LIMIT: u32 = 500;
955
956pub(crate) const MAX_LIST_LIMIT: u32 = 1000;
958
959pub(crate) const fn effective_limit(limit: Option<u32>) -> u32 {
963 match limit {
964 Some(limit) if limit > MAX_LIST_LIMIT => MAX_LIST_LIMIT,
965 Some(limit) => limit,
966 None => DEFAULT_LIST_LIMIT,
967 }
968}
969
970pub(crate) fn append_limit_offset<O: std::fmt::Display>(
976 sql: &mut String,
977 limit: Option<u32>,
978 offset: Option<O>,
979) {
980 let limit = effective_limit(limit);
981 match offset {
982 Some(offset) => sql.push_str(&format!(" LIMIT {limit} OFFSET {offset}")),
983 None => sql.push_str(&format!(" LIMIT {limit}")),
984 }
985}
986
987pub(crate) fn json1_available(conn: &rusqlite::Connection) -> bool {
989 conn.query_row("SELECT json_valid('[]')", [], |row| row.get::<_, i32>(0))
990 .map(|value| value == 1)
991 .unwrap_or(false)
992}
993
994pub(crate) fn sum_decimal_query(
996 conn: &rusqlite::Connection,
997 sql: &str,
998 params: &[&dyn rusqlite::ToSql],
999 entity: &str,
1000 field: &str,
1001) -> stateset_core::Result<Decimal> {
1002 let mut stmt = conn.prepare(sql).map_err(map_db_error)?;
1003 let mut rows = stmt.query(params).map_err(map_db_error)?;
1004 let mut total = Decimal::ZERO;
1005
1006 while let Some(row) = rows.next().map_err(map_db_error)? {
1007 let raw: Option<String> = row.get(0).map_err(map_db_error)?;
1008 if let Some(raw) = raw {
1009 if !raw.is_empty() {
1010 total += parse_decimal_strict(&raw, entity, field)?;
1011 }
1012 }
1013 }
1014
1015 Ok(total)
1016}
1017
1018pub(crate) fn uuid_params(ids: &[uuid::Uuid]) -> Vec<Box<dyn rusqlite::ToSql>> {
1020 ids.iter().map(|id| Box::new(id.to_string()) as Box<dyn rusqlite::ToSql>).collect()
1021}
1022
1023pub(crate) fn params_refs(params: &[Box<dyn rusqlite::ToSql>]) -> Vec<&dyn rusqlite::ToSql> {
1025 params.iter().map(std::convert::AsRef::as_ref).collect()
1026}
1027
1028pub(crate) fn i64_params(ids: &[i64]) -> Vec<Box<dyn rusqlite::ToSql>> {
1030 ids.iter().map(|id| Box::new(*id) as Box<dyn rusqlite::ToSql>).collect()
1031}
1032
1033pub(crate) fn string_params(strings: &[String]) -> Vec<Box<dyn rusqlite::ToSql>> {
1035 strings.iter().map(|s| Box::new(s.clone()) as Box<dyn rusqlite::ToSql>).collect()
1036}
1037
1038const MAX_RETRIES: u32 = 50;
1044
1045const INITIAL_BACKOFF_MS: u64 = 1;
1047
1048const MAX_BACKOFF_MS: u64 = 200;
1050
1051pub(crate) fn is_retryable_error(e: &rusqlite::Error) -> bool {
1053 match e {
1054 rusqlite::Error::SqliteFailure(ffi_err, msg) => {
1055 matches!(
1056 ffi_err.code,
1057 rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
1058 ) || msg.as_ref().is_some_and(|m| {
1059 m.contains("database is locked") || m.contains("database table is locked")
1060 })
1061 }
1062 _ => false,
1063 }
1064}
1065
1066pub(crate) fn retry_jitter(retries: u32) -> u64 {
1073 use std::cell::Cell;
1074 use std::hash::{BuildHasher, Hasher, RandomState};
1075
1076 thread_local! {
1077 static SEED: Cell<u64> = Cell::new({
1078 let mut hasher = RandomState::new().build_hasher();
1081 hasher.write_u64(0xa076_1d64_78bd_642f);
1082 hasher.finish().max(1)
1083 });
1084 }
1085
1086 SEED.with(|seed| {
1087 let mut s = seed.get().wrapping_add(u64::from(retries));
1088 s ^= s << 13;
1089 s ^= s >> 7;
1090 s ^= s << 17;
1091 seed.set(s);
1092 s % 50
1093 })
1094}
1095
1096pub(crate) fn with_retry<T, F>(mut f: F, max_retries: u32) -> Result<T, rusqlite::Error>
1097where
1098 F: FnMut() -> Result<T, rusqlite::Error>,
1099{
1100 let mut retries = 0;
1101 let mut backoff_ms = INITIAL_BACKOFF_MS;
1102
1103 loop {
1104 match f() {
1105 Ok(result) => return Ok(result),
1106 Err(e) if is_retryable_error(&e) && retries < max_retries => {
1107 retries += 1;
1108 let jitter = retry_jitter(retries);
1109 let delay = backoff_ms.min(MAX_BACKOFF_MS) + jitter;
1110 thread::sleep(Duration::from_millis(delay));
1111 backoff_ms = (backoff_ms * 2).min(MAX_BACKOFF_MS);
1113 }
1114 Err(e) => return Err(e),
1115 }
1116 }
1117}
1118
1119const SLOW_QUERY_THRESHOLD_MS: u128 = 500;
1121
1122pub(crate) fn begin_immediate(
1129 conn: &mut rusqlite::Connection,
1130) -> rusqlite::Result<rusqlite::Transaction<'_>> {
1131 conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
1132}
1133
1134pub(crate) fn with_immediate_transaction<T, F>(
1138 pool: &Pool<SqliteConnectionManager>,
1139 f: F,
1140) -> stateset_core::Result<T>
1141where
1142 F: Fn(&rusqlite::Transaction<'_>) -> Result<T, rusqlite::Error>,
1143{
1144 let start = std::time::Instant::now();
1145 let result = with_retry(
1146 || {
1147 let mut conn = pool.get().map_err(|e| {
1148 rusqlite::Error::SqliteFailure(
1149 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
1150 Some(e.to_string()),
1151 )
1152 })?;
1153
1154 conn.execute_batch("PRAGMA defer_foreign_keys = ON")?;
1156
1157 let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
1159
1160 let result = f(&tx)?;
1161 tx.commit()?;
1162 Ok(result)
1163 },
1164 MAX_RETRIES,
1165 )
1166 .map_err(map_db_error);
1167
1168 let elapsed = start.elapsed();
1169 if elapsed.as_millis() > SLOW_QUERY_THRESHOLD_MS {
1170 tracing::warn!(
1171 duration_ms = elapsed.as_millis() as u64,
1172 "Slow transaction detected (>{SLOW_QUERY_THRESHOLD_MS}ms)"
1173 );
1174 }
1175
1176 result
1177}
1178
1179use crate::DatabaseExt;
1181
1182impl DatabaseExt for SqliteDatabase {
1183 fn with_transaction<F, T>(&self, f: F) -> stateset_core::Result<T>
1184 where
1185 F: FnMut(&rusqlite::Connection) -> std::result::Result<T, rusqlite::Error>,
1186 {
1187 self.with_transaction_opts(crate::TransactionOptions::new(), f)
1188 }
1189
1190 fn with_transaction_opts<F, T>(
1191 &self,
1192 opts: crate::TransactionOptions,
1193 mut f: F,
1194 ) -> stateset_core::Result<T>
1195 where
1196 F: FnMut(&rusqlite::Connection) -> std::result::Result<T, rusqlite::Error>,
1197 {
1198 let retries = if opts.retry_on_conflict { opts.max_retries } else { 0 };
1199 let timeout_ms = opts.timeout_ms.unwrap_or(crate::DEFAULT_TRANSACTION_TIMEOUT_MS);
1200 let set_read_uncommitted =
1201 matches!(opts.isolation, crate::TransactionIsolation::ReadUncommitted);
1202 let fallback_timeout_ms = timeout_ms;
1203
1204 crate::sqlite::with_retry(
1205 || {
1206 let conn = self.pool.get().map_err(|e| {
1207 rusqlite::Error::SqliteFailure(
1208 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
1209 Some(e.to_string()),
1210 )
1211 })?;
1212 let mut conn =
1213 PragmaScope::new(conn, timeout_ms, set_read_uncommitted, fallback_timeout_ms)?;
1214
1215 {
1216 let tx =
1217 conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
1218 let result = panic::catch_unwind(AssertUnwindSafe(|| f(&tx)));
1219
1220 match result {
1221 Ok(result) => {
1222 let result = result?;
1223 tx.commit()?;
1224 Ok(result)
1225 }
1226 Err(panic_payload) => {
1227 panic::resume_unwind(panic_payload);
1228 }
1229 }
1230 }
1231 },
1232 retries,
1233 )
1234 .map_err(map_db_error)
1235 }
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241 use std::cell::Cell;
1242
1243 #[test]
1244 fn append_limit_offset_covers_all_cases() {
1245 let case = |limit: Option<u32>, offset: Option<u32>| {
1246 let mut sql = "SELECT * FROM t".to_string();
1247 append_limit_offset(&mut sql, limit, offset);
1248 sql
1249 };
1250 assert_eq!(case(Some(10), Some(5)), "SELECT * FROM t LIMIT 10 OFFSET 5");
1251 assert_eq!(case(Some(10), None), "SELECT * FROM t LIMIT 10");
1252 assert_eq!(case(None, None), "SELECT * FROM t LIMIT 500");
1254 assert_eq!(case(None, Some(5)), "SELECT * FROM t LIMIT 500 OFFSET 5");
1257 assert_eq!(case(Some(5000), None), "SELECT * FROM t LIMIT 1000");
1259 }
1260
1261 fn retryable_error() -> rusqlite::Error {
1262 rusqlite::Error::SqliteFailure(
1263 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
1264 Some("database is locked".to_string()),
1265 )
1266 }
1267
1268 #[test]
1269 fn build_in_clause_empty_is_null() {
1270 assert_eq!(build_in_clause(0), "NULL".to_string());
1271 }
1272
1273 #[test]
1274 fn build_in_clause_uses_question_mark_placeholders() {
1275 assert_eq!(build_in_clause(3), "?, ?, ?".to_string());
1276 }
1277
1278 #[test]
1279 fn with_transaction_restores_read_uncommitted_pragma() {
1280 let db = SqliteDatabase::new(&DatabaseConfig {
1281 url: ":memory:".to_string(),
1282 max_connections: 1,
1283 })
1284 .expect("db should initialize");
1285
1286 {
1287 let conn = db.conn().expect("connection should open");
1288 conn.execute_batch("PRAGMA read_uncommitted = true")
1289 .expect("read_uncommitted pragma should be set");
1290 let before: i64 = conn
1291 .query_row("PRAGMA read_uncommitted", [], |row| row.get::<_, i64>(0))
1292 .expect("read_uncommitted pragma should be readable");
1293 assert_eq!(before, 1);
1294 }
1295
1296 db.with_transaction_opts(
1297 crate::TransactionOptions::new()
1298 .isolation(crate::TransactionIsolation::ReadUncommitted),
1299 |conn| {
1300 conn.execute_batch("PRAGMA read_uncommitted = true")?;
1301 Ok(())
1302 },
1303 )
1304 .expect("transaction should succeed");
1305
1306 let conn = db.conn().expect("connection should reopen");
1307 let after: i64 = conn
1308 .query_row("PRAGMA read_uncommitted", [], |row| row.get::<_, i64>(0))
1309 .expect("read_uncommitted pragma should be readable");
1310 assert_eq!(after, 1);
1311 }
1312
1313 #[test]
1314 fn with_transaction_restores_pragmas_after_panic() {
1315 let db = SqliteDatabase::new(&DatabaseConfig {
1316 url: ":memory:".to_string(),
1317 max_connections: 1,
1318 })
1319 .expect("db should initialize");
1320
1321 {
1322 let conn = db.conn().expect("connection should open");
1323 conn.execute_batch("PRAGMA busy_timeout = 25; PRAGMA read_uncommitted = false;")
1324 .expect("initial pragmas should be set");
1325 }
1326
1327 let panic_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1328 let _ = db.with_transaction_opts(
1329 crate::TransactionOptions::new()
1330 .timeout_ms(250)
1331 .isolation(crate::TransactionIsolation::ReadUncommitted),
1332 |_conn| -> std::result::Result<(), rusqlite::Error> {
1333 panic!("boom");
1334 },
1335 );
1336 }));
1337 assert!(panic_result.is_err());
1338
1339 let conn = db.conn().expect("connection should reopen");
1340 let busy_timeout: u64 = conn
1341 .query_row("PRAGMA busy_timeout", [], |row| row.get::<_, u64>(0))
1342 .expect("busy_timeout pragma should be readable");
1343 let read_uncommitted: i64 = conn
1344 .query_row("PRAGMA read_uncommitted", [], |row| row.get::<_, i64>(0))
1345 .expect("read_uncommitted pragma should be readable");
1346 assert_eq!(busy_timeout, 25);
1347 assert_eq!(read_uncommitted, 0);
1348 }
1349
1350 #[test]
1351 fn with_retry_respects_zero_retries() {
1352 let attempts = Cell::new(0u32);
1353 let err: std::result::Result<(), rusqlite::Error> = with_retry(
1354 || {
1355 attempts.set(attempts.get() + 1);
1356 Err(retryable_error())
1357 },
1358 0,
1359 );
1360
1361 assert!(err.is_err());
1362 assert_eq!(attempts.get(), 1);
1363 }
1364
1365 #[test]
1366 fn with_retry_retries_until_success() {
1367 let attempts = Cell::new(0u32);
1368 let result = with_retry(
1369 || {
1370 let attempt = attempts.get() + 1;
1371 attempts.set(attempt);
1372 if attempt < 3 { Err(retryable_error()) } else { Ok(attempt) }
1373 },
1374 5,
1375 )
1376 .expect("operation should succeed after retries");
1377
1378 assert_eq!(result, 3);
1379 assert_eq!(attempts.get(), 3);
1380 }
1381
1382 #[test]
1383 fn retry_jitter_is_decorrelated_across_threads() {
1384 let sequences: Vec<Vec<u64>> = (0..4)
1388 .map(|_| {
1389 std::thread::spawn(|| (1..=8).map(retry_jitter).collect::<Vec<u64>>())
1390 .join()
1391 .expect("jitter thread panicked")
1392 })
1393 .collect();
1394
1395 let distinct: std::collections::HashSet<&Vec<u64>> = sequences.iter().collect();
1396 assert!(
1397 distinct.len() > 1,
1398 "all {} threads drew the identical jitter sequence {:?}",
1399 sequences.len(),
1400 sequences[0]
1401 );
1402 }
1403}