Skip to content

Commit 4152c7e

Browse files
Turso Agentclaude
andcommitted
fix: emit RealAffinity when reading subquery columns from ephemeral indexes
When columns with REAL affinity were read from ephemeral auto-indexes (used for CTE/subquery JOINs), the RealAffinity instruction was not emitted. This caused integer-representable float values (e.g. 1.0) to be returned as integers instead of reals, diverging from SQLite behavior. Add maybe_apply_affinity calls in both subquery Column-read paths (outer-scope index references and same-scope ephemeral index seeks). Closes #5474 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f51e31c commit 4152c7e

File tree

4 files changed

+231
-149
lines changed

4 files changed

+231
-149
lines changed

core/translate/expr.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,6 +2676,9 @@ pub fn translate_expr(
26762676
dest: target_register,
26772677
default: None,
26782678
});
2679+
if let Some(col) = from_clause_subquery.columns.get(*column) {
2680+
maybe_apply_affinity(col.ty(), target_register, program);
2681+
}
26792682
return Ok(target_register);
26802683
}
26812684
}
@@ -2712,6 +2715,9 @@ pub fn translate_expr(
27122715
dest: target_register,
27132716
default: None,
27142717
});
2718+
if let Some(col) = from_clause_subquery.columns.get(*column) {
2719+
maybe_apply_affinity(col.ty(), target_register, program);
2720+
}
27152721
return Ok(target_register);
27162722
}
27172723
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
@database :memory:
2+
3+
# Tests for REAL affinity preservation when reading CTE/subquery columns
4+
# through ephemeral auto-indexes during JOINs.
5+
# Regression tests for https://github.com/tursodatabase/turso/issues/5474
6+
7+
test cte-real-affinity-join-original-issue {
8+
WITH cte0 AS (SELECT CAST(('foo' >= -52) AS REAL) AS c0_0),
9+
cte1 AS (SELECT c0_0 AS c1_0 FROM cte0),
10+
cte2 AS (SELECT c1_0 AS c2_0 FROM cte1),
11+
cte3 AS (SELECT t1.c0_0 AS c3_0, t2.c0_0 AS c3_1 FROM cte0 t1, cte0 t2)
12+
SELECT t1.c3_0, t2.c0_0
13+
FROM cte3 t1 LEFT JOIN cte0 t2 ON t1.c3_0 = t2.c0_0
14+
ORDER BY 1, 2;
15+
}
16+
expect {
17+
1.0|1.0
18+
}
19+
20+
test cte-real-affinity-inner-join {
21+
WITH cte AS (SELECT CAST(1 AS REAL) AS val)
22+
SELECT t2.val, typeof(t2.val)
23+
FROM cte t1 JOIN cte t2 ON t1.val = t2.val;
24+
}
25+
expect {
26+
1.0|real
27+
}
28+
29+
test cte-real-affinity-left-join {
30+
WITH cte AS (SELECT CAST(1 AS REAL) AS val)
31+
SELECT t2.val, typeof(t2.val)
32+
FROM cte t1 LEFT JOIN cte t2 ON t1.val = t2.val;
33+
}
34+
expect {
35+
1.0|real
36+
}
37+
38+
test cte-real-affinity-zero {
39+
WITH cte AS (SELECT CAST(0 AS REAL) AS val)
40+
SELECT t2.val, typeof(t2.val)
41+
FROM cte t1 JOIN cte t2 ON t1.val = t2.val;
42+
}
43+
expect {
44+
0.0|real
45+
}
46+
47+
test cte-real-affinity-negative {
48+
WITH cte AS (SELECT CAST(-5 AS REAL) AS val)
49+
SELECT t2.val, typeof(t2.val)
50+
FROM cte t1 JOIN cte t2 ON t1.val = t2.val;
51+
}
52+
expect {
53+
-5.0|real
54+
}
55+
56+
test cte-real-affinity-multiple-columns {
57+
WITH cte AS (SELECT CAST(1 AS REAL) AS a, CAST(2 AS REAL) AS b)
58+
SELECT t2.a, typeof(t2.a), t2.b, typeof(t2.b)
59+
FROM cte t1 JOIN cte t2 ON t1.a = t2.a;
60+
}
61+
expect {
62+
1.0|real|2.0|real
63+
}
64+
65+
test subquery-real-affinity-join {
66+
SELECT t2.val, typeof(t2.val)
67+
FROM (SELECT CAST(1 AS REAL) AS val) t1
68+
JOIN (SELECT CAST(1 AS REAL) AS val) t2
69+
ON t1.val = t2.val;
70+
}
71+
expect {
72+
1.0|real
73+
}

testing/runner/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-multiple-independent.snap

Lines changed: 85 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -41,87 +41,88 @@ QUERY PLAN
4141
`--SEARCH l USING INDEX ephemeral_subquery_t8
4242

4343
BYTECODE
44-
addr opcode p1 p2 p3 p4 p5 comment
45-
0 Init 0 80 0 0 Start at 80
46-
1 OpenEphemeral 1 1 0 0 cursor=1 is_table=true
47-
2 OpenRead 2 4 0 k(4,B,B,B,B) 0 table=orders, root=4, iDb=0
48-
3 Rewind 2 15 0 0 Rewind table orders
49-
4 Column 2 3 8 0 r[8]=orders.total_amount
50-
5 RealAffinity 8 0 0 0
51-
6 Le 8 9 14 Binary 0 if r[8]<=r[9] goto 14
52-
7 RowId 2 4 0 0 r[4]=orders.rowid
53-
8 Column 2 1 5 0 r[5]=orders.customer_id
54-
9 Column 2 3 6 0 r[6]=orders.total_amount
55-
10 RealAffinity 6 0 0 0
56-
11 MakeRecord 4 3 10 0 r[10]=mkrec(r[4..6]); for
57-
12 NewRowid 1 11 0 0 r[11]=rowid
58-
13 Insert 1 10 11 4 intkey=r[11] data=r[10]
59-
14 Next 2 4 0 0
60-
15 InitCoroutine 12 51 16 0
61-
16 Once 27 0 0 0 goto 27
62-
17 OpenEphemeral 0 0 0 0 cursor=0 is_table=false
63-
18 OpenDup 3 1 0 0 new_cursor=3, original_cursor=1
64-
19 Rewind 3 27 0 0 Rewind
65-
20 Column 3 0 13 0 r[13]=.column 0
66-
21 Column 3 1 14 0 r[14]=.column 1
67-
22 Column 3 2 15 0 r[15]=.column 2
68-
23 Copy 14 16 0 0 r[16]=r[14]
69-
24 MakeRecord 16 1 17 0 r[17]=mkrec(r[16..16]); for ephemeral_index_where_sub_t4
70-
25 IdxInsert 0 17 0 8 key=r[17]
71-
26 Next 3 20 0 0
72-
27 OpenRead 4 6 0 k(4,B,B,B,B) 0 table=customers, root=6, iDb=0
73-
28 Rewind 4 50 0 0 Rewind table customers
74-
29 Integer 0 20 0 0 r[20]=0
75-
30 RowId 4 21 0 0 r[21]=customers.rowid
76-
31 Affinity 21 1 0 0 r[21..22] = C
77-
32 NotFound 0 34 21 0 if not found goto 34
78-
33 Goto 0 40 0 0
79-
34 Rewind 0 42 0 0 Rewind ephemeral_index_where_sub_t4
80-
35 Column 0 0 22 0 r[22]=ephemeral_index_where_sub_t4.customer_id
81-
36 Ne 21 22 38 Binary 0 if r[21]!=r[22] goto 38
82-
37 Goto 0 44 0 0
83-
38 Next 0 35 0 0
84-
39 Goto 0 42 0 0
85-
40 Integer 1 20 0 0 r[20]=1
86-
41 Goto 0 45 0 0
87-
42 Integer 0 20 0 0 r[20]=0
88-
43 Goto 0 45 0 0
89-
44 Null 0 20 0 0 r[20]=NULL
90-
45 IfNot 20 49 1 0 if !r[20] goto 49
91-
46 RowId 4 18 0 0 r[18]=customers.rowid
92-
47 Column 4 1 19 0 r[19]=customers.name
93-
48 Yield 12 0 0 0
94-
49 Next 4 29 0 0
95-
50 EndCoroutine 12 0 0 0
96-
51 OpenDup 5 1 0 0 new_cursor=5, original_cursor=1
97-
52 OpenEphemeral 6 0 0 0 cursor=6 is_table=false
98-
53 Rewind 5 64 0 0 Rewind
99-
54 Column 5 0 24 0 r[24]=.column 0
100-
55 Column 5 1 25 0 r[25]=.column 1
101-
56 Column 5 2 26 0 r[26]=.column 2
102-
57 Copy 25 27 0 0 r[27]=r[25]
103-
58 Copy 24 28 0 0 r[28]=r[24]
104-
59 Copy 26 29 0 0 r[29]=r[26]
105-
60 RowId 5 30 0 0 r[30]=.rowid
106-
61 MakeRecord 27 4 23 0 r[23]=mkrec(r[27..30]); for ephemeral_subquery_t8
107-
62 IdxInsert 6 23 27 0 key=r[23]
108-
63 Next 5 54 0 0
109-
64 InitCoroutine 12 0 16 0
110-
65 Yield 12 79 0 0
111-
66 Copy 18 36 0 0 r[36]=r[18]
112-
67 IsNull 36 78 0 0 if (r[36]==NULL) goto 78
113-
68 Affinity 36 1 0 0 r[36..37] = D
114-
69 SeekGE 6 78 36 0 key=[36..36]
115-
70 IdxGT 6 78 36 0 key=[36..36]
116-
71 Column 6 0 31 0 r[31]=ephemeral_subquery_t8.customer_id
117-
72 Column 6 1 32 0 r[32]=ephemeral_subquery_t8.id
118-
73 Column 6 2 33 0 r[33]=ephemeral_subquery_t8.total_amount
119-
74 Copy 19 34 0 0 r[34]=r[19]
120-
75 Column 6 2 35 0 r[35]=ephemeral_subquery_t8.total_amount
121-
76 ResultRow 34 2 0 0 output=r[34..35]
122-
77 Next 6 70 0 0
123-
78 Goto 0 65 0 0
124-
79 Halt 0 0 0 0
125-
80 Transaction 0 1 11 0 iDb=0 tx_mode=Read
126-
81 Integer 1000 9 0 0 r[9]=1000
127-
82 Goto 0 1 0 0
44+
addr opcode p1 p2 p3 p4 p5 comment
45+
0 Init 0 81 0 0 Start at 81
46+
1 OpenEphemeral 1 1 0 0 cursor=1 is_table=true
47+
2 OpenRead 2 4 0 k(4,B,B,B,B) 0 table=orders, root=4, iDb=0
48+
3 Rewind 2 15 0 0 Rewind table orders
49+
4 Column 2 3 8 0 r[8]=orders.total_amount
50+
5 RealAffinity 8 0 0 0
51+
6 Le 8 9 14 Binary 0 if r[8]<=r[9] goto 14
52+
7 RowId 2 4 0 0 r[4]=orders.rowid
53+
8 Column 2 1 5 0 r[5]=orders.customer_id
54+
9 Column 2 3 6 0 r[6]=orders.total_amount
55+
10 RealAffinity 6 0 0 0
56+
11 MakeRecord 4 3 10 0 r[10]=mkrec(r[4..6]); for
57+
12 NewRowid 1 11 0 0 r[11]=rowid
58+
13 Insert 1 10 11 4 intkey=r[11] data=r[10]
59+
14 Next 2 4 0 0
60+
15 InitCoroutine 12 51 16 0
61+
16 Once 27 0 0 0 goto 27
62+
17 OpenEphemeral 0 0 0 0 cursor=0 is_table=false
63+
18 OpenDup 3 1 0 0 new_cursor=3, original_cursor=1
64+
19 Rewind 3 27 0 0 Rewind
65+
20 Column 3 0 13 0 r[13]=.column 0
66+
21 Column 3 1 14 0 r[14]=.column 1
67+
22 Column 3 2 15 0 r[15]=.column 2
68+
23 Copy 14 16 0 0 r[16]=r[14]
69+
24 MakeRecord 16 1 17 0 r[17]=mkrec(r[16..16]); for ephemeral_index_where_sub_t4
70+
25 IdxInsert 0 17 0 8 key=r[17]
71+
26 Next 3 20 0 0
72+
27 OpenRead 4 6 0 k(4,B,B,B,B) 0 table=customers, root=6, iDb=0
73+
28 Rewind 4 50 0 0 Rewind table customers
74+
29 Integer 0 20 0 0 r[20]=0
75+
30 RowId 4 21 0 0 r[21]=customers.rowid
76+
31 Affinity 21 1 0 0 r[21..22] = C
77+
32 NotFound 0 34 21 0 if not found goto 34
78+
33 Goto 0 40 0 0
79+
34 Rewind 0 42 0 0 Rewind ephemeral_index_where_sub_t4
80+
35 Column 0 0 22 0 r[22]=ephemeral_index_where_sub_t4.customer_id
81+
36 Ne 21 22 38 Binary 0 if r[21]!=r[22] goto 38
82+
37 Goto 0 44 0 0
83+
38 Next 0 35 0 0
84+
39 Goto 0 42 0 0
85+
40 Integer 1 20 0 0 r[20]=1
86+
41 Goto 0 45 0 0
87+
42 Integer 0 20 0 0 r[20]=0
88+
43 Goto 0 45 0 0
89+
44 Null 0 20 0 0 r[20]=NULL
90+
45 IfNot 20 49 1 0 if !r[20] goto 49
91+
46 RowId 4 18 0 0 r[18]=customers.rowid
92+
47 Column 4 1 19 0 r[19]=customers.name
93+
48 Yield 12 0 0 0
94+
49 Next 4 29 0 0
95+
50 EndCoroutine 12 0 0 0
96+
51 OpenDup 5 1 0 0 new_cursor=5, original_cursor=1
97+
52 OpenEphemeral 6 0 0 0 cursor=6 is_table=false
98+
53 Rewind 5 64 0 0 Rewind
99+
54 Column 5 0 24 0 r[24]=.column 0
100+
55 Column 5 1 25 0 r[25]=.column 1
101+
56 Column 5 2 26 0 r[26]=.column 2
102+
57 Copy 25 27 0 0 r[27]=r[25]
103+
58 Copy 24 28 0 0 r[28]=r[24]
104+
59 Copy 26 29 0 0 r[29]=r[26]
105+
60 RowId 5 30 0 0 r[30]=.rowid
106+
61 MakeRecord 27 4 23 0 r[23]=mkrec(r[27..30]); for ephemeral_subquery_t8
107+
62 IdxInsert 6 23 27 0 key=r[23]
108+
63 Next 5 54 0 0
109+
64 InitCoroutine 12 0 16 0
110+
65 Yield 12 80 0 0
111+
66 Copy 18 36 0 0 r[36]=r[18]
112+
67 IsNull 36 79 0 0 if (r[36]==NULL) goto 79
113+
68 Affinity 36 1 0 0 r[36..37] = D
114+
69 SeekGE 6 79 36 0 key=[36..36]
115+
70 IdxGT 6 79 36 0 key=[36..36]
116+
71 Column 6 0 31 0 r[31]=ephemeral_subquery_t8.customer_id
117+
72 Column 6 1 32 0 r[32]=ephemeral_subquery_t8.id
118+
73 Column 6 2 33 0 r[33]=ephemeral_subquery_t8.total_amount
119+
74 Copy 19 34 0 0 r[34]=r[19]
120+
75 Column 6 2 35 0 r[35]=ephemeral_subquery_t8.total_amount
121+
76 RealAffinity 35 0 0 0
122+
77 ResultRow 34 2 0 0 output=r[34..35]
123+
78 Next 6 70 0 0
124+
79 Goto 0 65 0 0
125+
80 Halt 0 0 0 0
126+
81 Transaction 0 1 11 0 iDb=0 tx_mode=Read
127+
82 Integer 1000 9 0 0 r[9]=1000
128+
83 Goto 0 1 0 0

0 commit comments

Comments
 (0)