Skip to content

Commit 951acfc

Browse files
authored
feat: allow options on ingest queries (#74)
## What's Changed - Fix null values for structs - Allow ingest cases to set statement options
1 parent 88967d1 commit 951acfc

5 files changed

Lines changed: 64 additions & 40 deletions

File tree

adbc_drivers_validation/arrowjson.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,41 @@ def array_from_values(values: list, field: pyarrow.Field) -> pyarrow.Array:
111111
mask=pyarrow.array(mask, type=pyarrow.bool_()) if field.nullable else None,
112112
)
113113
elif isinstance(field.type, pyarrow.StructType):
114-
children = structlike_from_rows(values, field.type.fields)
115-
return pyarrow.StructArray.from_arrays(children, type=field.type)
114+
children, valid = structlike_from_rows(values, field.type.fields)
115+
if not field.nullable:
116+
if not all(valid):
117+
raise ValueError(
118+
f"Field '{field.name}' is not nullable but contains None values"
119+
)
120+
return pyarrow.StructArray.from_arrays(
121+
children,
122+
type=field.type,
123+
mask=pyarrow.array([not v for v in valid], type=pyarrow.bool_()),
124+
)
116125
return pyarrow.array(values, type=field.type)
117126

118127

119128
def structlike_from_rows(
120129
rows: list[dict], fields: list[pyarrow.Field]
121-
) -> list[pyarrow.Array]:
130+
) -> tuple[list[pyarrow.Array], list[bool]]:
122131
cols = []
123132
for field in fields:
124-
values = [row.pop(field.name, None) for row in rows]
133+
values = [(row.pop(field.name, None) if row else None) for row in rows]
125134
array = array_from_values(values, field)
126135
cols.append(array)
136+
valid = [row is not None for row in rows]
127137

128138
for row in rows:
129139
if row:
130140
raise ValueError(f"Extra fields in row: {row}")
131141

132-
return cols
142+
return cols, valid
133143

134144

135145
def table_from_rows(rows: list[dict], schema: pyarrow.Schema) -> pyarrow.Table:
136-
cols = structlike_from_rows(rows, list(schema))
146+
cols, valid = structlike_from_rows(rows, list(schema))
147+
if not all(valid):
148+
raise ValueError("Top-level table cannot have null rows")
137149
table = pyarrow.Table.from_arrays(cols, schema=schema)
138150
return table
139151

adbc_drivers_validation/tests/ingest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from adbc_drivers_validation import compare, model
3030
from adbc_drivers_validation.model import Query
31+
from adbc_drivers_validation.utils import setup_statement
3132

3233

3334
def generate_tests(all_quirks: list[model.DriverQuirks], metafunc) -> None:
@@ -107,7 +108,8 @@ def test_create(
107108

108109
with conn.cursor() as cursor:
109110
cursor.execute(driver.drop_table(table_name=table_name))
110-
cursor.adbc_ingest(table_name, data, mode="create")
111+
with setup_statement(query, cursor):
112+
cursor.adbc_ingest(table_name, data, mode="create")
111113

112114
idx = driver.quote_identifier("idx")
113115
value = driver.quote_identifier("value")

adbc_drivers_validation/tests/query.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from adbc_drivers_validation import compare, model
3030
from adbc_drivers_validation.model import Query
31-
from adbc_drivers_validation.utils import scoped_trace
31+
from adbc_drivers_validation.utils import scoped_trace, setup_statement
3232

3333

3434
def generate_tests(all_quirks: list[model.DriverQuirks], metafunc) -> None:
@@ -112,7 +112,7 @@ def test_query(
112112
cursor.adbc_statement.execute_update()
113113

114114
with conn.cursor() as cursor:
115-
with _setup_statement(query, cursor):
115+
with setup_statement(query, cursor):
116116
cursor.adbc_statement.set_sql_query(sql)
117117
handle, _ = cursor.adbc_statement.execute_query()
118118
with pyarrow.RecordBatchReader._import_from_c(handle.address) as reader:
@@ -135,7 +135,7 @@ def test_execute_schema(
135135
_setup_query(driver, conn, query)
136136

137137
with conn.cursor() as cursor:
138-
with _setup_statement(query, cursor):
138+
with setup_statement(query, cursor):
139139
schema = cursor.adbc_execute_schema(sql)
140140

141141
compare.compare_schemas(expected_schema, schema)
@@ -227,31 +227,3 @@ def _setup_query(
227227
with scoped_trace(f"setup statement: {statement}"):
228228
cursor.adbc_statement.set_sql_query(statement)
229229
cursor.adbc_statement.execute_update()
230-
231-
232-
@contextlib.contextmanager
233-
def _setup_statement(query: Query, cursor: adbc_driver_manager.dbapi.Cursor) -> None:
234-
md = query.metadata()
235-
if "statement" not in md:
236-
yield
237-
return
238-
239-
statement_md = md["statement"]
240-
if "options" not in statement_md:
241-
yield
242-
return
243-
244-
options = {}
245-
options_revert = {}
246-
for key, value in statement_md["options"].items():
247-
if isinstance(value, dict):
248-
assert "apply" in value
249-
assert "revert" in value
250-
options[key] = value["apply"]
251-
options_revert[key] = value["revert"]
252-
else:
253-
options[key] = value
254-
255-
cursor.adbc_statement.set_options(**options)
256-
yield
257-
cursor.adbc_statement.set_options(**options_revert)

adbc_drivers_validation/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
import contextlib
1616
import typing
1717

18+
import adbc_driver_manager.dbapi
19+
20+
if typing.TYPE_CHECKING:
21+
from adbc_drivers_validation.model import Query
22+
1823

1924
def merge_into(target: dict[str, typing.Any], values: dict[str, typing.Any]) -> None:
2025
for key, value in values.items():
@@ -36,3 +41,31 @@ def scoped_trace(msg: str) -> None:
3641
except Exception as e:
3742
e.add_note(msg)
3843
raise
44+
45+
46+
@contextlib.contextmanager
47+
def setup_statement(query: "Query", cursor: adbc_driver_manager.dbapi.Cursor) -> None:
48+
md = query.metadata()
49+
if "statement" not in md:
50+
yield
51+
return
52+
53+
statement_md = md["statement"]
54+
if "options" not in statement_md:
55+
yield
56+
return
57+
58+
options = {}
59+
options_revert = {}
60+
for key, value in statement_md["options"].items():
61+
if isinstance(value, dict):
62+
assert "apply" in value
63+
assert "revert" in value
64+
options[key] = value["apply"]
65+
options_revert[key] = value["revert"]
66+
else:
67+
options[key] = value
68+
69+
cursor.adbc_statement.set_options(**options)
70+
yield
71+
cursor.adbc_statement.set_options(**options_revert)

tests/test_arrowjson.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ def test_array_from_values_list() -> None:
220220

221221

222222
def test_array_from_values_struct() -> None:
223-
values = [{"a": 1, "b": base64.b64encode(b"hello").decode("utf-8")}]
224223
field = pyarrow.field(
225224
"struct_field",
226225
pyarrow.struct(
@@ -230,9 +229,15 @@ def test_array_from_values_struct() -> None:
230229
]
231230
),
232231
)
232+
233+
values = [
234+
None,
235+
{"a": 1, "b": base64.b64encode(b"hello").decode("utf-8")},
236+
{"a": None, "b": base64.b64encode(b"hello").decode("utf-8")},
237+
]
233238
actual = arrowjson.array_from_values(values, field)
234239
expected = pyarrow.array(
235-
[{"a": 1, "b": b"hello"}],
240+
[None, {"a": 1, "b": b"hello"}, {"a": None, "b": b"hello"}],
236241
type=field.type,
237242
)
238243
assert actual == expected

0 commit comments

Comments
 (0)