1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! PostgreSQL database adapter for time-partitioned reads and streaming writes.
//!
//! Provides three graph nodes:
//! - [`postgres_read`] — producer that replays a historical table in contiguous,
//! caller-defined time slices (one query per slice), driven by the run's
//! `RunMode::HistoricalFrom` / `RunFor::Duration` window. Shares its slicing
//! logic with the KDB+ adapter (`crate::adapters::common`).
//! - [`postgres_sub`] — real-time producer that live-tails a table via
//! `LISTEN`/`NOTIFY` (notification as wake-up, rows re-queried past a cursor).
//! - [`postgres_write`] — consumer that inserts each on-graph record, prepending
//! the graph timestamp as the first column.
//!
//! Time is carried **on-graph** in tuples `(NanoTime, T)`, never inside the record
//! struct: on read it is extracted from a timestamp column into the tuple; on write
//! it is prepended to the row. Your struct should hold only business data.
//!
//! # Setup
//!
//! ```sh
//! docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16-alpine
//! ```
//!
//! # Reading (time-sliced)
//!
//! [`postgres_read`] calls `query_fn` once per slice with the half-open interval
//! `[t0, t1)`, the KDB-style date integer, and the slice iteration index. Filter on
//! `time >= t0 AND time < t1` and `ORDER BY time` so rows arrive time-ordered.
//!
//! ```ignore
//! use wingfoil::adapters::postgres::*;
//! use wingfoil::*;
//!
//! #[derive(Debug, Clone, Default)]
//! struct Trade { sym: String, price: f64, qty: i64 }
//!
//! impl PostgresDeserialize for Trade {
//! fn from_row(row: &Row) -> anyhow::Result<(NanoTime, Self)> {
//! Ok((
//! row.get_nanotime(0)?, // col 0: time
//! Trade { sym: row.try_get(1)?, price: row.try_get(2)?, qty: row.try_get(3)? },
//! ))
//! }
//! }
//!
//! let conn = PostgresConnection::new("host=localhost user=postgres password=postgres dbname=postgres");
//! postgres_read::<Trade>(conn, std::time::Duration::from_secs(3600), |(t0, t1), _date, _| {
//! format!(
//! "SELECT time, sym, price, qty FROM trades \
//! WHERE time >= '{}' AND time < '{}' ORDER BY time",
//! postgres_timestamp(t0), postgres_timestamp(t1),
//! )
//! })
//! .collapse()
//! .print()
//! .run(
//! RunMode::HistoricalFrom(NanoTime::from_kdb_timestamp(0)),
//! RunFor::Duration(std::time::Duration::from_secs(86400)),
//! )
//! .unwrap();
//! ```
//!
//! # Subscribing (real-time live tail)
//!
//! [`postgres_sub`] streams rows as they are inserted, using `LISTEN`/`NOTIFY` as a
//! wake-up signal and re-querying past a time cursor (so nothing is lost to NOTIFY's
//! payload limits). Install the trigger with [`postgres_notify_trigger_sql`], then:
//!
//! ```ignore
//! use wingfoil::adapters::postgres::*;
//! use wingfoil::*;
//!
//! let conn = PostgresConnection::new("host=localhost user=postgres password=postgres dbname=postgres");
//!
//! // One-time setup (psql or any client): install the notify trigger.
//! // postgres_notify_trigger_sql("trades", "trades_feed")
//!
//! postgres_sub::<Trade, _>(conn, "trades_feed", NanoTime::now(), |cursor| {
//! format!(
//! "SELECT time, sym, price, qty FROM trades \
//! WHERE time > '{}' ORDER BY time",
//! postgres_timestamp(cursor),
//! )
//! })
//! .print() // prints each burst; several rows can arrive per real-time cycle
//! .run(RunMode::RealTime, RunFor::Forever)
//! .unwrap();
//! ```
//!
//! Note: `collapse()` keeps only the **last** element of each burst — fine for
//! historical replay (one row per tick when timestamps are distinct) but lossy on
//! a real-time tail, where a burst carries every row drained in that cycle.
//!
//! # Writing
//!
//! [`postgres_write`] (or the fluent `.postgres_write()` method) inserts each record,
//! prepending the graph timestamp as the first column. The target table's columns must
//! be `(time, <business columns in to_params() order>)`.
//!
//! ```ignore
//! use wingfoil::adapters::postgres::*;
//! use wingfoil::*;
//!
//! #[derive(Debug, Clone, Default)]
//! struct Trade { sym: String, price: f64, qty: i64 }
//!
//! impl PostgresSerialize for Trade {
//! fn to_params(&self) -> Vec<Box<dyn ToSql + Sync + Send>> {
//! vec![Box::new(self.sym.clone()), Box::new(self.price), Box::new(self.qty)]
//! }
//! }
//!
//! let conn = PostgresConnection::new("host=localhost user=postgres password=postgres dbname=postgres");
//! constant(burst![Trade { sym: "AAPL".into(), price: 1.0, qty: 1 }])
//! .postgres_write(conn, "trades")
//! .run(RunMode::HistoricalFrom(NanoTime::from_kdb_timestamp(0)), RunFor::Cycles(1))
//! .unwrap();
//! ```
pub use *;
pub use *;
pub use *;
/// Re-export of [`tokio_postgres::Row`] so callers can implement
/// [`PostgresDeserialize`] without depending on `tokio-postgres` directly.
pub use Row;
/// Re-export of [`tokio_postgres::types::ToSql`] for [`PostgresSerialize`] impls.
pub use ToSql;
/// Re-export of [`tokio_postgres::types::Type`] for dispatching on column SQL types
/// in [`PostgresDeserialize`] impls.
pub use Type;
/// Quote a PostgreSQL identifier: wrap in double quotes, doubling any embedded quotes.
///
/// Makes mixed-case, reserved-word, and special-character identifiers safe to splice
/// into SQL (`quote_ident("eventTime")` → `"eventTime"`). Note that quoting disables
/// PostgreSQL's lower-case folding, so the identifier must match the column/table name
/// exactly as stored in the catalog.
/// Quote a possibly schema-qualified table name, quoting each dot-separated segment.
///
/// `quote_table("public.My Trades")` → `"public"."My Trades"`.
/// PostgreSQL connection configuration.
///
/// Wraps a libpq-style connection string (see the [tokio-postgres config docs]).
///
/// [tokio-postgres config docs]: https://docs.rs/tokio-postgres/latest/tokio_postgres/config/struct.Config.html