1use chrono::{DateTime, Utc};
4
5use crate::error::{BackendError, StorageResult};
6use crate::search::{converters::IndexValue, extractor::ExtractedValue};
7
8fn internal_error(message: String) -> crate::error::StorageError {
9 crate::error::StorageError::Backend(BackendError::Internal {
10 backend_name: "postgres".to_string(),
11 message,
12 source: None,
13 })
14}
15
16pub struct PostgresSearchIndexWriter;
18
19impl PostgresSearchIndexWriter {
20 pub async fn write_entry(
25 client: &deadpool_postgres::Client,
26 tenant_id: &str,
27 resource_type: &str,
28 resource_id: &str,
29 extracted: &ExtractedValue,
30 ) -> StorageResult<()> {
31 match &extracted.value {
32 IndexValue::String(s) => {
33 client
34 .execute(
35 "INSERT INTO search_index (
36 tenant_id, resource_type, resource_id, param_name, param_url,
37 value_string, composite_group
38 ) VALUES ($1, $2, $3, $4, $5, $6, $7)",
39 &[
40 &tenant_id,
41 &resource_type,
42 &resource_id,
43 &extracted.param_name.as_str(),
44 &extracted.param_url.as_str(),
45 &Some(s.as_str()),
46 &extracted.composite_group.map(|g| g as i32),
47 ],
48 )
49 .await
50 .map_err(|e| {
51 internal_error(format!("Failed to insert string search index entry: {}", e))
52 })?;
53 }
54 IndexValue::Token {
55 system,
56 code,
57 display,
58 identifier_type_system,
59 identifier_type_code,
60 } => {
61 client
62 .execute(
63 "INSERT INTO search_index (
64 tenant_id, resource_type, resource_id, param_name, param_url,
65 value_token_system, value_token_code, value_token_display,
66 composite_group, value_identifier_type_system, value_identifier_type_code
67 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
68 &[
69 &tenant_id,
70 &resource_type,
71 &resource_id,
72 &extracted.param_name.as_str(),
73 &extracted.param_url.as_str(),
74 &system.as_deref(),
75 &code.as_str(),
76 &display.as_deref(),
77 &extracted.composite_group.map(|g| g as i32),
78 &identifier_type_system.as_deref(),
79 &identifier_type_code.as_deref(),
80 ],
81 )
82 .await
83 .map_err(|e| {
84 internal_error(format!("Failed to insert token search index entry: {}", e))
85 })?;
86 }
87 IndexValue::Date { value, precision } => {
88 let precision_str = precision.to_string();
89 let normalized = normalize_date_for_pg(value);
90 let timestamp: DateTime<Utc> = DateTime::parse_from_rfc3339(&normalized)
91 .map(|dt| dt.with_timezone(&Utc))
92 .or_else(|_| normalized.parse::<DateTime<Utc>>())
93 .unwrap_or_else(|_| Utc::now());
94 client
95 .execute(
96 "INSERT INTO search_index (
97 tenant_id, resource_type, resource_id, param_name, param_url,
98 value_date, value_date_precision, composite_group
99 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
100 &[
101 &tenant_id,
102 &resource_type,
103 &resource_id,
104 &extracted.param_name.as_str(),
105 &extracted.param_url.as_str(),
106 ×tamp,
107 &precision_str.as_str(),
108 &extracted.composite_group.map(|g| g as i32),
109 ],
110 )
111 .await
112 .map_err(|e| {
113 internal_error(format!("Failed to insert date search index entry: {}", e))
114 })?;
115 }
116 IndexValue::Number(n) => {
117 client
118 .execute(
119 "INSERT INTO search_index (
120 tenant_id, resource_type, resource_id, param_name, param_url,
121 value_number, composite_group
122 ) VALUES ($1, $2, $3, $4, $5, $6, $7)",
123 &[
124 &tenant_id,
125 &resource_type,
126 &resource_id,
127 &extracted.param_name.as_str(),
128 &extracted.param_url.as_str(),
129 n,
130 &extracted.composite_group.map(|g| g as i32),
131 ],
132 )
133 .await
134 .map_err(|e| {
135 internal_error(format!("Failed to insert number search index entry: {}", e))
136 })?;
137 }
138 IndexValue::Quantity {
139 value,
140 unit,
141 system,
142 code: _,
143 } => {
144 client
145 .execute(
146 "INSERT INTO search_index (
147 tenant_id, resource_type, resource_id, param_name, param_url,
148 value_quantity_value, value_quantity_unit, value_quantity_system, composite_group
149 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
150 &[
151 &tenant_id,
152 &resource_type,
153 &resource_id,
154 &extracted.param_name.as_str(),
155 &extracted.param_url.as_str(),
156 value,
157 &unit.as_deref(),
158 &system.as_deref(),
159 &extracted.composite_group.map(|g| g as i32),
160 ],
161 )
162 .await
163 .map_err(|e| {
164 internal_error(format!(
165 "Failed to insert quantity search index entry: {}",
166 e
167 ))
168 })?;
169 }
170 IndexValue::Reference {
171 reference,
172 resource_type: _,
173 resource_id: _,
174 } => {
175 client
176 .execute(
177 "INSERT INTO search_index (
178 tenant_id, resource_type, resource_id, param_name, param_url,
179 value_reference, composite_group
180 ) VALUES ($1, $2, $3, $4, $5, $6, $7)",
181 &[
182 &tenant_id,
183 &resource_type,
184 &resource_id,
185 &extracted.param_name.as_str(),
186 &extracted.param_url.as_str(),
187 &reference.as_str(),
188 &extracted.composite_group.map(|g| g as i32),
189 ],
190 )
191 .await
192 .map_err(|e| {
193 internal_error(format!(
194 "Failed to insert reference search index entry: {}",
195 e
196 ))
197 })?;
198 }
199 IndexValue::Uri(uri) => {
200 client
201 .execute(
202 "INSERT INTO search_index (
203 tenant_id, resource_type, resource_id, param_name, param_url,
204 value_uri, composite_group
205 ) VALUES ($1, $2, $3, $4, $5, $6, $7)",
206 &[
207 &tenant_id,
208 &resource_type,
209 &resource_id,
210 &extracted.param_name.as_str(),
211 &extracted.param_url.as_str(),
212 &uri.as_str(),
213 &extracted.composite_group.map(|g| g as i32),
214 ],
215 )
216 .await
217 .map_err(|e| {
218 internal_error(format!("Failed to insert URI search index entry: {}", e))
219 })?;
220 }
221 }
222
223 Ok(())
224 }
225}
226
227fn normalize_date_for_pg(value: &str) -> String {
235 if value.contains('T') {
236 if value.contains('+') || value.contains('Z') || value.ends_with("-00:00") {
238 value.to_string()
239 } else {
240 format!("{}+00:00", value)
241 }
242 } else if value.len() == 10 {
243 format!("{}T00:00:00+00:00", value)
245 } else if value.len() == 7 {
246 format!("{}-01T00:00:00+00:00", value)
248 } else if value.len() == 4 {
249 format!("{}-01-01T00:00:00+00:00", value)
251 } else {
252 value.to_string()
254 }
255}