Skip to content

Commit d467c84

Browse files
authored
Backport engine-side changes (#244)
1 parent a8cb9fb commit d467c84

2 files changed

Lines changed: 265 additions & 3 deletions

File tree

sdks/julia/LogicalQueryProtocol.jl/src/properties.jl

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,22 @@ function is_write_only(epoch::Epoch)
4343
end
4444

4545
"""
46-
read_transaction(path::String)::Transaction
46+
read_lqp(path::String)::Transaction
4747
48-
Utility function that reads the transaction from the given `path`.
48+
Utility function that reads the transaction from the given text `.lqp` file at `path`.
4949
"""
50-
function read_transaction(path::String)
50+
function read_lqp(path::String)
51+
contents = Base.read(path, String)
52+
txn, _ = Parser.parse(contents)
53+
return txn
54+
end
55+
56+
"""
57+
read_bin(path::String)::Transaction
58+
59+
Utility function that reads the transaction from the given binary protobuf file at `path`.
60+
"""
61+
function read_bin(path::String)
5162
bytes = IOBuffer(read(path))
5263
return ProtoBuf.decode(ProtoBuf.ProtoDecoder(bytes), Transaction)
5364
end
@@ -98,6 +109,61 @@ function is_supported_decimal_bits(bits::Integer)
98109
return bits in [8, 16, 32, 64, 128]
99110
end
100111

112+
"""
113+
collect_demanded_relations(epoch::Epoch)::Set{LQPRelationId}
114+
115+
Collect the set of relation IDs that are demanded by the reads of an epoch.
116+
"""
117+
function collect_demanded_relations(epoch::Epoch)
118+
ids = Set{LQPRelationId}()
119+
for read in epoch.reads
120+
_collect_read_ids!(ids, read)
121+
end
122+
return ids
123+
end
124+
125+
function _collect_read_ids!(ids::Set{LQPRelationId}, read::Read)
126+
isnothing(read.read_type) && return nothing
127+
if read.read_type.name == :demand
128+
_collect_read_ids!(ids, read.read_type[]::Demand)
129+
elseif read.read_type.name == :output
130+
_collect_read_ids!(ids, read.read_type[]::Output)
131+
elseif read.read_type.name == :abort
132+
_collect_read_ids!(ids, read.read_type[]::Abort)
133+
elseif read.read_type.name == :var"#export"
134+
_collect_read_ids!(ids, read.read_type[]::Export)
135+
else
136+
@assert false
137+
end
138+
return nothing
139+
end
140+
function _collect_read_ids!(ids::Set{LQPRelationId}, demand::Demand)
141+
!isnothing(demand.relation_id) && push!(ids, persistent_id(demand.relation_id))
142+
return nothing
143+
end
144+
function _collect_read_ids!(ids::Set{LQPRelationId}, output::Output)
145+
!isnothing(output.relation_id) && push!(ids, persistent_id(output.relation_id))
146+
return nothing
147+
end
148+
function _collect_read_ids!(ids::Set{LQPRelationId}, abort::Abort)
149+
!isnothing(abort.relation_id) && push!(ids, persistent_id(abort.relation_id))
150+
return nothing
151+
end
152+
function _collect_read_ids!(ids::Set{LQPRelationId}, _export::Export)
153+
isnothing(_export.export_config) && return nothing
154+
config = _export.export_config
155+
if config.name == :csv_config
156+
csv_config = config[]::ExportCSVConfig
157+
for column in csv_config.data_columns
158+
!isnothing(column.column_data) && push!(ids, persistent_id(column.column_data))
159+
end
160+
elseif config.name == :iceberg_config
161+
iceberg_config = config[]::ExportIcebergConfig
162+
!isnothing(iceberg_config.table_def) && push!(ids, persistent_id(iceberg_config.table_def))
163+
end
164+
return nothing
165+
end
166+
101167
persistent_id(fragment::Fragment) = persistent_id(fragment.id::FragmentId)
102168
persistent_id(id::FragmentId) = LQPFragmentId(id.id)
103169
persistent_id(id::RelationId) = UInt128(id.id_low) + (UInt128(id.id_high) << 64)

sdks/julia/LogicalQueryProtocol.jl/test/properties_tests.jl

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,199 @@ end
117117
)
118118
@test persistent_id(def) == UInt128(7)
119119
end
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

Comments
 (0)