1use super::{
10 blob_marker, decode_json_field_value, decode_legacy_document_body, delete_document_blob,
11 document_id_from_sqlite, encode_document_blobs, encode_stored_value, hydrate_document_blobs,
12 params, sqlite_doc_id, upsert_document_blob, BTreeMap, DocId, Document, DocumentMetadata,
13 ManagedConnection, OptionalExtension, SQLiteDocumentStore, SQLiteResult, StorageBackendResult,
14 StoredDocument, Value, DOCUMENT_BLOBS_TABLE,
15};
16
17impl SQLiteDocumentStore {
18 pub fn new(conn: ManagedConnection, table: impl Into<String>) -> Self {
19 Self {
20 conn,
21 table: table.into(),
22 }
23 }
24
25 pub fn max_doc_id(&self) -> StorageBackendResult<DocId> {
26 Ok(self.conn.with(|c| {
27 let id: Option<i64> = c
28 .prepare_cached("SELECT MAX(doc_id) FROM _documents WHERE table_name = ?1")?
29 .query_row(params![self.table], |r| r.get(0))?;
30 id.map_or(Ok(0), document_id_from_sqlite)
31 })?)
32 }
33
34 pub(super) fn put_stored_inner(
35 &self,
36 doc_id: DocId,
37 document: &Document,
38 metadata: DocumentMetadata,
39 ) -> SQLiteResult<()> {
40 let sqlite_doc_id = sqlite_doc_id(doc_id)?;
41 let document: Document = document
42 .iter()
43 .filter(|(_, value)| !matches!(value, uqa_core::Value::Null))
44 .map(|(key, value)| (key.clone(), value.clone()))
45 .collect();
46 let (document, blobs) = encode_document_blobs(document)?;
47 let body = serde_json::to_string(&document)?;
48 self.conn.with(|c| {
49 c.prepare_cached(&format!(
50 "DELETE FROM {DOCUMENT_BLOBS_TABLE}
51 WHERE table_name = ?1 AND doc_id = ?2"
52 ))?
53 .execute(params![self.table, sqlite_doc_id])?;
54 c.prepare_cached(
55 "INSERT INTO _documents (table_name, doc_id, body, tuple_xmin)
56 VALUES (?1, ?2, ?3, ?4)
57 ON CONFLICT (table_name, doc_id)
58 DO UPDATE SET body = excluded.body, tuple_xmin = excluded.tuple_xmin",
59 )?
60 .execute(params![
61 self.table,
62 sqlite_doc_id,
63 body,
64 metadata.tuple_xmin().map(i64::from),
65 ])?;
66 for (field, bytes) in blobs {
67 c.prepare_cached(&format!(
68 "INSERT OR REPLACE INTO {DOCUMENT_BLOBS_TABLE}
69 (table_name, doc_id, field_name, bytes)
70 VALUES (?1, ?2, ?3, ?4)"
71 ))?
72 .execute(params![self.table, sqlite_doc_id, field, bytes])?;
73 }
74 Ok(())
75 })
76 }
77
78 pub(super) fn get_inner(&self, doc_id: DocId) -> SQLiteResult<Option<Document>> {
79 self.get_stored_inner(doc_id)
80 .map(|document| document.map(StoredDocument::into_fields))
81 }
82
83 pub(super) fn get_stored_inner(&self, doc_id: DocId) -> SQLiteResult<Option<StoredDocument>> {
84 let sqlite_doc_id = sqlite_doc_id(doc_id)?;
85 self.conn.with(|c| {
86 let stored: Option<(String, Option<i64>)> = c
87 .prepare_cached(
88 "SELECT body, tuple_xmin FROM _documents
89 WHERE table_name = ?1 AND doc_id = ?2",
90 )?
91 .query_row(params![self.table, sqlite_doc_id], |row| {
92 Ok((row.get(0)?, row.get(1)?))
93 })
94 .optional()?;
95 let Some((body, tuple_xmin)) = stored else {
96 return Ok(None);
97 };
98 let mut document = decode_legacy_document_body(&body)?;
99 hydrate_document_blobs(c, &self.table, doc_id, &mut document)?;
100 let metadata = tuple_xmin.map_or_else(
101 || Ok(DocumentMetadata::default()),
102 |tuple_xmin| {
103 u32::try_from(tuple_xmin)
104 .map(DocumentMetadata::with_tuple_xmin)
105 .map_err(|_| {
106 super::SQLiteError::StorageBackend(format!(
107 "document `{}` row {doc_id} has an out-of-range tuple xmin",
108 self.table
109 ))
110 })
111 },
112 )?;
113 Ok(Some(StoredDocument::with_metadata(document, metadata)))
114 })
115 }
116
117 pub(super) fn get_field_inner(
118 &self,
119 doc_id: DocId,
120 field: &str,
121 ) -> SQLiteResult<Option<Value>> {
122 let sqlite_doc_id = sqlite_doc_id(doc_id)?;
123 let path = sqlite_json_path(field);
124 self.conn.with(|c| {
125 let row: Option<(Option<String>, String)> = c
126 .prepare_cached(
127 "SELECT json_type(body, ?3), json_quote(json_extract(body, ?3))
128 FROM _documents
129 WHERE table_name = ?1 AND doc_id = ?2",
130 )?
131 .query_row(params![self.table, sqlite_doc_id, path], |r| {
132 Ok((r.get(0)?, r.get(1)?))
133 })
134 .optional()?;
135 let Some((json_type, json_text)) = row else {
136 return Ok(None);
137 };
138 decode_json_field_value(c, &self.table, doc_id, field, json_type, &json_text)
139 })
140 }
141
142 pub(super) fn find_doc_id_by_field_inner(
143 &self,
144 field: &str,
145 value: &Value,
146 ) -> SQLiteResult<Option<DocId>> {
147 let path = sqlite_json_path(field);
148 match value {
149 Value::Str(value) => self
150 .conn
151 .with(|c| find_doc_id_by_scalar(c, &self.table, &path, value)),
152 Value::Int(value) => self
153 .conn
154 .with(|c| find_doc_id_by_scalar(c, &self.table, &path, value)),
155 Value::Float(value) if value.is_finite() => self
156 .conn
157 .with(|c| find_doc_id_by_scalar(c, &self.table, &path, value)),
158 Value::Bool(value) => self.conn.with(|c| {
159 let json_type = if *value { "true" } else { "false" };
160 let doc_id: Option<i64> = c
161 .query_row(
162 "SELECT doc_id FROM _documents
163 WHERE table_name = ?1 AND json_type(body, ?2) = ?3
164 ORDER BY doc_id LIMIT 1",
165 params![self.table, path, json_type],
166 |r| r.get(0),
167 )
168 .optional()?;
169 doc_id.map(document_id_from_sqlite).transpose()
170 }),
171 _ => {
172 let doc_ids = self.conn.with(|c| {
173 let mut stmt = c.prepare_cached(
174 "SELECT doc_id FROM _documents
175 WHERE table_name = ?1 ORDER BY doc_id",
176 )?;
177 let rows = stmt.query_map(params![self.table], |row| row.get::<_, i64>(0))?;
178 let mut out = Vec::new();
179 for row in rows {
180 out.push(document_id_from_sqlite(row?)?);
181 }
182 Ok(out)
183 })?;
184 for doc_id in doc_ids {
185 if self.get_field_inner(doc_id, field)?.as_ref() == Some(value) {
186 return Ok(Some(doc_id));
187 }
188 }
189 Ok(None)
190 }
191 }
192 }
193
194 pub(super) fn patch_fields_inner(
195 &self,
196 doc_id: DocId,
197 updates: &BTreeMap<String, Value>,
198 ) -> SQLiteResult<bool> {
199 if updates.is_empty() {
200 return Ok(true);
201 }
202 let sqlite_doc_id = sqlite_doc_id(doc_id)?;
203 self.conn.with(|c| {
204 let exists: Option<i64> = c
205 .prepare_cached(
206 "SELECT 1 FROM _documents
207 WHERE table_name = ?1 AND doc_id = ?2
208 LIMIT 1",
209 )?
210 .query_row(params![self.table, sqlite_doc_id], |r| r.get(0))
211 .optional()?;
212 if exists.is_none() {
213 return Ok(false);
214 }
215
216 for (field, value) in updates {
217 let path = sqlite_json_path(field);
218 match value {
219 Value::Null => {
220 delete_document_blob(c, &self.table, doc_id, field)?;
221 c.execute(
222 "UPDATE _documents SET body = json_remove(body, ?3)
223 WHERE table_name = ?1 AND doc_id = ?2",
224 params![self.table, sqlite_doc_id, path],
225 )?;
226 }
227 Value::Bytes(bytes) => {
228 let marker = serde_json::to_string(&blob_marker(field.clone()))?;
229 c.execute(
230 "UPDATE _documents SET body = json_set(body, ?3, json(?4))
231 WHERE table_name = ?1 AND doc_id = ?2",
232 params![self.table, sqlite_doc_id, path, marker],
233 )?;
234 upsert_document_blob(c, &self.table, doc_id, field, bytes)?;
235 }
236 other => {
237 let (stored, blob) = encode_stored_value(field, other.clone())?;
238 let json = serde_json::to_string(&stored)?;
239 c.execute(
240 "UPDATE _documents SET body = json_set(body, ?3, json(?4))
241 WHERE table_name = ?1 AND doc_id = ?2",
242 params![self.table, sqlite_doc_id, path, json],
243 )?;
244 if let Some(bytes) = blob {
245 upsert_document_blob(c, &self.table, doc_id, field, &bytes)?;
246 } else {
247 delete_document_blob(c, &self.table, doc_id, field)?;
248 }
249 }
250 }
251 }
252 Ok(true)
253 })
254 }
255}
256
257fn sqlite_json_path(field: &str) -> String {
258 if field.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
259 && field
260 .chars()
261 .next()
262 .is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
263 {
264 format!("$.{field}")
265 } else {
266 format!("$.{}", serde_json::Value::String(field.to_string()))
267 }
268}
269
270fn find_doc_id_by_scalar<T: rusqlite::ToSql>(
271 conn: &rusqlite::Connection,
272 table: &str,
273 path: &str,
274 value: &T,
275) -> SQLiteResult<Option<DocId>> {
276 let doc_id: Option<i64> = conn
277 .query_row(
278 "SELECT doc_id FROM _documents
279 WHERE table_name = ?1 AND json_extract(body, ?2) = ?3
280 ORDER BY doc_id LIMIT 1",
281 (table, path, value),
282 |r| r.get(0),
283 )
284 .optional()?;
285 doc_id.map(document_id_from_sqlite).transpose()
286}