postgresql-jdbc.git / summary / log / commit / refs
commit bf00387461bafdf0f19a8557bff554e469207cce
Author: Jorge Solórzano <jorsol@gmail.com>
Commit: GitHub <noreply@github.com>
Date: Wed Sep 29 02:02:42 2021 +0000
fix: handle nulls when the following clasess are used: PGbox, PGcircle, PGline, PGlseg, PGpath, PGpoint, PGpolygon, and PGmoney (#2271)
1) PGobject.setValue(null) can be used to set the wrapped value to null
Note: previously the sub-classes of PGobject threw NPE for setValue(null)
2) Return null from `getValue` in case the typed null is received from the database
3) Assume PGBinaryObject#lengthInBytes == 0 means the value should be set as null when sending data to the database
see #1870
Co-authored-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
CHANGELOG.md | 1 +
.../main/java/org/postgresql/geometric/PGbox.java | 59 +++++++---
.../java/org/postgresql/geometric/PGcircle.java | 29 +++--
.../main/java/org/postgresql/geometric/PGline.java | 70 ++++++++---
.../main/java/org/postgresql/geometric/PGlseg.java | 36 ++++--
.../main/java/org/postgresql/geometric/PGpath.java | 45 ++++---
.../java/org/postgresql/geometric/PGpoint.java | 31 ++++-
.../java/org/postgresql/geometric/PGpolygon.java | 36 +++---
.../java/org/postgresql/jdbc/PgConnection.java | 4 +-
.../org/postgresql/jdbc/PgPreparedStatement.java | 7 +-
.../main/java/org/postgresql/util/PGInterval.java | 40 ++++++-
.../src/main/java/org/postgresql/util/PGmoney.java | 24 +++-
.../main/java/org/postgresql/util/PGobject.java | 14 ++-
.../org/postgresql/test/jdbc2/PGObjectGetTest.java | 130 +++++++++++++++++++++
.../org/postgresql/test/jdbc2/PGObjectSetTest.java | 127 ++++++++++++++++++++
15 files changed, 562 insertions(+), 91 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d69d4aa..0172589c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -75,6 +75,7 @@ is executed a second time it will fail.
- The driver returns enum and jsonb arrays elements as String objects (like in 42.2.14 and earlier versions) [PR 1879](https://github.com/pgjdbc/pgjdbc/pull/1879).
- PgTokenizer was ignoring last empty token [PR #1882](https://github.com/pgjdbc/pgjdbc/pull/1882)
- Remove osgi from karaf fixes Issue #1891 [PR #1902](https://github.com/pgjdbc/pgjdbc/pull/1902)
+- Handle nulls when the following clasess are used: PGbox, PGcircle, PGline, PGlseg, PGpath, PGpoint, PGpolygon, and PGmoney.
## [42.2.16] (2020-08-20)
### Known issues
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java
index 2476f40a..db74e048 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGbox.java
@@ -5,6 +5,8 @@
package org.postgresql.geometric;
+import static org.postgresql.util.internal.Nullness.castNonNull;
+
import org.postgresql.util.GT;
import org.postgresql.util.PGBinaryObject;
import org.postgresql.util.PGobject;
@@ -24,7 +26,7 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
/**
* These are the two points.
*/
- public PGpoint[] point = new PGpoint[2];
+ public PGpoint @Nullable [] point;
/**
* @param x1 first x coordinate
@@ -33,9 +35,7 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
* @param y2 second y coordinate
*/
public PGbox(double x1, double y1, double x2, double y2) {
- this();
- this.point[0] = new PGpoint(x1, y1);
- this.point[1] = new PGpoint(x2, y2);
+ this(new PGpoint(x1, y1), new PGpoint(x2, y2));
}
/**
@@ -44,8 +44,7 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
*/
public PGbox(PGpoint p1, PGpoint p2) {
this();
- this.point[0] = p1;
- this.point[1] = p2;
+ this.point = new PGpoint[]{p1, p2};
}
/**
@@ -73,7 +72,11 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
* @throws SQLException thrown if value is invalid for this type
*/
@Override
- public void setValue(String value) throws SQLException {
+ public void setValue(@Nullable String value) throws SQLException {
+ if (value == null) {
+ this.point = null;
+ return;
+ }
PGtokenizer t = new PGtokenizer(value, ',');
if (t.getSize() != 2) {
throw new PSQLException(
@@ -81,6 +84,10 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
PSQLState.DATA_TYPE_MISMATCH);
}
+ PGpoint[] point = this.point;
+ if (point == null) {
+ this.point = point = new PGpoint[2];
+ }
point[0] = new PGpoint(t.getToken(0));
point[1] = new PGpoint(t.getToken(1));
}
@@ -89,10 +96,15 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
* @param b Definition of this point in PostgreSQL's binary syntax
*/
public void setByteValue(byte[] b, int offset) {
+ PGpoint[] point = this.point;
+ if (point == null) {
+ this.point = point = new PGpoint[2];
+ }
point[0] = new PGpoint();
point[0].setByteValue(b, offset);
point[1] = new PGpoint();
point[1].setByteValue(b, offset + point[0].lengthInBytes());
+ this.point = point;
}
/**
@@ -104,26 +116,34 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
PGbox p = (PGbox) obj;
// Same points.
- if (p.point[0].equals(point[0]) && p.point[1].equals(point[1])) {
+ PGpoint[] point = this.point;
+ PGpoint[] pPoint = p.point;
+ if (point == null) {
+ return pPoint == null;
+ } else if (pPoint == null) {
+ return false;
+ }
+
+ if (pPoint[0].equals(point[0]) && pPoint[1].equals(point[1])) {
return true;
}
// Points swapped.
- if (p.point[0].equals(point[1]) && p.point[1].equals(point[0])) {
+ if (pPoint[0].equals(point[1]) && pPoint[1].equals(point[0])) {
return true;
}
// Using the opposite two points of the box:
// (x1,y1),(x2,y2) -> (x1,y2),(x2,y1)
- if (p.point[0].x == point[0].x && p.point[0].y == point[1].y
- && p.point[1].x == point[1].x && p.point[1].y == point[0].y) {
+ if (pPoint[0].x == point[0].x && pPoint[0].y == point[1].y
+ && pPoint[1].x == point[1].x && pPoint[1].y == point[0].y) {
return true;
}
// Using the opposite two points of the box, and the points are swapped
// (x1,y1),(x2,y2) -> (x2,y1),(x1,y2)
- if (p.point[0].x == point[1].x && p.point[0].y == point[0].y
- && p.point[1].x == point[0].x && p.point[1].y == point[1].y) {
+ if (pPoint[0].x == point[1].x && pPoint[0].y == point[0].y
+ && pPoint[1].x == point[0].x && pPoint[1].y == point[1].y) {
return true;
}
}
@@ -136,7 +156,8 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
// its X and Y components; we end up with an exclusive-OR of the two X and
// two Y components, which is equal whenever equals() would return true
// since xor is commutative.
- return point[0].hashCode() ^ point[1].hashCode();
+ PGpoint[] point = this.point;
+ return point == null ? 0 : point[0].hashCode() ^ point[1].hashCode();
}
public Object clone() throws CloneNotSupportedException {
@@ -155,15 +176,21 @@ public class PGbox extends PGobject implements PGBinaryObject, Serializable, Clo
/**
* @return the PGbox in the syntax expected by org.postgresql
*/
- public String getValue() {
- return point[0].toString() + "," + point[1].toString();
+ public @Nullable String getValue() {
+ PGpoint[] point = this.point;
+ return point == null ? null : point[0].toString() + "," + point[1].toString();
}
public int lengthInBytes() {
+ PGpoint[] point = this.point;
+ if (point == null) {
+ return 0;
+ }
return point[0].lengthInBytes() + point[1].lengthInBytes();
}
public void toBytes(byte[] bytes, int offset) {
+ PGpoint[] point = castNonNull(this.point);
point[0].toBytes(bytes, offset);
point[1].toBytes(bytes, offset + point[0].lengthInBytes());
}
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java
index 14c7df82..8e842126 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGcircle.java
@@ -71,7 +71,11 @@ public class PGcircle extends PGobject implements Serializable, Cloneable {
* @throws SQLException on conversion failure
*/
@Override
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ if (s == null) {
+ center = null;
+ return;
+ }
PGtokenizer t = new PGtokenizer(PGtokenizer.removeAngle(s), ',');
if (t.getSize() != 2) {
throw new PSQLException(GT.tr("Conversion to type {0} failed: {1}.", type, s),
@@ -94,17 +98,26 @@ public class PGcircle extends PGobject implements Serializable, Cloneable {
public boolean equals(@Nullable Object obj) {
if (obj instanceof PGcircle) {
PGcircle p = (PGcircle) obj;
- return p.radius == radius && equals(p.center, center);
+ PGpoint center = this.center;
+ PGpoint pCenter = p.center;
+ if (center == null) {
+ return pCenter == null;
+ } else if (pCenter == null) {
+ return false;
+ }
+
+ return p.radius == radius && equals(pCenter, center);
}
return false;
}
public int hashCode() {
- long bits = Double.doubleToLongBits(radius);
- int v = (int)(bits ^ (bits >>> 32));
- if (center != null) {
- v = v * 31 + center.hashCode();
+ if (center == null) {
+ return 0;
}
+ long bits = Double.doubleToLongBits(radius);
+ int v = (int) (bits ^ (bits >>> 32));
+ v = v * 31 + center.hashCode();
return v;
}
@@ -119,7 +132,7 @@ public class PGcircle extends PGobject implements Serializable, Cloneable {
/**
* @return the PGcircle in the syntax expected by org.postgresql
*/
- public String getValue() {
- return "<" + center + "," + radius + ">";
+ public @Nullable String getValue() {
+ return center == null ? null : "<" + center + "," + radius + ">";
}
}
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java
index c2b98398..ca7f73f1 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGline.java
@@ -36,6 +36,8 @@ public class PGline extends PGobject implements Serializable, Cloneable {
*/
public double c;
+ private boolean isNull;
+
/**
* @param a coefficient of x
* @param b coefficient of y
@@ -54,31 +56,57 @@ public class PGline extends PGobject implements Serializable, Cloneable {
* @param x2 coordinate for second point on the line
* @param y2 coordinate for second point on the line
*/
+ @SuppressWarnings("method.invocation.invalid")
public PGline(double x1, double y1, double x2, double y2) {
this();
- if (x1 == x2) {
- a = -1;
- b = 0;
- } else {
- a = (y2 - y1) / (x2 - x1);
- b = -1;
- }
- c = y1 - a * x1;
+ setValue(x1, y1, x2, y2);
}
/**
* @param p1 first point on the line
* @param p2 second point on the line
*/
- public PGline(PGpoint p1, PGpoint p2) {
- this(p1.x, p1.y, p2.x, p2.y);
+ @SuppressWarnings("method.invocation.invalid")
+ public PGline(@Nullable PGpoint p1, @Nullable PGpoint p2) {
+ this();
+ setValue(p1, p2);
}
/**
* @param lseg Line segment which calls on this line.
*/
- public PGline(PGlseg lseg) {
- this(lseg.point[0], lseg.point[1]);
+ @SuppressWarnings("method.invocation.invalid")
+ public PGline(@Nullable PGlseg lseg) {
+ this();
+ if (lseg == null) {
+ isNull = true;
+ return;
+ }
+ PGpoint[] point = lseg.point;
+ if (point == null) {
+ isNull = true;
+ return;
+ }
+ setValue(point[0], point[1]);
+ }
+
+ private void setValue(@Nullable PGpoint p1, @Nullable PGpoint p2) {
+ if (p1 == null || p2 == null) {
+ isNull = true;
+ } else {
+ setValue(p1.x, p1.y, p2.x, p2.y);
+ }
+ }
+
+ private void setValue(double x1, double y1, double x2, double y2) {
+ if (x1 == x2) {
+ a = -1;
+ b = 0;
+ } else {
+ a = (y2 - y1) / (x2 - x1);
+ b = -1;
+ }
+ c = y1 - a * x1;
}
/**
@@ -103,7 +131,11 @@ public class PGline extends PGobject implements Serializable, Cloneable {
* @throws SQLException on conversion failure
*/
@Override
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ isNull = s == null;
+ if (s == null) {
+ return;
+ }
if (s.trim().startsWith("{")) {
PGtokenizer t = new PGtokenizer(PGtokenizer.removeCurlyBrace(s), ',');
if (t.getSize() != 3) {
@@ -143,6 +175,11 @@ public class PGline extends PGobject implements Serializable, Cloneable {
}
PGline pGline = (PGline) obj;
+ if (isNull) {
+ return pGline.isNull;
+ } else if (pGline.isNull) {
+ return false;
+ }
return Double.compare(pGline.a, a) == 0
&& Double.compare(pGline.b, b) == 0
@@ -150,6 +187,9 @@ public class PGline extends PGobject implements Serializable, Cloneable {
}
public int hashCode() {
+ if (isNull) {
+ return 0;
+ }
int result = super.hashCode();
long temp;
temp = Double.doubleToLongBits(a);
@@ -164,8 +204,8 @@ public class PGline extends PGobject implements Serializable, Cloneable {
/**
* @return the PGline in the syntax expected by org.postgresql
*/
- public String getValue() {
- return "{" + a + "," + b + "," + c + "}";
+ public @Nullable String getValue() {
+ return isNull ? null : "{" + a + "," + b + "," + c + "}";
}
@Override
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java
index 698fe40f..b8e667f7 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGlseg.java
@@ -23,7 +23,7 @@ public class PGlseg extends PGobject implements Serializable, Cloneable {
/**
* These are the two points.
*/
- public PGpoint[] point = new PGpoint[2];
+ public PGpoint @Nullable [] point;
/**
* @param x1 coordinate for first point
@@ -41,8 +41,7 @@ public class PGlseg extends PGobject implements Serializable, Cloneable {
*/
public PGlseg(PGpoint p1, PGpoint p2) {
this();
- this.point[0] = p1;
- this.point[1] = p2;
+ point = new PGpoint[]{p1, p2};
}
/**
@@ -67,13 +66,21 @@ public class PGlseg extends PGobject implements Serializable, Cloneable {
* @throws SQLException on conversion failure
*/
@Override
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ if (s == null) {
+ point = null;
+ return;
+ }
PGtokenizer t = new PGtokenizer(PGtokenizer.removeBox(s), ',');
if (t.getSize() != 2) {
throw new PSQLException(GT.tr("Conversion to type {0} failed: {1}.", type, s),
PSQLState.DATA_TYPE_MISMATCH);
}
+ PGpoint[] point = this.point;
+ if (point == null) {
+ this.point = point = new PGpoint[2];
+ }
point[0] = new PGpoint(t.getToken(0));
point[1] = new PGpoint(t.getToken(1));
}
@@ -85,13 +92,24 @@ public class PGlseg extends PGobject implements Serializable, Cloneable {
public boolean equals(@Nullable Object obj) {
if (obj instanceof PGlseg) {
PGlseg p = (PGlseg) obj;
- return (p.point[0].equals(point[0]) && p.point[1].equals(point[1]))
- || (p.point[0].equals(point[1]) && p.point[1].equals(point[0]));
+ PGpoint[] point = this.point;
+ PGpoint[] pPoint = p.point;
+ if (point == null) {
+ return pPoint == null;
+ } else if (pPoint == null) {
+ return false;
+ }
+ return (pPoint[0].equals(point[0]) && pPoint[1].equals(point[1]))
+ || (pPoint[0].equals(point[1]) && pPoint[1].equals(point[0]));
}
return false;
}
public int hashCode() {
+ PGpoint[] point = this.point;
+ if (point == null) {
+ return 0;
+ }
return point[0].hashCode() ^ point[1].hashCode();
}
@@ -111,7 +129,11 @@ public class PGlseg extends PGobject implements Serializable, Cloneable {
/**
* @return the PGlseg in the syntax expected by org.postgresql
*/
- public String getValue() {
+ public @Nullable String getValue() {
+ PGpoint[] point = this.point;
+ if (point == null) {
+ return null;
+ }
return "[" + point[0] + "," + point[1] + "]";
}
}
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java
index c263735b..68e290f6 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGpath.java
@@ -34,7 +34,7 @@ public class PGpath extends PGobject implements Serializable, Cloneable {
* @param points the PGpoints that define the path
* @param open True if the path is open, false if closed
*/
- public PGpath(PGpoint[] points, boolean open) {
+ public PGpath(PGpoint @Nullable [] points, boolean open) {
this();
this.points = points;
this.open = open;
@@ -61,7 +61,11 @@ public class PGpath extends PGobject implements Serializable, Cloneable {
* @param s Definition of the path in PostgreSQL's syntax
* @throws SQLException on conversion failure
*/
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ if (s == null) {
+ points = null;
+ return;
+ }
// First test to see if were open
if (s.startsWith("[") && s.endsWith("]")) {
open = true;
@@ -91,24 +95,24 @@ public class PGpath extends PGobject implements Serializable, Cloneable {
if (obj instanceof PGpath) {
PGpath p = (PGpath) obj;
- if (p.open != open) {
+ PGpoint[] points = this.points;
+ PGpoint[] pPoints = p.points;
+ if (points == null) {
+ return pPoints == null;
+ } else if (pPoints == null) {
return false;
}
- if (points == null ^ p.points == null) {
+ if (p.open != open) {
return false;
}
- if (points == null) {
- return true;
- }
-
- if (p.points.length != points.length) {
+ if (pPoints.length != points.length) {
return false;
}
for (int i = 0; i < points.length; i++) {
- if (!points[i].equals(p.points[i])) {
+ if (!points[i].equals(pPoints[i])) {
return false;
}
}
@@ -119,11 +123,12 @@ public class PGpath extends PGobject implements Serializable, Cloneable {
}
public int hashCode() {
- // XXX not very good..
- int hash = open ? 1231 : 1237;
+ PGpoint[] points = this.points;
if (points == null) {
- return hash;
+ return 0;
}
+ // XXX not very good..
+ int hash = open ? 1231 : 1237;
for (int i = 0; i < points.length && i < 5; ++i) {
hash = hash * 31 + points[i].hashCode();
}
@@ -144,12 +149,16 @@ public class PGpath extends PGobject implements Serializable, Cloneable {
/**
* This returns the path in the syntax expected by org.postgresql.
+ * @return the value of this object
*/
- public String getValue() {
+ public @Nullable String getValue() {
+ PGpoint[] points = this.points;
+ if (points == null) {
+ return null;
+ }
StringBuilder b = new StringBuilder(open ? "[" : "(");
- PGpoint[] points = this.points;
- for (int p = 0; points != null && p < points.length; p++) {
+ for (int p = 0; p < points.length; p++) {
if (p > 0) {
b.append(",");
}
@@ -161,11 +170,11 @@ public class PGpath extends PGobject implements Serializable, Cloneable {
}
public boolean isOpen() {
- return open;
+ return open && points != null;
}
public boolean isClosed() {
- return !open;
+ return !open && points != null;
}
public void closePath() {
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java
index 4e1ebdb7..cc0af5f0 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGpoint.java
@@ -35,6 +35,11 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
*/
public double y;
+ /**
+ * True if the point represents {@code null::point}.
+ */
+ public boolean isNull;
+
/**
* @param x coordinate
* @param y coordinate
@@ -70,7 +75,11 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
* @throws SQLException on conversion failure
*/
@Override
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ isNull = s == null;
+ if (s == null) {
+ return;
+ }
PGtokenizer t = new PGtokenizer(PGtokenizer.removePara(s), ',');
try {
x = Double.parseDouble(t.getToken(0));
@@ -85,6 +94,7 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
* @param b Definition of this point in PostgreSQL's binary syntax
*/
public void setByteValue(byte[] b, int offset) {
+ this.isNull = false;
x = ByteConverter.float8(b, offset);
y = ByteConverter.float8(b, offset + 8);
}
@@ -96,12 +106,20 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
public boolean equals(@Nullable Object obj) {
if (obj instanceof PGpoint) {
PGpoint p = (PGpoint) obj;
+ if (isNull) {
+ return p.isNull;
+ } else if (p.isNull) {
+ return false;
+ }
return x == p.x && y == p.y;
}
return false;
}
public int hashCode() {
+ if (isNull) {
+ return 0;
+ }
long v1 = Double.doubleToLongBits(x);
long v2 = Double.doubleToLongBits(y);
return (int) (v1 ^ v2 ^ (v1 >>> 32) ^ (v2 >>> 32));
@@ -110,18 +128,21 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
/**
* @return the PGpoint in the syntax expected by org.postgresql
*/
- public String getValue() {
- return "(" + x + "," + y + ")";
+ public @Nullable String getValue() {
+ return isNull ? null : "(" + x + "," + y + ")";
}
public int lengthInBytes() {
- return 16;
+ return isNull ? 0 : 16;
}
/**
* Populate the byte array with PGpoint in the binary syntax expected by org.postgresql.
*/
public void toBytes(byte[] b, int offset) {
+ if (isNull) {
+ return;
+ }
ByteConverter.float8(b, offset, x);
ByteConverter.float8(b, offset + 8, y);
}
@@ -143,6 +164,7 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
* @param y double amount to add on the y axis
*/
public void translate(double x, double y) {
+ this.isNull = false;
this.x += x;
this.y += y;
}
@@ -164,6 +186,7 @@ public class PGpoint extends PGobject implements PGBinaryObject, Serializable, C
* @param y double coordinate
*/
public void move(double x, double y) {
+ this.isNull = false;
this.x = x;
this.y = y;
}
diff --git a/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java b/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java
index 8726e5df..11ff85ed 100644
--- a/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java
+++ b/pgjdbc/src/main/java/org/postgresql/geometric/PGpolygon.java
@@ -53,11 +53,17 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable {
* @param s Definition of the polygon in PostgreSQL's syntax
* @throws SQLException on conversion failure
*/
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ if (s == null) {
+ points = null;
+ return;
+ }
PGtokenizer t = new PGtokenizer(PGtokenizer.removePara(s), ',');
int npoints = t.getSize();
- PGpoint[] points = new PGpoint[npoints];
- this.points = points;
+ PGpoint[] points = this.points;
+ if (points == null || points.length != npoints) {
+ this.points = points = new PGpoint[npoints];
+ }
for (int p = 0; p < npoints; p++) {
points[p] = new PGpoint(t.getToken(p));
}
@@ -71,20 +77,20 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable {
if (obj instanceof PGpolygon) {
PGpolygon p = (PGpolygon) obj;
- if (points == null ^ p.points == null) {
- return false;
- }
-
+ PGpoint[] points = this.points;
+ PGpoint[] pPoints = p.points;
if (points == null) {
- return true;
+ return pPoints == null;
+ } else if (pPoints == null) {
+ return false;
}
- if (p.points.length != points.length) {
+ if (pPoints.length != points.length) {
return false;
}
for (int i = 0; i < points.length; i++) {
- if (!points[i].equals(p.points[i])) {
+ if (!points[i].equals(pPoints[i])) {
return false;
}
}
@@ -95,7 +101,6 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable {
}
public int hashCode() {
- // XXX not very good..
int hash = 0;
PGpoint[] points = this.points;
if (points == null) {
@@ -124,11 +129,14 @@ public class PGpolygon extends PGobject implements Serializable, Cloneable {
/**
* @return the PGpolygon in the syntax expected by org.postgresql
*/
- public String getValue() {
+ public @Nullable String getValue() {
+ PGpoint[] points = this.points;
+ if (points == null) {
+ return null;
+ }
StringBuilder b = new StringBuilder();
b.append("(");
- PGpoint[] points = this.points;
- for (int p = 0; points != null && p < points.length; p++) {
+ for (int p = 0; p < points.length; p++) {
if (p > 0) {
b.append(",");
}
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 9968aa20..2b98f5e1 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -651,14 +651,14 @@ public class PgConnection implements BaseConnection {
PGBinaryObject binObj = (PGBinaryObject) obj;
binObj.setByteValue(byteValue, 0);
} else {
- obj.setValue(castNonNull(value));
+ obj.setValue(value);
}
} else {
// If className is null, then the type is unknown.
// so return a PGobject with the type set, and the value set
obj = new PGobject();
obj.setType(type);
- obj.setValue(castNonNull(value));
+ obj.setValue(value);
}
return obj;
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
index 0aeb7e72..221760e0 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
@@ -489,7 +489,12 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
if ((x instanceof PGBinaryObject) && connection.binaryTransferSend(oid)) {
PGBinaryObject binObj = (PGBinaryObject) x;
- byte[] data = new byte[binObj.lengthInBytes()];
+ int length = binObj.lengthInBytes();
+ if (length == 0) {
+ preparedParameters.setNull(parameterIndex, oid);
+ return;
+ }
+ byte[] data = new byte[length];
binObj.toBytes(data, 0);
bindBytes(parameterIndex, data, oid);
} else {
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
index 37d8bb39..c28f9fe6 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
@@ -30,6 +30,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
private int minutes;
private int wholeSeconds;
private int microSeconds;
+ private boolean isNull;
/**
* required by the driver.
@@ -135,7 +136,13 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param value String representated interval (e.g. '3 years 2 mons')
* @throws SQLException Is thrown if the string representation has an unknown format
*/
- public void setValue(String value) throws SQLException {
+ public void setValue(@Nullable String value) throws SQLException {
+ isNull = value == null;
+ if (value == null) {
+ setValue(0, 0, 0, 0, 0, 0);
+ isNull = true;
+ return;
+ }
final boolean PostgresFormat = !value.startsWith("@");
if (value.startsWith("P")) {
parseISO8601Format(value);
@@ -247,7 +254,10 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
*
* @return String represented interval
*/
- public String getValue() {
+ public @Nullable String getValue() {
+ if (isNull) {
+ return null;
+ }
DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(Locale.US);
df.applyPattern("0.0#####");
@@ -278,6 +288,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param years years to set
*/
public void setYears(int years) {
+ isNull = false;
this.years = years;
}
@@ -296,6 +307,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param months months to set
*/
public void setMonths(int months) {
+ isNull = false;
this.months = months;
}
@@ -314,6 +326,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param days days to set
*/
public void setDays(int days) {
+ isNull = false;
this.days = days;
}
@@ -332,6 +345,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param hours hours to set
*/
public void setHours(int hours) {
+ isNull = false;
this.hours = hours;
}
@@ -350,6 +364,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param minutes minutes to set
*/
public void setMinutes(int minutes) {
+ isNull = false;
this.minutes = minutes;
}
@@ -376,6 +391,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param seconds seconds to set
*/
public void setSeconds(double seconds) {
+ isNull = false;
wholeSeconds = (int) seconds;
microSeconds = (int) Math.round((seconds - wholeSeconds) * MICROS_IN_SECOND);
}
@@ -386,6 +402,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param cal Calendar instance to add to
*/
public void add(Calendar cal) {
+ if (isNull) {
+ return;
+ }
final int milliseconds = (microSeconds + ((microSeconds < 0) ? -500 : 500)) / 1000 + wholeSeconds * 1000;
@@ -403,6 +422,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param date Date instance to add to
*/
public void add(Date date) {
+ if (isNull) {
+ return;
+ }
final Calendar cal = Calendar.getInstance();
cal.setTime(date);
add(cal);
@@ -416,6 +438,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param interval intval to add
*/
public void add(PGInterval interval) {
+ if (isNull || interval.isNull) {
+ return;
+ }
interval.setYears(interval.getYears() + getYears());
interval.setMonths(interval.getMonths() + getMonths());
interval.setDays(interval.getDays() + getDays());
@@ -433,6 +458,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
* @param factor scale factor
*/
public void scale(int factor) {
+ if (isNull) {
+ return;
+ }
setYears(factor * getYears());
setMonths(factor * getMonths());
setDays(factor * getDays());
@@ -483,6 +511,11 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
}
final PGInterval pgi = (PGInterval) obj;
+ if (isNull) {
+ return pgi.isNull;
+ } else if (pgi.isNull) {
+ return false;
+ }
return pgi.years == years
&& pgi.months == months
@@ -500,6 +533,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
*/
@Override
public int hashCode() {
+ if (isNull) {
+ return 0;
+ }
return (((((((8 * 31 + microSeconds) * 31 + wholeSeconds) * 31 + minutes) * 31 + hours) * 31
+ days) * 31 + months) * 31 + years) * 31;
}
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java b/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java
index 8607d19b..249dc240 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PGmoney.java
@@ -19,6 +19,11 @@ public class PGmoney extends PGobject implements Serializable, Cloneable {
*/
public double val;
+ /**
+ * If the object represents {@code null::money}
+ */
+ public boolean isNull;
+
/**
* @param value of field
*/
@@ -40,7 +45,11 @@ public class PGmoney extends PGobject implements Serializable, Cloneable {
type = "money";
}
- public void setValue(String s) throws SQLException {
+ public void setValue(@Nullable String s) throws SQLException {
+ isNull = s == null;
+ if (s == null) {
+ return;
+ }
try {
String s1;
boolean negative;
@@ -68,6 +77,9 @@ public class PGmoney extends PGobject implements Serializable, Cloneable {
@Override
public int hashCode() {
+ if (isNull) {
+ return 0;
+ }
final int prime = 31;
int result = super.hashCode();
long temp;
@@ -79,12 +91,20 @@ public class PGmoney extends PGobject implements Serializable, Cloneable {
public boolean equals(@Nullable Object obj) {
if (obj instanceof PGmoney) {
PGmoney p = (PGmoney) obj;
+ if (isNull) {
+ return p.isNull;
+ } else if (p.isNull) {
+ return false;
+ }
return val == p.val;
}
return false;
}
- public String getValue() {
+ public @Nullable String getValue() {
+ if (isNull) {
+ return null;
+ }
if (val < 0) {
return "-$" + (-val);
} else {
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGobject.java b/pgjdbc/src/main/java/org/postgresql/util/PGobject.java
index 291ce93f..e52b786d 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PGobject.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PGobject.java
@@ -43,7 +43,7 @@ public class PGobject implements Serializable, Cloneable {
* @param value a string representation of the value of the object
* @throws SQLException thrown if value is invalid for this type
*/
- public void setValue(String value) throws SQLException {
+ public void setValue(@Nullable String value) throws SQLException {
this.value = value;
}
@@ -53,7 +53,7 @@ public class PGobject implements Serializable, Cloneable {
* @return the type name of this object
*/
public final String getType() {
- return castNonNull(type);
+ return castNonNull(type, "PGobject#type is uninitialized. Please call setType(String)");
}
/**
@@ -66,6 +66,16 @@ public class PGobject implements Serializable, Cloneable {
return value;
}
+ /**
+ * Returns true if the current object wraps `null` value.
+ * This might be helpful
+ *
+ * @return true if the current object wraps `null` value.
+ */
+ public boolean isNull() {
+ return getValue() != null;
+ }
+
/**
* This must be overidden to allow comparisons of objects.
*
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectGetTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectGetTest.java
new file mode 100644
index 00000000..c59abcfc
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectGetTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.postgresql.geometric.PGbox;
+import org.postgresql.geometric.PGcircle;
+import org.postgresql.geometric.PGline;
+import org.postgresql.geometric.PGlseg;
+import org.postgresql.geometric.PGpath;
+import org.postgresql.geometric.PGpoint;
+import org.postgresql.geometric.PGpolygon;
+import org.postgresql.util.PGInterval;
+import org.postgresql.util.PGmoney;
+import org.postgresql.util.PGobject;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class PGObjectGetTest extends BaseTest4 {
+ private final String sqlExpression;
+ private final Class<? extends PGobject> type;
+ private final String expected;
+ private final String stringValue;
+
+ public PGObjectGetTest(BinaryMode binaryMode, String sqlExpression,
+ Class<? extends PGobject> type, String expected, String stringValue) {
+ setBinaryMode(binaryMode);
+ this.sqlExpression = sqlExpression;
+ this.type = type;
+ this.expected = expected;
+ this.stringValue = stringValue;
+ }
+
+ @Parameterized.Parameters(name = "binary = {0}, sql = {1}, type = {2}")
+ public static Iterable<Object[]> data() {
+ Collection<Object[]> ids = new ArrayList<Object[]>();
+ for (BinaryMode binaryMode : BinaryMode.values()) {
+ ids.add(new Object[]{binaryMode, "null::inet", PGobject.class,
+ "PGobject(type=inet, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::box", PGbox.class,
+ "PGbox(type=box, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::circle", PGcircle.class,
+ "PGcircle(type=circle, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::line", PGline.class,
+ "PGline(type=line, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::lseg", PGlseg.class,
+ "PGlseg(type=lseg, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::path", PGpath.class,
+ "PGpath(type=path, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::point", PGpoint.class,
+ "PGpoint(type=point, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::polygon", PGpolygon.class,
+ "PGpolygon(type=polygon, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::money", PGmoney.class,
+ "PGmoney(type=money, value=null)", null});
+ ids.add(new Object[]{binaryMode, "null::interval", PGInterval.class,
+ "PGInterval(type=interval, value=null)", null});
+ }
+ return ids;
+ }
+
+ @Test
+ public void getAsPGobject() throws SQLException {
+ testGet(sqlExpression, expected, PGobject.class);
+ }
+
+ @Test
+ public void getAsPGobjectSubtype() throws SQLException {
+ testGet(sqlExpression, expected, type);
+ }
+
+ @Test
+ public void getAsString() throws SQLException {
+ PreparedStatement ps = con.prepareStatement("select " + sqlExpression);
+ ResultSet rs = ps.executeQuery();
+ rs.next();
+ assertEquals(
+ "'" + sqlExpression + "'.getString(1)",
+ stringValue,
+ rs.getString(1)
+ );
+ }
+
+ private void testGet(final String s, String expected, Class<? extends PGobject> type) throws SQLException {
+ PreparedStatement ps = con.prepareStatement("select " + s);
+ ResultSet rs = ps.executeQuery();
+ rs.next();
+ assertEquals(
+ "'" + s + "'.getObject(1, " + type.getSimpleName() + ".class)",
+ expected,
+ printObject(rs.getObject(1, type))
+ );
+ if (expected.contains("value=null)")) {
+ // For some reason we return objects as nulls
+ assertNull(
+ "'select " + s + "'.getObject(1)",
+ rs.getObject(1)
+ );
+ } else {
+ assertEquals(
+ "'select " + s + "'.getObject(1)",
+ expected,
+ printObject(rs.getObject(1))
+ );
+ }
+ }
+
+ String printObject(@Nullable Object object) {
+ if (!(object instanceof PGobject)) {
+ return String.valueOf(object);
+ }
+ PGobject pg = (PGobject) object;
+ return pg.getClass().getSimpleName() + "(type=" + pg.getType() + ", value=" + pg.getValue() + ")";
+ }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectSetTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectSetTest.java
new file mode 100644
index 00000000..7ee8c400
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGObjectSetTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.postgresql.geometric.PGbox;
+import org.postgresql.geometric.PGcircle;
+import org.postgresql.geometric.PGline;
+import org.postgresql.geometric.PGlseg;
+import org.postgresql.geometric.PGpath;
+import org.postgresql.geometric.PGpoint;
+import org.postgresql.geometric.PGpolygon;
+import org.postgresql.util.PGInterval;
+import org.postgresql.util.PGmoney;
+import org.postgresql.util.PGobject;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.lang.reflect.InvocationTargetException;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class PGObjectSetTest extends BaseTest4 {
+ private final String typeName;
+ private final String expected;
+ private final Class<? extends PGobject> type;
+
+ public PGObjectSetTest(BinaryMode binaryMode, Class<? extends PGobject> type,
+ String typeName, String expected) {
+ setBinaryMode(binaryMode);
+ this.expected = expected;
+ this.type = type;
+ this.typeName = typeName;
+ }
+
+ @Parameterized.Parameters(name = "binary = {0}, sql = {2}, type = {1}")
+ public static Iterable<Object[]> data() {
+ Collection<Object[]> ids = new ArrayList<Object[]>();
+ for (BinaryMode binaryMode : BinaryMode.values()) {
+ ids.add(new Object[]{binaryMode, PGobject.class, "inet",
+ "PGobject(type=inet, value=null)"});
+ ids.add(new Object[]{binaryMode, PGbox.class, "box",
+ "PGbox(type=box, value=null)"});
+ ids.add(new Object[]{binaryMode, PGcircle.class, "circle",
+ "PGcircle(type=circle, value=null)"});
+ ids.add(new Object[]{binaryMode, PGline.class, "line",
+ "PGline(type=line, value=null)"});
+ ids.add(new Object[]{binaryMode, PGlseg.class, "lseg",
+ "PGlseg(type=lseg, value=null)"});
+ ids.add(new Object[]{binaryMode, PGpath.class, "path",
+ "PGpath(type=path, value=null)"});
+ ids.add(new Object[]{binaryMode, PGpoint.class, "point",
+ "PGpoint(type=point, value=null)"});
+ ids.add(new Object[]{binaryMode, PGpolygon.class, "polygon",
+ "PGpolygon(type=polygon, value=null)"});
+ ids.add(new Object[]{binaryMode, PGmoney.class, "money",
+ "PGmoney(type=money, value=null)"});
+ ids.add(new Object[]{binaryMode, PGInterval.class, "interval",
+ "PGInterval(type=interval, value=null)"});
+ }
+ return ids;
+ }
+
+ @Test
+ public void setNullAsPGobject() throws SQLException {
+ PGobject object = new PGobject();
+ object.setType(typeName);
+ object.setValue(null);
+ testSet(object, expected, PGobject.class);
+ }
+
+ @Test
+ public void setNullAsPGobjectSubtype() throws SQLException, NoSuchMethodException,
+ IllegalAccessException, InvocationTargetException, InstantiationException {
+ if (type == PGobject.class) {
+ // We can't use PGobject without setType
+ return;
+ }
+ PGobject object = type.getConstructor().newInstance();
+ object.setValue(null);
+ testSet(object, expected, type);
+ }
+
+ private void testSet(PGobject value, String expected, Class<? extends PGobject> type) throws SQLException {
+ PreparedStatement ps = con.prepareStatement("select ?::" + value.getType());
+ ps.setObject(1, value);
+ ResultSet rs = ps.executeQuery();
+ rs.next();
+ assertEquals(
+ "'select ?::" + value.getType() + "'.withParam(" + printObject(value) + ").getObject(1, " + type.getSimpleName() + ".class)",
+ expected,
+ printObject(rs.getObject(1, type))
+ );
+ if (expected.contains("value=null)")) {
+ assertNull(
+ "'select ?::" + value.getType() + "'.withParam(" + printObject(value) + ").getObject(1)",
+ rs.getObject(1)
+ );
+ } else {
+ assertEquals(
+ "'select ?::" + value.getType() + "'.withParam(" + printObject(value) + ").getObject(1)",
+ expected,
+ printObject(rs.getObject(1))
+ );
+ }
+ }
+
+ String printObject(@Nullable Object object) {
+ if (!(object instanceof PGobject)) {
+ return String.valueOf(object);
+ }
+ PGobject pg = (PGobject) object;
+ return pg.getClass().getSimpleName() + "(type=" + pg.getType() + ", value=" + pg.getValue() + ")";
+ }
+}
[parent: a2085ca41c9b]