1mod deserializer;
2mod neon_response;
3mod query_builder;
4mod request;
5mod transaction_builder;
6
7#[cfg(feature = "orm_beta")]
8pub mod orm;
9
10use anyhow::{Context, Result};
11pub use neon_response::{QueryResponse, QueryResult, TransactionResponse, TransactionResult};
12pub use query_builder::{Query, QueryBuilder};
13use request::post;
14
15pub use transaction_builder::{Transaction, TransactionBuilder};
16
17#[cfg(feature = "orm_beta")]
18pub use sql_macro::*;
19
20pub struct Client {
21 pub(crate) connection_string: String,
22 pub(crate) url: String,
23 #[cfg(target_os = "wasi")]
24 pub client: wstd::http::Client,
25 #[cfg(not(target_os = "wasi"))]
26 pub client: reqwest::Client,
27}
28
29impl Client {
30 pub fn new_from_env() -> Result<Self> {
32 let connection_string = std::env::var("NEON_CONNECTION_STRING")
33 .context("ENV \"NEON_CONNECTION_STRING\" isn't set")?;
34
35 Self::new(&connection_string)
36 }
37
38 pub fn new(connection_string: &str) -> Result<Self> {
39 let host = connection_string
40 .split('@')
41 .next_back()
42 .context("Invalid connection string, missing credentials")?;
43 let host = host
44 .split('/')
45 .next()
46 .context("Invalid connection string, missing db path")?;
47 let protocol = if host == "db.localtest.me:4444" {
48 "http".to_string()
49 } else {
50 "https".to_string()
51 };
52
53 Ok(Self {
54 connection_string: connection_string.to_owned(),
55 client: Default::default(),
56 url: format!("{protocol}://{host}/sql"),
57 })
58 }
59
60 pub async fn execute(&self, query: Query) -> Result<()> {
62 self.execute_raw(query, false).await?;
63 Ok(())
64 }
65
66 pub async fn execute_raw(&self, sql: Query, is_select: bool) -> Result<QueryResponse> {
68 post(
69 self,
70 &self.url,
71 if is_select {
72 serde_json::json!({ "query": format!(
73 "WITH SelectQueryRes AS (
74 {0}
75 )
76 SELECT row_to_json(SelectQueryRes) as jsonb_build_object FROM SelectQueryRes;",
77 sql.query
78 ), "params": sql.params })
79 } else {
80 serde_json::json!({ "query": sql.query, "params": sql.params })
81 },
82 )
83 .await
84 }
85
86 pub async fn execute_transaction(&self, transaction: Transaction) -> Result<()> {
88 self.execute_transaction_raw(transaction).await?;
89 Ok(())
90 }
91
92 pub async fn execute_transaction_raw(&self, sql: Transaction) -> Result<TransactionResponse> {
94 post(self, &self.url, sql).await
95 }
96
97 #[cfg(feature = "orm_beta")]
98 pub(crate) async fn execute_orm(&self, transaction: serde_json::Value) -> Result<()> {
99 self.execute_orm_raw(transaction).await?;
100 Ok(())
101 }
102
103 #[cfg(feature = "orm_beta")]
104 pub(crate) async fn execute_orm_raw(
105 &self,
106 sql: serde_json::Value,
107 ) -> Result<TransactionResponse> {
108 post(self, &self.url, sql).await
109 }
110
111 #[cfg(feature = "orm_beta")]
112 pub(crate) async fn execute_orm_raw_query(
113 &self,
114 sql: serde_json::Value,
115 ) -> Result<QueryResponse> {
116 post(self, &self.url, sql).await
117 }
118}
119
120#[cfg(test)]
121mod test {
122 use super::*;
123 use anyhow::Result;
124 use serde::{Deserialize, Serialize};
125
126 #[cfg(feature = "orm_beta")]
127 #[derive(Debug, Serialize, Deserialize, NeonTable, Clone)]
128 #[neon_table(table_name = "test", pk = "id", crate_path = "crate")]
129 pub struct TestTable {
130 pub id: Option<u64>,
131 pub name: Option<String>,
132 pub description: Option<String>,
133 #[neon_table(is_related)]
134 pub history: Vec<TestHistory>,
135 #[neon_table(is_related)]
136 pub data: Option<TestData>,
137 }
138
139 #[cfg(feature = "orm_beta")]
140 #[derive(Debug, Serialize, Deserialize, NeonTable, Clone)]
141 #[neon_table(table_name = "test_history", pk = "id", crate_path = "crate")]
142 pub struct TestHistory {
143 pub id: Option<u64>,
144 pub test_id: Option<u64>,
145 pub state: TestHistoryState,
146 }
147
148 #[cfg(feature = "orm_beta")]
149 #[derive(Debug, Serialize, Deserialize, NeonTable, Clone)]
150 #[neon_table(
151 table_name = "test_data",
152 pk = "id",
153 crate_path = "crate",
154 on_conflict = "state"
155 )]
156 pub struct TestData {
157 pub id: Option<u64>,
158 pub test_id: Option<u64>,
159 pub state: TestHistoryState,
160 }
161
162 #[derive(Debug, Serialize, Deserialize, Clone)]
163 #[serde(tag = "type", content = "data")]
164 pub enum TestHistoryState {
165 Active,
166 Closed { closed_at: String },
167 Value { int_value: i64 },
168 }
169
170 #[wstd::test]
171 pub async fn test() -> Result<()> {
172 let client = Client::new("<CONNECT_STRING>")?;
173
174 QueryBuilder::new("SELECT * FROM playing_with_neon")
175 .execute_raw(&client, true)
176 .await?;
177
178 TransactionBuilder::new()
179 .add(QueryBuilder::new("SELECT * FROM playing_with_neon").build())
180 .add(QueryBuilder::new("SELECT * FROM playing_with_neon").build())
181 .execute_raw(&client)
182 .await?;
183
184 Ok(())
185 }
186
187 #[cfg(feature = "orm_beta")]
188 #[test]
189 pub fn test_orm_insert_generation() {
190 let item = TestTable {
191 id: None,
192 name: Some("Test Name".to_string()),
193 description: None,
194 history: vec![
195 TestHistory {
196 id: None,
197 test_id: None,
198 state: TestHistoryState::Active,
199 },
200 TestHistory {
201 id: None,
202 test_id: None,
203 state: TestHistoryState::Closed {
204 closed_at: "Yesterday".to_string(),
205 },
206 },
207 TestHistory {
208 id: None,
209 test_id: None,
210 state: TestHistoryState::Value { int_value: 12 },
211 },
212 ],
213 data: Some(TestData {
214 id: None,
215 test_id: None,
216 state: TestHistoryState::Active,
217 }),
218 };
219
220 let payload = orm::OrmBuilder::new().insert(item).build();
221
222 println!("Payload-Query: => {}", payload["queries"][0]["query"]);
223 println!("Payload-params: => {}", payload["queries"][0]["params"]);
224
225 let expected_sql = "WITH inserted_parent AS (INSERT INTO test (name) VALUES ($1) RETURNING id) , child_history_0_0 AS (INSERT INTO test_history (test_id, state) VALUES ((SELECT id FROM inserted_parent), $2) RETURNING id) , child_history_1_0 AS (INSERT INTO test_history (test_id, state) VALUES ((SELECT id FROM inserted_parent), $3) RETURNING id) , child_history_2_0 AS (INSERT INTO test_history (test_id, state) VALUES ((SELECT id FROM inserted_parent), $4) RETURNING id) , child_data_0_0 AS (INSERT INTO test_data (test_id, state) VALUES ((SELECT id FROM inserted_parent), $5) ON CONFLICT (state) DO NOTHING RETURNING id) SELECT id FROM inserted_parent";
226
227 let expected_params = serde_json::json!([
228 "Test Name",
229 "{\"type\":\"Active\"}",
230 "{\"data\":{\"closed_at\":\"Yesterday\"},\"type\":\"Closed\"}",
231 "{\"data\":{\"int_value\":12},\"type\":\"Value\"}",
232 "{\"type\":\"Active\"}",
233 ]);
234
235 assert_eq!(payload["queries"][0]["query"], expected_sql);
236 assert_eq!(payload["queries"][0]["params"], expected_params);
237 }
238
239 #[cfg(feature = "orm_beta")]
240 #[test]
241 pub fn test_orm_insert_with_empty_child_generation() {
242 let item = TestTable {
243 id: None,
244 name: Some("Test Name".to_string()),
245 description: None,
246 history: Vec::new(),
247 data: None,
248 };
249
250 let payload = orm::OrmBuilder::new().insert(item).build();
251
252 println!("Payload-Query: => {}", payload["queries"][0]["query"]);
253 println!("Payload-params: => {}", payload["queries"][0]["params"]);
254
255 let expected_sql = "INSERT INTO test (name) VALUES ($1)";
256
257 let expected_params = serde_json::json!(["Test Name"]);
258
259 assert_eq!(payload["queries"][0]["query"], expected_sql);
260 assert_eq!(payload["queries"][0]["params"], expected_params);
261 }
262
263 #[cfg(feature = "orm_beta")]
264 #[test]
265 fn test_orm_update_generation() {
266 let item = TestTable {
267 id: Some(42),
268 name: Some("Updated Test Name".to_string()),
269 description: None,
270 history: vec![],
271 data: None,
272 };
273
274 let payload = orm::OrmBuilder::new().update(item).build();
275
276 let expected_sql = "UPDATE test SET name = $1 WHERE id = $2";
277 let expected_params = serde_json::json!(["Updated Test Name", 42]);
278
279 let queries = &payload["queries"];
280 assert_eq!(queries[0]["query"].as_str().unwrap(), expected_sql);
281 assert_eq!(queries[0]["params"], expected_params);
282 }
283
284 #[cfg(feature = "orm_beta")]
285 #[test]
286 fn test_orm_delete_generation() {
287 use crate::orm::NeonTable;
288 let apa = TestTable::select_as_json_sql();
289 println!("{apa}");
290 }
291}