@@ -117,3 +117,199 @@ end
117117 )
118118 @test persistent_id (def) == UInt128 (7 )
119119end
120+
121+ @testitem " global_ids" tags = [:ring1 , :unit ] begin
122+ using LogicalQueryProtocol: global_ids, persistent_id, LQPRelationId
123+ using LogicalQueryProtocol. relationalai. lqp. v1
124+ using ProtoBuf: OneOf
125+
126+ rid1 = RelationId (; id_low= UInt64 (10 ), id_high= UInt64 (0 ))
127+ rid2 = RelationId (; id_low= UInt64 (20 ), id_high= UInt64 (0 ))
128+ rid3 = RelationId (; id_low= UInt64 (30 ), id_high= UInt64 (0 ))
129+ t1 = var"#Type" (; var"#type" = OneOf (:int_type , IntType ()))
130+
131+ # Def declaration
132+ def = Def (;
133+ name= rid1,
134+ body= Abstraction (; vars= Binding[], value= Formula ()),
135+ )
136+ decl_def = Declaration (; declaration_type= OneOf (:def , def))
137+ @test global_ids (decl_def) == [persistent_id (rid1)]
138+
139+ # Constraint declaration
140+ fd = FunctionalDependency (; keys= Var[], values= Var[])
141+ constraint = Constraint (;
142+ name= rid2,
143+ constraint_type= OneOf (:functional_dependency , fd),
144+ )
145+ decl_constraint = Declaration (; declaration_type= OneOf (:constraint , constraint))
146+ @test global_ids (decl_constraint) == [persistent_id (rid2)]
147+
148+ # EDB data declaration
149+ edb = EDB (; target_id= rid1)
150+ data_edb = Data (; data_type= OneOf (:edb , edb))
151+ decl_edb = Declaration (; declaration_type= OneOf (:data , data_edb))
152+ @test global_ids (decl_edb) == [persistent_id (rid1)]
153+
154+ # CSVData declaration with multiple columns
155+ col1 = GNFColumn (; column_path= [" a" ], target_id= rid1, types= [t1])
156+ col2 = GNFColumn (; column_path= [" b" ], target_id= rid2, types= [t1])
157+ csv = CSVData (; columns= [col1, col2])
158+ data_csv = Data (; data_type= OneOf (:csv_data , csv))
159+ @test global_ids (data_csv) == [persistent_id (rid1), persistent_id (rid2)]
160+
161+ # CSVData skips columns without target_id
162+ col_no_target = GNFColumn (; column_path= [" c" ], types= [t1])
163+ csv2 = CSVData (; columns= [col1, col_no_target, col2])
164+ data_csv2 = Data (; data_type= OneOf (:csv_data , csv2))
165+ @test global_ids (data_csv2) == [persistent_id (rid1), persistent_id (rid2)]
166+
167+ # IcebergData declaration with multiple columns
168+ loc = IcebergLocator (; table_name= " t" , namespace= [" n" ], warehouse= " w" )
169+ cfg = IcebergCatalogConfig (;
170+ catalog_uri= " uri" , scope= " " , properties= Dict (), auth_properties= Dict (),
171+ )
172+ icol1 = GNFColumn (; column_path= [" x" ], target_id= rid1, types= [t1])
173+ icol2 = GNFColumn (; column_path= [" y" ], target_id= rid2, types= [t1])
174+ icol3 = GNFColumn (; column_path= [" z" ], target_id= rid3, types= [t1])
175+ iceberg = IcebergData (;
176+ locator= loc, config= cfg, columns= [icol1, icol2, icol3],
177+ from_snapshot= " s1" , to_snapshot= " s2" , returns_delta= false ,
178+ )
179+ data_iceberg = Data (; data_type= OneOf (:iceberg_data , iceberg))
180+ @test global_ids (data_iceberg) == [
181+ persistent_id (rid1), persistent_id (rid2), persistent_id (rid3),
182+ ]
183+
184+ # IcebergData skips columns without target_id
185+ icol_no_target = GNFColumn (; column_path= [" w" ], types= [t1])
186+ iceberg2 = IcebergData (;
187+ locator= loc, config= cfg, columns= [icol1, icol_no_target, icol3],
188+ from_snapshot= " " , to_snapshot= " " , returns_delta= true ,
189+ )
190+ data_iceberg2 = Data (; data_type= OneOf (:iceberg_data , iceberg2))
191+ @test global_ids (data_iceberg2) == [persistent_id (rid1), persistent_id (rid3)]
192+
193+ # Data routed through Declaration
194+ decl_iceberg = Declaration (; declaration_type= OneOf (:data , data_iceberg))
195+ @test global_ids (decl_iceberg) == [
196+ persistent_id (rid1), persistent_id (rid2), persistent_id (rid3),
197+ ]
198+ end
199+
200+ @testitem " collect_demanded_relations" tags = [:ring1 , :unit ] begin
201+ using LogicalQueryProtocol: collect_demanded_relations, persistent_id, LQPRelationId
202+ using LogicalQueryProtocol. relationalai. lqp. v1
203+ using ProtoBuf: OneOf
204+
205+ rid1 = RelationId (; id_low= UInt64 (1 ), id_high= UInt64 (0 ))
206+ rid2 = RelationId (; id_low= UInt64 (2 ), id_high= UInt64 (0 ))
207+ rid3 = RelationId (; id_low= UInt64 (3 ), id_high= UInt64 (0 ))
208+ rid4 = RelationId (; id_low= UInt64 (4 ), id_high= UInt64 (0 ))
209+
210+ # Empty epoch
211+ epoch = Epoch (; writes= Write[], reads= Read[])
212+ @test collect_demanded_relations (epoch) == Set {LQPRelationId} ()
213+
214+ # Demand read
215+ demand = Demand (; relation_id= rid1)
216+ rd_demand = Read (; read_type= OneOf (:demand , demand))
217+
218+ # Output read
219+ output = Output (; name= " out" , relation_id= rid2)
220+ rd_output = Read (; read_type= OneOf (:output , output))
221+
222+ # Abort read
223+ abort = Abort (; name= " ab" , relation_id= rid3)
224+ rd_abort = Read (; read_type= OneOf (:abort , abort))
225+
226+ # Epoch with all three read types
227+ epoch_mixed = Epoch (; writes= Write[], reads= [rd_demand, rd_output, rd_abort])
228+ ids = collect_demanded_relations (epoch_mixed)
229+ @test ids == Set ([persistent_id (rid1), persistent_id (rid2), persistent_id (rid3)])
230+
231+ # Duplicate relation IDs are deduplicated
232+ demand2 = Demand (; relation_id= rid1)
233+ rd_demand2 = Read (; read_type= OneOf (:demand , demand2))
234+ epoch_dup = Epoch (; writes= Write[], reads= [rd_demand, rd_demand2])
235+ @test length (collect_demanded_relations (epoch_dup)) == 1
236+
237+ # CSV export collects column data relation IDs
238+ col1 = ExportCSVColumn (; column_name= " a" , column_data= rid1)
239+ col2 = ExportCSVColumn (; column_name= " b" , column_data= rid2)
240+ csv_config = ExportCSVConfig (; path= " /tmp/out" , data_columns= [col1, col2])
241+ export_csv = Export (; export_config= OneOf (:csv_config , csv_config))
242+ rd_export_csv = Read (; read_type= OneOf (:var"#export" , export_csv))
243+ epoch_csv = Epoch (; writes= Write[], reads= [rd_export_csv])
244+ csv_ids = collect_demanded_relations (epoch_csv)
245+ @test csv_ids == Set ([persistent_id (rid1), persistent_id (rid2)])
246+
247+ # CSV export skips columns without column_data
248+ col_no_data = ExportCSVColumn (; column_name= " c" )
249+ csv_config2 = ExportCSVConfig (; path= " /tmp/out" , data_columns= [col1, col_no_data])
250+ export_csv2 = Export (; export_config= OneOf (:csv_config , csv_config2))
251+ rd_export_csv2 = Read (; read_type= OneOf (:var"#export" , export_csv2))
252+ epoch_csv2 = Epoch (; writes= Write[], reads= [rd_export_csv2])
253+ @test collect_demanded_relations (epoch_csv2) == Set ([persistent_id (rid1)])
254+
255+ # Iceberg export collects table_def relation ID
256+ loc = IcebergLocator (; table_name= " t" , namespace= [" n" ], warehouse= " w" )
257+ cfg = IcebergCatalogConfig (;
258+ catalog_uri= " uri" , scope= " " , properties= Dict (), auth_properties= Dict (),
259+ )
260+ iceberg_config = ExportIcebergConfig (; locator= loc, config= cfg, table_def= rid4)
261+ export_iceberg = Export (; export_config= OneOf (:iceberg_config , iceberg_config))
262+ rd_export_ice = Read (; read_type= OneOf (:var"#export" , export_iceberg))
263+ epoch_ice = Epoch (; writes= Write[], reads= [rd_export_ice])
264+ @test collect_demanded_relations (epoch_ice) == Set ([persistent_id (rid4)])
265+
266+ # Export with no export_config is handled gracefully
267+ export_empty = Export ()
268+ rd_export_empty = Read (; read_type= OneOf (:var"#export" , export_empty))
269+ epoch_empty_export = Epoch (; writes= Write[], reads= [rd_export_empty])
270+ @test collect_demanded_relations (epoch_empty_export) == Set {LQPRelationId} ()
271+ end
272+
273+ @testitem " read_lqp" tags = [:ring1 , :unit ] begin
274+ using LogicalQueryProtocol: read_lqp
275+ using LogicalQueryProtocol. relationalai. lqp. v1: Transaction
276+
277+ lqp_dir = joinpath (@__DIR__ , " lqp" )
278+ @test isdir (lqp_dir)
279+
280+ txn = read_lqp (joinpath (lqp_dir, " arithmetic.lqp" ))
281+ @test txn isa Transaction
282+ @test length (txn. epochs) >= 1
283+ end
284+
285+ @testitem " read_bin" tags = [:ring1 , :unit ] begin
286+ using LogicalQueryProtocol: read_bin
287+ using LogicalQueryProtocol. relationalai. lqp. v1: Transaction
288+
289+ bin_dir = joinpath (@__DIR__ , " bin" )
290+ @test isdir (bin_dir)
291+
292+ txn = read_bin (joinpath (bin_dir, " arithmetic.bin" ))
293+ @test txn isa Transaction
294+ @test length (txn. epochs) >= 1
295+ end
296+
297+ @testitem " read_lqp and read_bin agree" tags = [:ring1 , :unit ] begin
298+ using LogicalQueryProtocol: read_lqp, read_bin
299+
300+ lqp_dir = joinpath (@__DIR__ , " lqp" )
301+ bin_dir = joinpath (@__DIR__ , " bin" )
302+
303+ lqp_files = sort (filter (f -> endswith (f, " .lqp" ), readdir (lqp_dir)))
304+ @test ! isempty (lqp_files)
305+
306+ for lqp_file in lqp_files
307+ bin_file = replace (lqp_file, " .lqp" => " .bin" )
308+ bin_path = joinpath (bin_dir, bin_file)
309+ isfile (bin_path) || continue
310+
311+ txn_lqp = read_lqp (joinpath (lqp_dir, lqp_file))
312+ txn_bin = read_bin (bin_path)
313+ @test txn_lqp == txn_bin
314+ end
315+ end
0 commit comments