join fix and runner update to support multiple output files

main
casey 2026-06-13 08:40:01 +10:00
parent ab3d7ab971
commit dd1431760a
7 changed files with 1717 additions and 1709 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
# Alteryx example files
Alteryx_TestWorkflows/JoinTesting/Output/
!Alteryx_TestWorkflows/JoinTesting/Output/**/
# uv
uv.lock

File diff suppressed because it is too large Load Diff

View File

@ -1,226 +1,226 @@
Left_Store_ID,Product_ID,Left_Stock_On_Hand
15,31,4
15,32,16
15,33,8
15,34,7
16,31,14
16,32,7
16,33,6
16,34,2
16,35,6
17,31,20
17,32,15
17,33,27
17,34,11
18,31,4
18,32,9
18,33,9
18,34,8
18,35,10
19,31,4
19,32,5
19,33,0
19,34,15
19,35,14
20,31,10
20,32,9
20,33,28
20,34,19
21,31,19
21,32,3
21,33,16
21,34,16
22,31,34
22,32,38
22,33,8
22,34,6
22,35,2
23,31,19
23,32,11
23,33,6
23,34,18
23,35,4
24,31,10
24,32,10
24,33,4
24,34,17
24,35,19
25,31,0
25,32,10
25,33,4
25,34,23
26,31,4
26,32,2
26,33,2
26,34,17
26,35,8
27,31,13
27,32,6
27,33,7
27,34,9
28,31,18
28,32,3
28,33,9
28,34,19
29,31,3
29,32,7
29,33,6
29,34,16
30,31,20
30,32,13
30,33,10
30,34,18
31,31,39
31,32,12
31,33,20
31,34,20
32,31,4
32,32,8
32,33,13
32,34,20
33,31,7
33,32,15
33,33,9
33,34,14
33,35,18
34,31,30
34,32,19
34,33,9
34,34,17
34,35,20
35,31,74
35,32,20
35,33,14
35,34,9
36,31,6
36,32,7
36,33,21
36,34,2
36,35,12
37,31,14
37,32,0
37,33,10
37,34,13
37,35,14
38,31,17
38,32,20
38,33,9
38,34,18
38,35,2
39,31,15
39,32,5
39,33,14
39,34,4
40,31,5
40,32,7
40,33,16
40,34,5
41,31,18
41,32,29
41,33,13
41,34,15
41,35,10
1,31,7
1,32,4
1,33,2
1,34,0
1,35,12
2,31,18
2,32,10
2,33,11
2,34,18
3,31,29
3,32,4
3,33,4
3,34,7
4,31,35
4,32,6
4,33,2
4,34,0
4,35,4
5,31,31
5,32,10
5,33,17
5,34,10
6,31,17
6,32,7
6,33,7
6,34,8
6,35,3
7,31,15
7,32,3
7,33,18
7,34,2
7,35,17
8,31,27
8,32,7
8,33,17
8,34,18
8,35,8
9,31,6
9,32,3
9,33,9
9,34,5
9,35,4
10,31,7
10,32,13
10,33,12
10,34,16
10,35,2
11,31,20
11,32,4
11,33,6
11,34,9
12,31,13
12,32,9
12,33,5
12,34,9
12,35,9
13,31,24
13,32,7
13,33,3
13,34,3
14,31,5
14,32,2
14,33,2
14,34,8
42,31,11
42,32,4
42,33,18
42,34,34
42,35,13
43,31,18
43,32,38
43,33,5
43,34,7
44,31,8
44,32,29
44,33,0
44,34,22
45,31,6
45,32,6
45,33,7
45,34,3
46,31,13
46,32,8
46,33,11
46,34,24
47,31,48
47,32,6
47,33,13
47,34,3
48,31,41
48,32,7
48,33,0
48,34,39
48,35,3
49,31,51
49,32,11
49,33,15
49,34,2
49,35,19
50,31,18
50,32,9
50,33,1
50,34,17
50,35,8
Store_ID,Product_ID,Stock_On_Hand
15,31,4
15,32,16
15,33,8
15,34,7
16,31,14
16,32,7
16,33,6
16,34,2
16,35,6
17,31,20
17,32,15
17,33,27
17,34,11
18,31,4
18,32,9
18,33,9
18,34,8
18,35,10
19,31,4
19,32,5
19,33,0
19,34,15
19,35,14
20,31,10
20,32,9
20,33,28
20,34,19
21,31,19
21,32,3
21,33,16
21,34,16
22,31,34
22,32,38
22,33,8
22,34,6
22,35,2
23,31,19
23,32,11
23,33,6
23,34,18
23,35,4
24,31,10
24,32,10
24,33,4
24,34,17
24,35,19
25,31,0
25,32,10
25,33,4
25,34,23
26,31,4
26,32,2
26,33,2
26,34,17
26,35,8
27,31,13
27,32,6
27,33,7
27,34,9
28,31,18
28,32,3
28,33,9
28,34,19
42,31,11
42,32,4
42,33,18
42,34,34
42,35,13
43,31,18
43,32,38
43,33,5
43,34,7
44,31,8
44,32,29
44,33,0
44,34,22
45,31,6
45,32,6
45,33,7
45,34,3
46,31,13
46,32,8
46,33,11
46,34,24
47,31,48
47,32,6
47,33,13
47,34,3
48,31,41
48,32,7
48,33,0
48,34,39
48,35,3
49,31,51
49,32,11
49,33,15
49,34,2
49,35,19
50,31,18
50,32,9
50,33,1
50,34,17
50,35,8
29,31,3
29,32,7
29,33,6
29,34,16
30,31,20
30,32,13
30,33,10
30,34,18
31,31,39
31,32,12
31,33,20
31,34,20
32,31,4
32,32,8
32,33,13
32,34,20
33,31,7
33,32,15
33,33,9
33,34,14
33,35,18
34,31,30
34,32,19
34,33,9
34,34,17
34,35,20
35,31,74
35,32,20
35,33,14
35,34,9
36,31,6
36,32,7
36,33,21
36,34,2
36,35,12
37,31,14
37,32,0
37,33,10
37,34,13
37,35,14
38,31,17
38,32,20
38,33,9
38,34,18
38,35,2
39,31,15
39,32,5
39,33,14
39,34,4
40,31,5
40,32,7
40,33,16
40,34,5
41,31,18
41,32,29
41,33,13
41,34,15
41,35,10
1,31,7
1,32,4
1,33,2
1,34,0
1,35,12
2,31,18
2,32,10
2,33,11
2,34,18
3,31,29
3,32,4
3,33,4
3,34,7
4,31,35
4,32,6
4,33,2
4,34,0
4,35,4
5,31,31
5,32,10
5,33,17
5,34,10
6,31,17
6,32,7
6,33,7
6,34,8
6,35,3
7,31,15
7,32,3
7,33,18
7,34,2
7,35,17
8,31,27
8,32,7
8,33,17
8,34,18
8,35,8
9,31,6
9,32,3
9,33,9
9,34,5
9,35,4
10,31,7
10,32,13
10,33,12
10,34,16
10,35,2
11,31,20
11,32,4
11,33,6
11,34,9
12,31,13
12,32,9
12,33,5
12,34,9
12,35,9
13,31,24
13,32,7
13,33,3
13,34,3
14,31,5
14,32,2
14,33,2
14,34,8

1 Left_Store_ID Store_ID Product_ID Left_Stock_On_Hand Stock_On_Hand
2 15 31 4
3 15 32 16
4 15 33 8
5 15 34 7
6 16 31 14
7 16 32 7
8 16 33 6
9 16 34 2
10 16 35 6
11 17 31 20
12 17 32 15
13 17 33 27
14 17 34 11
15 18 31 4
16 18 32 9
17 18 33 9
18 18 34 8
19 18 35 10
20 19 31 4
21 19 32 5
22 19 33 0
23 19 34 15
24 19 35 14
25 20 31 10
26 20 32 9
27 20 33 28
28 20 34 19
29 21 31 19
30 21 32 3
31 21 33 16
32 21 34 16
33 22 31 34
34 22 32 38
35 22 33 8
36 22 34 6
37 22 35 2
38 23 31 19
39 23 32 11
40 23 33 6
41 23 34 18
42 23 35 4
43 24 31 10
44 24 32 10
45 24 33 4
46 24 34 17
47 24 35 19
48 25 31 0
49 25 32 10
50 25 33 4
51 25 34 23
52 26 31 4
53 26 32 2
54 26 33 2
55 26 34 17
56 26 35 8
57 27 31 13
58 27 32 6
59 27 33 7
60 27 34 9
61 28 31 18
62 28 32 3
63 28 33 9
64 28 34 19
65 29 42 31 3 11
66 29 42 32 7 4
67 29 42 33 6 18
68 29 42 34 16 34
69 30 42 31 35 20 13
70 30 43 32 31 13 18
71 30 43 33 32 10 38
72 30 43 34 33 18 5
73 31 43 31 34 39 7
74 31 44 32 31 12 8
75 31 44 33 32 20 29
76 31 44 34 33 20 0
77 32 44 31 34 4 22
78 32 45 32 31 8 6
79 32 45 33 32 13 6
80 32 45 34 33 20 7
81 33 45 31 34 7 3
82 33 46 32 31 15 13
83 33 46 33 32 9 8
84 33 46 34 33 14 11
85 33 46 35 34 18 24
86 34 47 31 30 48
87 34 47 32 19 6
88 34 47 33 9 13
89 34 47 34 17 3
90 34 48 35 31 20 41
91 35 48 31 32 74 7
92 35 48 32 33 20 0
93 35 48 33 34 14 39
94 35 48 34 35 9 3
95 36 49 31 6 51
96 36 49 32 7 11
97 36 49 33 21 15
98 36 49 34 2
99 36 49 35 12 19
100 37 50 31 14 18
101 37 50 32 0 9
102 37 50 33 10 1
103 37 50 34 13 17
104 37 50 35 14 8
105 38 29 31 17 3
106 38 29 32 20 7
107 38 29 33 9 6
108 38 29 34 18 16
109 38 30 35 31 2 20
110 39 30 31 32 15 13
111 39 30 32 33 5 10
112 39 30 33 34 14 18
113 39 31 34 31 4 39
114 40 31 31 32 5 12
115 40 31 32 33 7 20
116 40 31 33 34 16 20
117 40 32 34 31 5 4
118 41 32 31 32 18 8
119 41 32 32 33 29 13
120 41 32 33 34 13 20
121 41 33 34 31 15 7
122 41 33 35 32 10 15
123 1 33 31 33 7 9
124 1 33 32 34 4 14
125 1 33 33 35 2 18
126 1 34 34 31 0 30
127 1 34 35 32 12 19
128 2 34 31 33 18 9
129 2 34 32 34 10 17
130 2 34 33 35 11 20
131 2 35 34 31 18 74
132 3 35 31 32 29 20
133 3 35 32 33 4 14
134 3 35 33 34 4 9
135 3 36 34 31 7 6
136 4 36 31 32 35 7
137 4 36 32 33 6 21
138 4 36 33 34 2
139 4 36 34 35 0 12
140 4 37 35 31 4 14
141 5 37 31 32 31 0
142 5 37 32 33 10
143 5 37 33 34 17 13
144 5 37 34 35 10 14
145 6 38 31 17
146 6 38 32 7 20
147 6 38 33 7 9
148 6 38 34 8 18
149 6 38 35 3 2
150 7 39 31 15
151 7 39 32 3 5
152 7 39 33 18 14
153 7 39 34 2 4
154 7 40 35 31 17 5
155 8 40 31 32 27 7
156 8 40 32 33 7 16
157 8 40 33 34 17 5
158 8 41 34 31 18
159 8 41 35 32 8 29
160 9 41 31 33 6 13
161 9 41 32 34 3 15
162 9 41 33 35 9 10
163 9 1 34 31 5 7
164 9 1 35 32 4
165 10 1 31 33 7 2
166 10 1 32 34 13 0
167 10 1 33 35 12
168 10 2 34 31 16 18
169 10 2 35 32 2 10
170 11 2 31 33 20 11
171 11 2 32 34 4 18
172 11 3 33 31 6 29
173 11 3 34 32 9 4
174 12 3 31 33 13 4
175 12 3 32 34 9 7
176 12 4 33 31 5 35
177 12 4 34 32 9 6
178 12 4 35 33 9 2
179 13 4 31 34 24 0
180 13 4 32 35 7 4
181 13 5 33 31 3 31
182 13 5 34 32 3 10
183 14 5 31 33 5 17
184 14 5 32 34 2 10
185 14 6 33 31 2 17
186 14 6 34 32 8 7
187 42 6 31 33 11 7
188 42 6 32 34 4 8
189 42 6 33 35 18 3
190 42 7 34 31 34 15
191 42 7 35 32 13 3
192 43 7 31 33 18
193 43 7 32 34 38 2
194 43 7 33 35 5 17
195 43 8 34 31 7 27
196 44 8 31 32 8 7
197 44 8 32 33 29 17
198 44 8 33 34 0 18
199 44 8 34 35 22 8
200 45 9 31 6
201 45 9 32 6 3
202 45 9 33 7 9
203 45 9 34 3 5
204 46 9 31 35 13 4
205 46 10 32 31 8 7
206 46 10 33 32 11 13
207 46 10 34 33 24 12
208 47 10 31 34 48 16
209 47 10 32 35 6 2
210 47 11 33 31 13 20
211 47 11 34 32 3 4
212 48 11 31 33 41 6
213 48 11 32 34 7 9
214 48 12 33 31 0 13
215 48 12 34 32 39 9
216 48 12 35 33 3 5
217 49 12 31 34 51 9
218 49 12 32 35 11 9
219 49 13 33 31 15 24
220 49 13 34 32 2 7
221 49 13 35 33 19 3
222 50 13 31 34 18 3
223 50 14 32 31 9 5
224 50 14 33 32 1 2
225 50 14 34 33 17 2
226 50 14 35 34 8

View File

@ -1,2 +1,2 @@
Product_ID,Right_Product_Name,Right_Product_Category,Right_Product_Cost,Right_Product_Price
100,Non-product,NoCat,$1,$1
Product_ID,Product_Name,Product_Category,Product_Cost,Product_Price
100,Non-product,NoCat,$1,$1

1 Product_ID Right_Product_Name Product_Name Right_Product_Category Product_Category Right_Product_Cost Product_Cost Right_Product_Price Product_Price
2 100 Non-product Non-product NoCat NoCat $1 $1 $1 $1

View File

@ -1,32 +1,32 @@
Product_ID,Product_Name,Product_Category,Product_Cost,Product_Price
1,Action Figure,Toys,$9.99,$15.99
2,Animal Figures,Toys,$9.99,$12.99
3,Barrel O' Slime,Art & Crafts,$1.99,$3.99
4,Chutes & Ladders,Games,$9.99,$12.99
5,Classic Dominoes,Games,$7.99,$9.99
6,Colorbuds,Electronics,$6.99,$14.99
7,Dart Gun,Sports & Outdoors,$11.99,$15.99
8,Deck Of Cards,Games,$3.99,$6.99
9,Dino Egg,Toys,$9.99,$10.99
10,Dinosaur Figures,Toys,$10.99,$14.99
11,Etch A Sketch,Art & Crafts,$10.99,$20.99
12,Foam Disk Launcher,Sports & Outdoors,$8.99,$11.99
13,Gamer Headphones,Electronics,$14.99,$20.99
14,Glass Marbles,Games,$5.99,$10.99
15,Hot Wheels 5-Pack,Toys,$3.99,$5.99
16,Jenga,Games,$2.99,$9.99
17,Kids Makeup Kit,Art & Crafts,$13.99,$19.99
18,Lego Bricks,Toys,$34.99,$39.99
19,Magic Sand,Art & Crafts,$13.99,$15.99
20,Mini Basketball Hoop,Sports & Outdoors,$8.99,$24.99
21,Mini Ping Pong Set,Sports & Outdoors,$6.99,$9.99
22,Monopoly,Games,$13.99,$19.99
23,Mr. Potatohead,Toys,$4.99,$9.99
24,Nerf Gun,Sports & Outdoors,$14.99,$19.99
25,PlayDoh Can,Art & Crafts,$1.99,$2.99
26,PlayDoh Playset,Art & Crafts,$20.99,$24.99
27,PlayDoh Toolkit,Art & Crafts,$3.99,$4.99
28,Playfoam,Art & Crafts,$3.99,$10.99
29,Plush Pony,Toys,$8.99,$19.99
30,Rubik's Cube,Games,$17.99,$19.99
100,Non-product,NoCat,$1,$1
Product_ID,Product_Name,Product_Category,Product_Cost,Product_Price
1,Action Figure,Toys,$9.99,$15.99
2,Animal Figures,Toys,$9.99,$12.99
3,Barrel O' Slime,Art & Crafts,$1.99,$3.99
4,Chutes & Ladders,Games,$9.99,$12.99
5,Classic Dominoes,Games,$7.99,$9.99
6,Colorbuds,Electronics,$6.99,$14.99
7,Dart Gun,Sports & Outdoors,$11.99,$15.99
8,Deck Of Cards,Games,$3.99,$6.99
9,Dino Egg,Toys,$9.99,$10.99
10,Dinosaur Figures,Toys,$10.99,$14.99
11,Etch A Sketch,Art & Crafts,$10.99,$20.99
12,Foam Disk Launcher,Sports & Outdoors,$8.99,$11.99
13,Gamer Headphones,Electronics,$14.99,$20.99
14,Glass Marbles,Games,$5.99,$10.99
15,Hot Wheels 5-Pack,Toys,$3.99,$5.99
16,Jenga,Games,$2.99,$9.99
17,Kids Makeup Kit,Art & Crafts,$13.99,$19.99
18,Lego Bricks,Toys,$34.99,$39.99
19,Magic Sand,Art & Crafts,$13.99,$15.99
20,Mini Basketball Hoop,Sports & Outdoors,$8.99,$24.99
21,Mini Ping Pong Set,Sports & Outdoors,$6.99,$9.99
22,Monopoly,Games,$13.99,$19.99
23,Mr. Potatohead,Toys,$4.99,$9.99
24,Nerf Gun,Sports & Outdoors,$14.99,$19.99
25,PlayDoh Can,Art & Crafts,$1.99,$2.99
26,PlayDoh Playset,Art & Crafts,$20.99,$24.99
27,PlayDoh Toolkit,Art & Crafts,$3.99,$4.99
28,Playfoam,Art & Crafts,$3.99,$10.99
29,Plush Pony,Toys,$8.99,$19.99
30,Rubik's Cube,Games,$17.99,$19.99
100,Non-product,NoCat,$1,$1

1 Product_ID Product_Name Product_Category Product_Cost Product_Price
2 1 Action Figure Toys $9.99 $15.99
3 2 Animal Figures Toys $9.99 $12.99
4 3 Barrel O' Slime Art & Crafts $1.99 $3.99
5 4 Chutes & Ladders Games $9.99 $12.99
6 5 Classic Dominoes Games $7.99 $9.99
7 6 Colorbuds Electronics $6.99 $14.99
8 7 Dart Gun Sports & Outdoors $11.99 $15.99
9 8 Deck Of Cards Games $3.99 $6.99
10 9 Dino Egg Toys $9.99 $10.99
11 10 Dinosaur Figures Toys $10.99 $14.99
12 11 Etch A Sketch Art & Crafts $10.99 $20.99
13 12 Foam Disk Launcher Sports & Outdoors $8.99 $11.99
14 13 Gamer Headphones Electronics $14.99 $20.99
15 14 Glass Marbles Games $5.99 $10.99
16 15 Hot Wheels 5-Pack Toys $3.99 $5.99
17 16 Jenga Games $2.99 $9.99
18 17 Kids Makeup Kit Art & Crafts $13.99 $19.99
19 18 Lego Bricks Toys $34.99 $39.99
20 19 Magic Sand Art & Crafts $13.99 $15.99
21 20 Mini Basketball Hoop Sports & Outdoors $8.99 $24.99
22 21 Mini Ping Pong Set Sports & Outdoors $6.99 $9.99
23 22 Monopoly Games $13.99 $19.99
24 23 Mr. Potatohead Toys $4.99 $9.99
25 24 Nerf Gun Sports & Outdoors $14.99 $19.99
26 25 PlayDoh Can Art & Crafts $1.99 $2.99
27 26 PlayDoh Playset Art & Crafts $20.99 $24.99
28 27 PlayDoh Toolkit Art & Crafts $3.99 $4.99
29 28 Playfoam Art & Crafts $3.99 $10.99
30 29 Plush Pony Toys $8.99 $19.99
31 30 Rubik's Cube Games $17.99 $19.99
32 100 Non-product NoCat $1 $1

View File

@ -74,7 +74,10 @@ class OutputDataTool(BaseTool):
or "True"
)
header = header_val.lower() != "false"
df.write_csv(str(path), separator=delim, include_header=header)
line_end = (opts.findtext("LineEndStyle") or "LF").strip().upper()
eol = "\r\n" if line_end == "CRLF" else "\n"
df.write_csv(str(path), separator=delim, include_header=header,
line_terminator=eol)
elif fmt == 25: # Excel
df.write_excel(str(path))
elif fmt == 2: # Parquet

View File

@ -66,74 +66,80 @@ class JoinTool(BaseTool):
"""Apply field selection and renaming from SelectConfiguration."""
if df.is_empty() or self.config is None:
return df
select_config = self.config.find("SelectConfiguration")
if select_config is None:
return df
# Find the Configuration for this output connection
for cfg in select_config.findall("Configuration"):
if cfg.attrib.get("outputConnection") == output_connection:
select_fields = cfg.find("SelectFields")
if select_fields is None:
return df
# Build column mapping
# First, collect explicitly selected fields
explicit_selections = [] # list of (src_col, output_name)
order_changed_el = cfg.find("OrderChanged")
order_changed = (
order_changed_el is not None
and order_changed_el.attrib.get("value", "False") == "True"
)
# Parse field rules
rename_map: dict[str, str] = {} # src_col → output_name
exclude_set: set[str] = set() # columns explicitly excluded
explicit_order: list[str] = [] # for OrderChanged=True
has_unknown = False
unknown_selected = True
for sf in select_fields.findall("SelectField"):
field = sf.attrib.get("field", "")
selected = sf.attrib.get("selected", "False") == "True"
selected = sf.attrib.get("selected", "True") == "True"
rename = sf.attrib.get("rename", "")
input_prefix = sf.attrib.get("input", "")
if not selected:
continue
if field == "*Unknown":
has_unknown = True
unknown_selected = selected
continue
# Resolve column name in the DataFrame
if field not in df.columns:
continue
if not selected:
exclude_set.add(field)
else:
# Find the column with prefix
src_col = f"{input_prefix}{field}" if input_prefix else field
if src_col in df.columns:
output_name = rename if rename else field
explicit_selections.append((src_col, output_name))
elif field in df.columns:
output_name = rename if rename else field
explicit_selections.append((field, output_name))
explicit_order.append(field)
if rename and rename != field:
rename_map[field] = rename
# Build final column list
selected_cols = []
rename_map = {}
# Add explicitly selected columns
for src, dst in explicit_selections:
selected_cols.append(src)
if src != dst:
rename_map[src] = dst
# Handle *Unknown: include all remaining columns, stripping prefixes
if has_unknown:
explicit_srcs = {src for src, _ in explicit_selections}
mentioned = set(explicit_order) | exclude_set
if order_changed:
# Explicit selections first (in specified order), then *Unknown
final_cols = list(explicit_order)
if has_unknown and unknown_selected:
for col in df.columns:
if col not in mentioned:
final_cols.append(col)
else:
# Preserve original DataFrame column order
final_cols = []
for col in df.columns:
if col not in explicit_srcs:
# Strip Left_/Right_ prefix for output name
output_name = col
if col.startswith("Left_"):
output_name = col[5:]
elif col.startswith("Right_"):
output_name = col[6:]
selected_cols.append(col)
if col != output_name:
rename_map[col] = output_name
# Apply selection and renaming
if selected_cols:
df = df.select(selected_cols)
if rename_map:
df = df.rename(rename_map)
if col in exclude_set:
continue
if col in mentioned or (has_unknown and unknown_selected):
final_cols.append(col)
elif not has_unknown and col not in mentioned:
# Default: include if not explicitly excluded
final_cols.append(col)
if final_cols:
df = df.select(final_cols)
if rename_map:
df = df.rename(
{k: v for k, v in rename_map.items() if k in df.columns}
)
break
return df
@ -145,45 +151,40 @@ class JoinTool(BaseTool):
) -> tuple[pl.DataFrame, pl.DataFrame, pl.DataFrame]:
con = self.ctx.duckdb_con
# Disambiguate conflicting column names
key_l = {k[0] for k in join_keys}
key_r = {k[1] for k in join_keys}
l_non_key = [c for c in left.columns if c not in key_l]
r_non_key = [c for c in right.columns if c not in key_r]
# Only right non-key columns that clash with left columns need a prefix
conflicts = set(l_non_key) & set(r_non_key)
# Prefix all non-key columns: Left_ for left, Right_ for right
# This matches Alteryx behavior where SelectConfiguration references
# fields with these prefixes
rename_l = {c: f"Left_{c}" for c in l_non_key}
rename_r = {c: f"Right_{c}" for c in r_non_key}
# But keep join keys without prefix (they come from left)
left_r = left.rename(rename_l) if rename_l else left
right_r = right.rename(rename_r) if rename_r else right
con.register("__join_left__", left_r.to_arrow())
con.register("__join_right__", right_r.to_arrow())
# Map renamed key column names
def lk(k: str) -> str:
return rename_l.get(k, k)
def rk(k: str) -> str:
return rename_r.get(k, k)
# Register the original (un-prefixed) tables
con.register("__join_left__", left.to_arrow())
con.register("__join_right__", right.to_arrow())
on_clause = " AND ".join(
f'l."{lk(k[0])}" = r."{rk(k[1])}"' for k in join_keys
f'l."{k[0]}" = r."{k[1]}"' for k in join_keys
)
# Include right join keys with Right_ prefix for SelectConfiguration
r_key_cols_sql = ", ".join(f'r."{rk(k[1])}" AS "Right_{k[1]}"' for k in join_keys)
r_cols_sql = ", ".join(f'r."{rk(c)}"' for c in r_non_key)
if r_key_cols_sql:
r_cols_sql = f"{r_key_cols_sql}, {r_cols_sql}"
r_key0 = rk(join_keys[0][1])
l_key0 = lk(join_keys[0][0])
j_sql = f"SELECT l.*, {r_cols_sql} FROM __join_left__ l INNER JOIN __join_right__ r ON {on_clause}"
# --- Inner join SELECT ------------------------------------------------
# Left columns first (no prefix), then right join keys with Right_
# prefix, then right non-key columns (Right_ prefix only on conflicts).
l_cols_sql = ", ".join(f'l."{c}"' for c in left.columns)
r_key_cols_sql = ", ".join(
f'r."{k[1]}" AS "Right_{k[1]}"' for k in join_keys
)
r_non_key_sql = ", ".join(
f'r."{c}" AS "Right_{c}"' if c in conflicts else f'r."{c}"'
for c in r_non_key
)
j_parts = [p for p in (l_cols_sql, r_key_cols_sql, r_non_key_sql) if p]
j_select = ", ".join(j_parts)
l_key0 = join_keys[0][0]
r_key0 = join_keys[0][1]
j_sql = f"SELECT {j_select} FROM __join_left__ l INNER JOIN __join_right__ r ON {on_clause}"
# Left/right unmatched keep original column names (no prefixes)
l_sql = f'SELECT l.* FROM __join_left__ l LEFT JOIN __join_right__ r ON {on_clause} WHERE r."{r_key0}" IS NULL'
r_sql = f'SELECT r.* FROM __join_right__ r LEFT JOIN __join_left__ l ON {on_clause} WHERE l."{l_key0}" IS NULL'