Skip to content

Commit b6f864f

Browse files
NPCRUSnpcrusde
andauthored
JoinNullable deserialisation fix (#70)
In the current implementation, JoinNullable in order to determine that R shouldBe Option or Some is deserialising whole set and then counting nulls. In cases where you have user written TypeMapper, that doesn't do nulls tricks(like inside Dialects.scala), such: ``` enum Enumeration { case A, B, C } object Enumeration { given TypeMapper[Enumeration] = StringType.bimap(_.toString(), Enumeration.valueOf) } ``` then leftJoin query, where the right side can be nullable instead of returning (A, None) will fail with exception, check new test in DataTypesTests new implementation will check first if the rest of columns are null without deserialising them into type and only if one of them is not - will try to construct comments on everything are welcome! fixes #65 --------- Co-authored-by: nikitaglushchenko <[email protected]>
1 parent fb19451 commit b6f864f

File tree

8 files changed

+107
-6
lines changed

8 files changed

+107
-6
lines changed

docs/reference.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9918,6 +9918,32 @@ db.run(Enclosing.select) ==> Seq(value1, value2)
99189918
99199919
99209920
9921+
### DataTypes.JoinNullable proper type mapping
9922+
9923+
9924+
9925+
```scala
9926+
case class A[T[_]](id: T[Int], bId: T[Option[Int]])
9927+
object A extends Table[A]
9928+
9929+
object Custom extends Enumeration {
9930+
val Foo, Bar = Value
9931+
9932+
implicit def make: String => Value = withName
9933+
}
9934+
9935+
case class B[T[_]](id: T[Int], custom: T[Custom.Value])
9936+
object B extends Table[B]
9937+
db.run(A.insert.columns(_.id := 1, _.bId := None))
9938+
val result = db.run(A.select.leftJoin(B)(_.id === _.id).single)
9939+
result._2 ==> None
9940+
```
9941+
9942+
9943+
9944+
9945+
9946+
99219947
## Optional
99229948
Queries using columns that may be `NULL`, `Expr[Option[T]]` or `Option[T]` in Scala
99239949
### Optional

scalasql/core/src/Queryable.scala

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ object Queryable {
6868
var nulls = 0
6969
var nonNulls = 0
7070

71+
def consumeNulls(columnsCount: Int): Boolean = {
72+
val result = Range.inclusive(index + 1, index + columnsCount).forall { i =>
73+
r.getObject(i) == null
74+
}
75+
if (result) index = index + columnsCount
76+
result
77+
}
78+
7179
def get[T](mt: TypeMapper[T]) = {
7280
index += 1
7381
val res = mt.get(r, index)
@@ -143,10 +151,11 @@ object Queryable {
143151
def walkExprs(q: JoinNullable[Q]) = qr.walkExprs(q.get)
144152

145153
def construct(args: Queryable.ResultSetIterator): Option[R] = {
146-
val startNonNulls = args.nonNulls
147-
val res = qr.construct(args)
148-
if (startNonNulls == args.nonNulls) None
149-
else Option(res)
154+
if (args.consumeNulls(qr.walkLabels().length)) {
155+
None
156+
} else {
157+
Option(qr.construct(args))
158+
}
150159
}
151160

152161
def deconstruct(r: Option[R]): JoinNullable[Q] = JoinNullable(qr.deconstruct(r.get))

scalasql/src/dialects/Dialect.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,7 @@ trait Dialect extends DialectTypeMappers {
228228
def jdbcType: JDBCType = JDBCType.VARCHAR
229229
def get(r: ResultSet, idx: Int): T = {
230230
val str = r.getString(idx)
231-
if (str == null) null.asInstanceOf[T]
232-
else constructor(str)
231+
constructor(str)
233232
}
234233
def put(r: PreparedStatement, idx: Int, v: T) =
235234
r.setObject(idx, v, java.sql.Types.OTHER)

scalasql/test/resources/h2-customer-schema.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ DROP TABLE IF EXISTS product CASCADE;
33
DROP TABLE IF EXISTS shipping_info CASCADE;
44
DROP TABLE IF EXISTS purchase CASCADE;
55
DROP TABLE IF EXISTS data_types CASCADE;
6+
DROP TABLE IF EXISTS a CASCADE;
7+
DROP TABLE IF EXISTS b CASCADE;
68
DROP TABLE IF EXISTS non_round_trip_types CASCADE;
79
DROP TABLE IF EXISTS opt_cols CASCADE;
810
DROP TABLE IF EXISTS nested CASCADE;
@@ -58,6 +60,16 @@ CREATE TABLE data_types (
5860
-- my_offset_time TIME WITH TIME ZONE,
5961
);
6062

63+
CREATE TABLE a(
64+
id INTEGER,
65+
b_id INTEGER
66+
);
67+
68+
CREATE TABLE b(
69+
id INTEGER,
70+
custom VARCHAR(256)
71+
);
72+
6173
CREATE TABLE non_round_trip_types(
6274
my_zoned_date_time TIMESTAMP WITH TIME ZONE,
6375
my_offset_date_time TIMESTAMP WITH TIME ZONE

scalasql/test/resources/mysql-customer-schema.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ DROP TABLE IF EXISTS `product` CASCADE;
44
DROP TABLE IF EXISTS `shipping_info` CASCADE;
55
DROP TABLE IF EXISTS `purchase` CASCADE;
66
DROP TABLE IF EXISTS `data_types` CASCADE;
7+
DROP TABLE IF EXISTS `a` CASCADE;
8+
DROP TABLE IF EXISTS `b` CASCADE;
79
DROP TABLE IF EXISTS `non_round_trip_types` CASCADE;
810
DROP TABLE IF EXISTS `opt_cols` CASCADE;
911
DROP TABLE IF EXISTS `nested` CASCADE;
@@ -57,6 +59,16 @@ CREATE TABLE data_types (
5759
my_enum ENUM ('foo', 'bar', 'baz')
5860
);
5961

62+
CREATE TABLE a(
63+
id INTEGER,
64+
b_id INTEGER
65+
);
66+
67+
CREATE TABLE b(
68+
id INTEGER,
69+
custom VARCHAR(256)
70+
);
71+
6072
CREATE TABLE non_round_trip_types(
6173
my_zoned_date_time TIMESTAMP,
6274
my_offset_date_time TIMESTAMP

scalasql/test/resources/postgres-customer-schema.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ DROP TABLE IF EXISTS product CASCADE;
33
DROP TABLE IF EXISTS shipping_info CASCADE;
44
DROP TABLE IF EXISTS purchase CASCADE;
55
DROP TABLE IF EXISTS data_types CASCADE;
6+
DROP TABLE IF EXISTS a CASCADE;
7+
DROP TABLE IF EXISTS b CASCADE;
68
DROP TABLE IF EXISTS non_round_trip_types CASCADE;
79
DROP TABLE IF EXISTS opt_cols CASCADE;
810
DROP TABLE IF EXISTS nested CASCADE;
@@ -61,6 +63,16 @@ CREATE TABLE data_types (
6163

6264
);
6365

66+
CREATE TABLE a(
67+
id INTEGER,
68+
b_id INTEGER
69+
);
70+
71+
CREATE TABLE b(
72+
id INTEGER,
73+
custom VARCHAR(256)
74+
);
75+
6476
CREATE TABLE non_round_trip_types(
6577
my_zoned_date_time TIMESTAMP WITH TIME ZONE,
6678
my_offset_date_time TIMESTAMP WITH TIME ZONE

scalasql/test/resources/sqlite-customer-schema.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ DROP TABLE IF EXISTS product;
33
DROP TABLE IF EXISTS shipping_info;
44
DROP TABLE IF EXISTS purchase;
55
DROP TABLE IF EXISTS data_types;
6+
DROP TABLE IF EXISTS a;
7+
DROP TABLE IF EXISTS b;
68
DROP TABLE IF EXISTS non_round_trip_types;
79
DROP TABLE IF EXISTS nested;
810
DROP TABLE IF EXISTS enclosing;
@@ -56,6 +58,16 @@ CREATE TABLE data_types (
5658
-- my_offset_time TIME WITH TIME ZONE,
5759
);
5860

61+
CREATE TABLE a(
62+
id INTEGER,
63+
b_id INTEGER
64+
);
65+
66+
CREATE TABLE b(
67+
id INTEGER,
68+
custom VARCHAR(256)
69+
);
70+
5971
CREATE TABLE non_round_trip_types(
6072
my_zoned_date_time TIMESTAMP,
6173
my_offset_date_time TIMESTAMP

scalasql/test/src/datatypes/DataTypesTests.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,24 @@ trait DataTypesTests extends ScalaSqlSuite {
212212

213213
}
214214
)
215+
test("JoinNullable proper type mapping") - checker.recorded(
216+
"",
217+
Text {
218+
case class A[T[_]](id: T[Int], bId: T[Option[Int]])
219+
object A extends Table[A]
220+
221+
object Custom extends Enumeration {
222+
val Foo, Bar = Value
223+
224+
implicit def make: String => Value = withName
225+
}
226+
227+
case class B[T[_]](id: T[Int], custom: T[Custom.Value])
228+
object B extends Table[B]
229+
db.run(A.insert.columns(_.id := 1, _.bId := None))
230+
val result = db.run(A.select.leftJoin(B)(_.id === _.id).single)
231+
result._2 ==> None
232+
}
233+
)
215234
}
216235
}

0 commit comments

Comments
 (0)