Skip to content

Commit a5ae84a

Browse files
committed
Add the README
1 parent bca6cae commit a5ae84a

4 files changed

Lines changed: 252 additions & 12 deletions

File tree

PgBulkInsert/src/main/java/de/bytefish/pgbulkinsert/PgBulkInsert.java

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.io.IOException;
1111
import java.math.BigDecimal;
1212
import java.math.BigInteger;
13+
import java.net.Inet4Address;
14+
import java.net.InetAddress;
1315
import java.nio.charset.StandardCharsets;
1416
import java.sql.Connection;
1517
import java.sql.SQLException;
@@ -31,6 +33,16 @@ public interface ToShortFunction<T> {
3133
short applyAsShort(T value);
3234
}
3335

36+
// Geometric Types
37+
public record PgPoint(double x, double y) {}
38+
public record PgLine(double a, double b, double c) {}
39+
public record PgLseg(PgPoint p1, PgPoint p2) {}
40+
public record PgBox(PgPoint p1, PgPoint p2) {}
41+
public record PgPath(boolean closed, List<PgPoint> points) {}
42+
public record PgPolygon(List<PgPoint> points) {}
43+
public record PgCircle(PgPoint center, double radius) {}
44+
45+
3446
public record PgRange<T>(T lower, T upper, boolean lowerInclusive, boolean upperInclusive, boolean lowerInfinite,
3547
boolean upperInfinite, boolean empty) {
3648
public static <T> PgRange<T> emptyRange() {
@@ -114,8 +126,23 @@ public interface BinaryRowWriter {
114126

115127
void writeHstore(Map<String, String> v) throws IOException;
116128

129+
// Network
130+
void writeInet(InetAddress v) throws IOException;
131+
void writeMacAddress(String v) throws IOException;
132+
133+
// Geometry
134+
void writePoint(PgPoint v) throws IOException;
135+
void writeLine(PgLine v) throws IOException;
136+
void writeLseg(PgLseg v) throws IOException;
137+
void writeBox(PgBox v) throws IOException;
138+
void writePath(PgPath v) throws IOException;
139+
void writePolygon(PgPolygon v) throws IOException;
140+
void writeCircle(PgCircle v) throws IOException;
141+
142+
// Arrays
117143
<E> void writeArray(Collection<?> elements, PgType<E> baseElementType) throws IOException;
118144

145+
// Ranges
119146
<E> void writeRange(PgRange<E> range, PgType<E> elementType) throws IOException;
120147
}
121148

@@ -297,15 +324,54 @@ public void writeHstore(Map<String, String> v) throws IOException {
297324
}
298325
}
299326

327+
@Override public void writeInet(InetAddress v) throws IOException {
328+
byte[] addr = v.getAddress();
329+
out.writeInt(addr.length + 4);
330+
out.writeByte(v instanceof Inet4Address ? 2 : 3); // PGSQL_AF_INET or INET6
331+
out.writeByte(addr.length * 8); // mask
332+
out.writeByte(0); // is_res
333+
out.writeByte(addr.length);
334+
out.write(addr);
335+
}
336+
337+
@Override public void writeMacAddress(String v) throws IOException {
338+
String[] hex = v.split("[:-]");
339+
out.writeInt(6);
340+
for (String s : hex) out.writeByte(Integer.parseInt(s, 16));
341+
}
342+
343+
@Override public void writePoint(PgPoint v) throws IOException { out.writeInt(16); out.writeDouble(v.x()); out.writeDouble(v.y()); }
344+
345+
@Override public void writeLine(PgLine v) throws IOException { out.writeInt(24); out.writeDouble(v.a()); out.writeDouble(v.b()); out.writeDouble(v.c()); }
346+
347+
@Override public void writeLseg(PgLseg v) throws IOException { out.writeInt(32); writePointInternal(v.p1()); writePointInternal(v.p2()); }
348+
349+
@Override public void writeBox(PgBox v) throws IOException { out.writeInt(32); writePointInternal(v.p1()); writePointInternal(v.p2()); }
350+
351+
@Override public void writePath(PgPath v) throws IOException {
352+
out.writeInt(1 + 4 + (v.points().size() * 16));
353+
out.writeByte(v.closed() ? 1 : 0);
354+
out.writeInt(v.points().size());
355+
for (PgPoint p : v.points()) writePointInternal(p);
356+
}
357+
358+
@Override public void writePolygon(PgPolygon v) throws IOException {
359+
out.writeInt(4 + (v.points().size() * 16));
360+
out.writeInt(v.points().size());
361+
for (PgPoint p : v.points()) writePointInternal(p);
362+
}
363+
364+
@Override public void writeCircle(PgCircle v) throws IOException { out.writeInt(24); writePointInternal(v.center()); out.writeDouble(v.radius()); }
365+
366+
private void writePointInternal(PgPoint p) throws IOException { out.writeDouble(p.x()); out.writeDouble(p.y()); }
300367

301368
@Override public <E> void writeArray(Collection<?> elements, PgType<E> baseElementType) throws IOException {
302369
if (elements == null) { writeNull(); return; }
303370

304371
// Determine dimensions
305372
List<Integer> dims = new ArrayList<>();
306373
Object current = elements;
307-
while (current instanceof Collection) {
308-
Collection<?> c = (Collection<?>) current;
374+
while (current instanceof Collection<?> c) {
309375
dims.add(c.size());
310376
if (c.isEmpty()) break;
311377
current = c.iterator().next();
@@ -685,6 +751,24 @@ public void write(BinaryRowWriter w, Instant v) throws IOException {
685751
}
686752
};
687753

754+
// Geometry
755+
public static final PgType<PgPoint> POINT = createType(600, BinaryRowWriter::writePoint);
756+
public static final PgType<PgLine> LINE = createType(628, BinaryRowWriter::writeLine);
757+
public static final PgType<PgLseg> LSEG = createType(601, BinaryRowWriter::writeLseg);
758+
public static final PgType<PgBox> BOX = createType(603, BinaryRowWriter::writeBox);
759+
public static final PgType<PgPath> PATH = createType(602, BinaryRowWriter::writePath);
760+
public static final PgType<PgPolygon> POLYGON = createType(604, BinaryRowWriter::writePolygon);
761+
public static final PgType<PgCircle> CIRCLE = createType(718, BinaryRowWriter::writeCircle);
762+
763+
// Ranges
764+
public static final PgType<PgRange<Integer>> INT4RANGE = createType(3904, (w, v) -> w.writeRange(v, INT4));
765+
public static final PgType<PgRange<Long>> INT8RANGE = createType(3926, (w, v) -> w.writeRange(v, INT8));
766+
public static final PgType<PgRange<BigDecimal>> NUMRANGE = createType(3906, (w, v) -> w.writeRange(v, NUMERIC));
767+
public static final PgType<PgRange<LocalDateTime>> TSRANGE = createType(3908, (w, v) -> w.writeRange(v, TIMESTAMP));
768+
public static final PgType<PgRange<Instant>> TSTZRANGE = createType(3910, (w, v) -> w.writeRange(v, TIMESTAMPTZ));
769+
public static final PgType<PgRange<LocalDate>> DATERANGE = createType(3912, (w, v) -> w.writeRange(v, DATE));
770+
771+
// Arrays
688772
public static <E> PgType<Collection<E>> array(PgType<E> baseType) {
689773
return createType(0, (w, v) -> w.writeArray(v, baseType));
690774
}

PgBulkInsert/src/test/java/de/bytefish/pgbulkinsert/test/IntegrationTest.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import org.junit.jupiter.api.AfterAll;
55
import org.junit.jupiter.api.BeforeAll;
66
import org.junit.jupiter.api.Test;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
710
import java.math.BigDecimal;
811
import java.sql.Connection;
912
import java.sql.DriverManager;
@@ -18,22 +21,23 @@
1821
import java.util.Arrays;
1922
import java.util.List;
2023
import java.math.BigInteger;
24+
import java.util.Properties;
2125

2226
import static org.junit.jupiter.api.Assertions.assertEquals;
2327
import static org.junit.jupiter.api.Assertions.assertTrue;
2428

2529
public class IntegrationTest {
2630

27-
// Passe diese Verbindungsdaten an deinen laufenden Docker-Container an
28-
private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/postgres";
29-
private static final String DB_USER = "postgres";
30-
private static final String DB_PASSWORD = "password";
31-
3231
private static Connection connection;
3332

3433
@BeforeAll
3534
public static void setupDatabase() throws Exception {
36-
connection = DriverManager.getConnection(JDBC_URL, DB_USER, DB_PASSWORD);
35+
Properties properties = getProperties("db.properties");
36+
37+
connection = DriverManager.getConnection(
38+
properties.getProperty("db.url"),
39+
properties.getProperty("db.user"),
40+
properties.getProperty("db.password"));
3741

3842
// Create Test Table handle all the various data types
3943
try (Statement stmt = connection.createStatement()) {
@@ -137,7 +141,7 @@ public void testBulkInsertSavesDataCorrectly() throws Exception {
137141
// Verify first element
138142
assertTrue(rs.next());
139143
assertEquals(1L, rs.getLong("id"));
140-
assertEquals("Normaler Text", rs.getString("text_val"));
144+
assertEquals("Normal Text", rs.getString("text_val"));
141145
assertEquals(new BigDecimal("42.1234"), rs.getBigDecimal("numeric_val"));
142146
assertEquals(new BigInteger("98765432101234567890987654321"), rs.getBigDecimal("numeric_int_val").toBigInteger());
143147
assertTrue(rs.getBoolean("is_active"));
@@ -160,7 +164,7 @@ public void testBulkInsertSavesDataCorrectly() throws Exception {
160164
// Verify Null-Byte Handling
161165
assertTrue(rs.next());
162166
assertEquals(2L, rs.getLong("id"));
163-
assertEquals("Fieser Text", rs.getString("text_val")); // \u0000 wurde restlos entfernt!
167+
assertEquals("Bad Text", rs.getString("text_val"));
164168
assertEquals(new BigDecimal("-99.99"), rs.getBigDecimal("numeric_val"));
165169
assertEquals(new BigInteger("-12345678909876543210123456789"), rs.getBigDecimal("numeric_int_val").toBigInteger());
166170

@@ -183,4 +187,20 @@ public void testBulkInsertSavesDataCorrectly() throws Exception {
183187
assertTrue(!rs.next());
184188
}
185189
}
190+
191+
private static Properties getProperties(String filename) {
192+
193+
Properties props = new Properties();
194+
195+
InputStream is = ClassLoader.getSystemResourceAsStream(filename);
196+
197+
try {
198+
props.load(is);
199+
}
200+
catch (IOException e) {
201+
throw new RuntimeException("Could not load unittest.properties", e);
202+
}
203+
204+
return props;
205+
}
186206
}

README.md

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,146 @@ You can add the following dependencies to your pom.xml to include [PgBulkInsert]
2525
<dependency>
2626
<groupId>de.bytefish</groupId>
2727
<artifactId>pgbulkinsert</artifactId>
28-
<version>8.1.8</version>
28+
<version>9.0.0</version>
2929
</dependency>
3030
```
3131

32+
## PgBulkInsert 9: Modern, Functional API ##
33+
34+
PgBulkInsert 9.0.0 comes with a new API, that focuses on a functional API surface leveraging modern Java
35+
features (Lambdas, Method References, and Records) and reduces memory allocation-overhead.
36+
37+
## Quick Start ##
38+
39+
The new API separates the What (Structure and Mapping) from the How (Execution and Configuration).
40+
41+
### 1. Define your Data Model ###
42+
43+
The library works with Java Records, POJOs, or any other data carrier.
44+
45+
```java
46+
public record UserSession(
47+
UUID id,
48+
long visits, // Primitive
49+
String userAgent, // Nullable String
50+
Instant createdAt, // Precise Timestamp
51+
double[] latLon, // Array
52+
PgPoint location // Geometric Point
53+
) {}
54+
```
55+
56+
### 2. Define your Mapping ###
57+
58+
The `PgMapper` is the heart of the library. It is stateless and thread-safe.
59+
60+
```java
61+
PgMapper<UserSession> mapper = PgMapper.forClass(UserSession.class)
62+
.map("id", PostgresTypes.UUID.from(UserSession::id))
63+
64+
// ZERO-ALLOCATION: Direct access to primitives via ToLongFunction
65+
.map("visits", PostgresTypes.INT8.primitive(UserSession::visits))
66+
67+
// SAFE STRINGS: Strips invalid \u0000 characters to prevent COPY errors
68+
.map("user_agent", PostgresTypes.TEXT.removeNullCharacters().from(UserSession::userAgent))
69+
70+
// TYPE-SAFE TIME: Validated at compile-time to prevent timezone issues
71+
.map("created_at", PostgresTypes.TIMESTAMPTZ.instant(UserSession::createdAt))
72+
73+
// GEOMETRY: Native support for Postgres geometric types
74+
.map("location", PostgresTypes.POINT.from(UserSession::location));
75+
```
76+
77+
### 3. Configure and Execute ###
78+
79+
The `PgBulkWriter` handles execution details like buffer sizes and stream management.
80+
81+
```java
82+
PgBulkWriter<UserSession> writer = new PgBulkWriter<>(mapper)
83+
.withBufferSize(256 * 1024); // 256 KB Buffer
84+
85+
try (Connection conn = dataSource.getConnection()) {
86+
writer.saveAll(conn, "public.user_sessions", sessionList);
87+
}
88+
```
89+
90+
## Streaming and Lazy Evaluation ##
91+
92+
One of the key strengths of the `saveAll` method is that it accepts an `Iterable<T>`. This means you are never
93+
forced to load your entire dataset into memory.
94+
95+
If you are working with a Java Stream (e.g., from a file, a reactive source, or another database), you
96+
can pass it directly using a method reference:
97+
98+
```java
99+
Stream<UserSession> massiveStream = getMassiveStreamFromSource();
100+
101+
try (Connection conn = dataSource.getConnection()) {
102+
// Uses the stream's iterator to pull data lazily
103+
writer.saveAll(conn, "public.user_sessions", massiveStream::iterator);
104+
}
105+
```
106+
107+
This approach ensures that records are transformed and written to the PostgreSQL wire format on-the-fly,
108+
keeping your application's memory usage constant regardless of the total number of rows.
109+
110+
## Mastering the Fluent API ##
111+
112+
The API is designed around a so called `PostgresType`. This class serves as your single
113+
entry point for all PostgreSQL data types.
114+
115+
### The Power of PostgresTypes ###
116+
117+
Instead of a generic `map()` method that tries to guess your intent, the API uses a "Type-First"
118+
approach. When you type `PostgresTypes.INT4.`, your IDE will offer specific choices:
119+
120+
* `.primitive(ToIntFunction<T>)`: High-performance path. No objects created on the heap.
121+
* `.boxed(Function<T, Integer>)`: Use this for nullable database columns or if your POJO uses Integer.
122+
* `.from(Function<T, Integer>)`: Standard object mapping.
123+
124+
### Eliminating Timezone Confusion ###
125+
126+
One of the most common bugs is the confusion between `timestamp` and `timestamptz`.
127+
128+
The API solves this:
129+
130+
* `PostgresTypes.TIMESTAMP` only allows `.localDateTime()`.
131+
* `PostgresTypes.TIMESTAMPTZ` allows `.instant()`, `.zonedDateTime()`, or `.offsetDateTime()`.
132+
133+
## Advanced Type Mapping ##
134+
135+
### N-Dimensional Arrays (Matrices & Tensors) ###
136+
137+
```java
138+
// Mapping a 2D Matrix (Collection of Collections)
139+
.map("data_matrix", PostgresTypes.array2D(PostgresTypes.INT4).from(MyEntity::getMatrix))
140+
141+
// Mapping a 3D Tensor
142+
.map("data_tensor", PostgresTypes.array3D(PostgresTypes.FLOAT8).from(MyEntity::getTensor))
143+
```
144+
145+
### Ranges ###
146+
147+
```java
148+
// Mapping an integer range
149+
.map("age_limit", PostgresTypes.INT4RANGE.from(MyEntity::getAgeRange))
150+
151+
// Mapping a timestamp range
152+
.map("validity", PostgresTypes.TSRANGE.from(MyEntity::getValidityPeriod))
153+
```
154+
155+
### Geometric Types ###
156+
157+
Native support for all PostgreSQL geometric types using dedicated helper records:
158+
159+
* `POINT`: `PostgresTypes.POINT`
160+
* `CIRCLE`: `PostgresTypes.CIRCLE`
161+
* `POLYGON`: `PostgresTypes.POLYGON`
162+
* `PATH` / `LSEG` / `BOX` / `LINE`
163+
164+
```java
165+
.map("area", PostgresTypes.POLYGON.from(MyEntity::getBoundaries))
166+
```
167+
32168
## Supported PostgreSQL Types ##
33169

34170
* [Numeric Types](http://www.postgresql.org/docs/current/static/datatype-numeric.html)

docker/docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: '3.8'
22

33
services:
44
postgres:
5-
image: postgres:16
5+
image: postgres:18
66
container_name: postgres
77
ports:
88
- "5431:5432"

0 commit comments

Comments
 (0)