Skip to content

Commit 5e82d1a

Browse files
Merge pull request #39 from michelleN/postgres
spin 3 postgres support
2 parents 97526e8 + f324f57 commit 5e82d1a

File tree

17 files changed

+1134
-0
lines changed

17 files changed

+1134
-0
lines changed

Cargo.lock

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ name = "spin_sdk"
1919
[dependencies]
2020
anyhow = "1"
2121
async-trait = "0.1.74"
22+
chrono = "0.4.38"
2223
form_urlencoded = "1.0"
2324
spin-executor = { version = "3.0.1", path = "crates/executor" }
2425
spin-macro = { version = "3.0.1", path = "crates/macro" }
@@ -52,6 +53,7 @@ members = [
5253
"examples/key-value",
5354
"examples/mysql",
5455
"examples/postgres",
56+
"examples/postgres-v3",
5557
"examples/redis-outbound",
5658
"examples/mqtt-outbound",
5759
"examples/variables",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasi"

examples/postgres-v3/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "rust-outbound-pg-v3"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies]
10+
# Useful crate to handle errors.
11+
anyhow = "1"
12+
# General-purpose crate with common HTTP types.
13+
http = "1.0.0"
14+
# The Spin SDK.
15+
spin-sdk = { path = "../.." }
16+
# For handling date/time types
17+
chrono = "0.4.38"

examples/postgres-v3/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Spin Outbound PostgreSQL example
2+
3+
This example shows how to access a PostgreSQL database from Spin component.
4+
5+
## Spin up
6+
7+
From example root:
8+
9+
```
10+
createdb spin_dev
11+
psql -d spin_dev -f db/testdata.sql
12+
RUST_LOG=spin=trace spin build --up
13+
```
14+
15+
Curl the read route:
16+
17+
```
18+
$ curl -i localhost:3000/read
19+
HTTP/1.1 200 OK
20+
transfer-encoding: chunked
21+
date: Wed, 06 Nov 2024 20:17:03 GMT
22+
23+
Found 2 article(s) as follows:
24+
article: Article {
25+
id: 1,
26+
title: "My Life as a Goat",
27+
content: "I went to Nepal to live as a goat, and it was much better than being a butler.",
28+
authorname: "E. Blackadder",
29+
published: Date(
30+
2024-11-05,
31+
),
32+
coauthor: None,
33+
}
34+
article: Article {
35+
id: 2,
36+
title: "Magnificent Octopus",
37+
content: "Once upon a time there was a lovely little sausage.",
38+
authorname: "S. Baldrick",
39+
published: Date(
40+
2024-11-06,
41+
),
42+
coauthor: None,
43+
}
44+
45+
(Column info: id:DbDataType::Int32, title:DbDataType::Str, content:DbDataType::Str, authorname:DbDataType::Str, published:DbDataType::Date, coauthor:DbDataType::Str)
46+
```
47+
48+
Curl the write route:
49+
50+
```
51+
$ curl -i localhost:3000/write
52+
HTTP/1.1 200 OK
53+
content-length: 9
54+
date: Sun, 25 Sep 2022 15:46:22 GMT
55+
56+
Count: 3
57+
```
58+
59+
Curl the write_datetime_info route to experiment with date time types:
60+
```
61+
$ curl -i localhost:3000/write_datetime_info
62+
HTTP/1.1 200 OK
63+
content-length: 9
64+
date: Sun, 25 Sep 2022 15:46:22 GMT
65+
66+
Count: 4
67+
```
68+
69+
Read endpoint should now also show a row with publisheddate, publishedtime, publisheddatetime and readtime values.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
CREATE TABLE articletest (
2+
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3+
title varchar(40) NOT NULL,
4+
content text NOT NULL,
5+
authorname varchar(40) NOT NULL ,
6+
publisheddate date NOT NULL,
7+
publishedtime time,
8+
publisheddatetime timestamp,
9+
readtime bigint,
10+
coauthor text
11+
);
12+
13+
INSERT INTO articletest (title, content, authorname, publisheddate) VALUES
14+
(
15+
'My Life as a Goat',
16+
'I went to Nepal to live as a goat, and it was much better than being a butler.',
17+
'E. Blackadder',
18+
'2024-11-05'
19+
),
20+
(
21+
'Magnificent Octopus',
22+
'Once upon a time there was a lovely little sausage.',
23+
'S. Baldrick',
24+
'2024-11-06'
25+
);

examples/postgres-v3/spin.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
spin_manifest_version = 2
2+
3+
[application]
4+
authors = ["Fermyon Engineering <[email protected]>"]
5+
name = "rust-outbound-pg-v3-example"
6+
version = "0.1.0"
7+
8+
[[trigger.http]]
9+
route = "/..."
10+
component = "outbound-pg"
11+
12+
[component.outbound-pg]
13+
environment = { DB_URL = "host=localhost user=postgres dbname=spin_dev" }
14+
source = "../../target/wasm32-wasi/release/rust_outbound_pg_v3.wasm"
15+
allowed_outbound_hosts = ["postgres://localhost"]
16+
[component.outbound-pg.build]
17+
command = "cargo build --target wasm32-wasi --release"

examples/postgres-v3/src/lib.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#![allow(dead_code)]
2+
use anyhow::Result;
3+
use http::{Request, Response};
4+
use spin_sdk::{http_component, pg3, pg3::Decode};
5+
6+
// The environment variable set in `spin.toml` that points to the
7+
// address of the Pg server that the component will write to
8+
const DB_URL_ENV: &str = "DB_URL";
9+
10+
#[derive(Debug, Clone)]
11+
struct Article {
12+
id: i32,
13+
title: String,
14+
content: String,
15+
authorname: String,
16+
published_date: chrono::NaiveDate,
17+
published_time: Option<chrono::NaiveTime>,
18+
published_datetime: Option<chrono::NaiveDateTime>,
19+
read_time: Option<i64>,
20+
coauthor: Option<String>,
21+
}
22+
23+
impl TryFrom<&pg3::Row> for Article {
24+
type Error = anyhow::Error;
25+
26+
fn try_from(row: &pg3::Row) -> Result<Self, Self::Error> {
27+
let id = i32::decode(&row[0])?;
28+
let title = String::decode(&row[1])?;
29+
let content = String::decode(&row[2])?;
30+
let authorname = String::decode(&row[3])?;
31+
let published_date = chrono::NaiveDate::decode(&row[4])?;
32+
let published_time = Option::<chrono::NaiveTime>::decode(&row[5])?;
33+
let published_datetime = Option::<chrono::NaiveDateTime>::decode(&row[6])?;
34+
let read_time = Option::<i64>::decode(&row[7])?;
35+
let coauthor = Option::<String>::decode(&row[8])?;
36+
37+
Ok(Self {
38+
id,
39+
title,
40+
content,
41+
authorname,
42+
published_date,
43+
published_time,
44+
published_datetime,
45+
read_time,
46+
coauthor,
47+
})
48+
}
49+
}
50+
51+
#[http_component]
52+
fn process(req: Request<()>) -> Result<Response<String>> {
53+
match req.uri().path() {
54+
"/read" => read(req),
55+
"/write" => write(req),
56+
"/write_datetime_info" => write_datetime_info(req),
57+
"/pg_backend_pid" => pg_backend_pid(req),
58+
_ => Ok(http::Response::builder()
59+
.status(404)
60+
.body("Not found".into())?),
61+
}
62+
}
63+
64+
fn read(_req: Request<()>) -> Result<Response<String>> {
65+
let address = std::env::var(DB_URL_ENV)?;
66+
let conn = pg3::Connection::open(&address)?;
67+
68+
let sql = "SELECT id, title, content, authorname, publisheddate, publishedtime, publisheddatetime, readtime, coauthor FROM articletest";
69+
let rowset = conn.query(sql, &[])?;
70+
71+
let column_summary = rowset
72+
.columns
73+
.iter()
74+
.map(format_col)
75+
.collect::<Vec<_>>()
76+
.join(", ");
77+
78+
let mut response_lines = vec![];
79+
80+
for row in rowset.rows {
81+
let article = Article::try_from(&row)?;
82+
83+
println!("article: {:#?}", article);
84+
response_lines.push(format!("article: {:#?}", article));
85+
}
86+
87+
// use it in business logic
88+
89+
let response = format!(
90+
"Found {} article(s) as follows:\n{}\n\n(Column info: {})\n",
91+
response_lines.len(),
92+
response_lines.join("\n"),
93+
column_summary,
94+
);
95+
96+
Ok(http::Response::builder().status(200).body(response)?)
97+
}
98+
99+
fn write_datetime_info(_req: Request<()>) -> Result<Response<String>> {
100+
let address = std::env::var(DB_URL_ENV)?;
101+
let conn = pg3::Connection::open(&address)?;
102+
103+
let date: chrono::NaiveDate = chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
104+
let time: chrono::NaiveTime = chrono::NaiveTime::from_hms_nano_opt(12, 34, 56, 1).unwrap();
105+
let datetime: chrono::NaiveDateTime = chrono::NaiveDateTime::new(date, time);
106+
let readtime = 123i64;
107+
108+
let nrow_executed = conn.execute(
109+
"INSERT INTO articletest(title, content, authorname, publisheddate, publishedtime, publisheddatetime, readtime) VALUES ($1, $2, $3, $4, $5, $6, $7)",
110+
&[ "aaa".to_string().into(), "bbb".to_string().into(), "ccc".to_string().into(), date.into(), time.into(), datetime.into(), readtime.into() ],
111+
);
112+
113+
println!("nrow_executed: {:?}", nrow_executed);
114+
115+
let sql = "SELECT COUNT(id) FROM articletest";
116+
let rowset = conn.query(sql, &[])?;
117+
let row = &rowset.rows[0];
118+
let count = i64::decode(&row[0])?;
119+
let response = format!("Count: {}\n", count);
120+
121+
Ok(http::Response::builder().status(200).body(response)?)
122+
}
123+
124+
fn write(_req: Request<()>) -> Result<Response<String>> {
125+
let address = std::env::var(DB_URL_ENV)?;
126+
let conn = pg3::Connection::open(&address)?;
127+
128+
let sql =
129+
"INSERT INTO articletest (title, content, authorname, published) VALUES ('aaa', 'bbb', 'ccc', '2024-01-01')";
130+
let nrow_executed = conn.execute(sql, &[])?;
131+
132+
println!("nrow_executed: {}", nrow_executed);
133+
134+
let sql = "SELECT COUNT(id) FROM articletest";
135+
let rowset = conn.query(sql, &[])?;
136+
let row = &rowset.rows[0];
137+
let count = i64::decode(&row[0])?;
138+
let response = format!("Count: {}\n", count);
139+
140+
Ok(http::Response::builder().status(200).body(response)?)
141+
}
142+
143+
fn pg_backend_pid(_req: Request<()>) -> Result<Response<String>> {
144+
let address = std::env::var(DB_URL_ENV)?;
145+
let conn = pg3::Connection::open(&address)?;
146+
let sql = "SELECT pg_backend_pid()";
147+
148+
let get_pid = || {
149+
let rowset = conn.query(sql, &[])?;
150+
let row = &rowset.rows[0];
151+
152+
i32::decode(&row[0])
153+
};
154+
155+
assert_eq!(get_pid()?, get_pid()?);
156+
157+
let response = format!("pg_backend_pid: {}\n", get_pid()?);
158+
159+
Ok(http::Response::builder().status(200).body(response)?)
160+
}
161+
162+
fn format_col(column: &pg3::Column) -> String {
163+
format!("{}:{:?}", column.name, column.data_type)
164+
}

0 commit comments

Comments
 (0)