Skip to content

Commit dea235f

Browse files
Clarify ARRAY conversion in typed selects and direct queries
1 parent e6bc6fa commit dea235f

1 file changed

Lines changed: 47 additions & 92 deletions

File tree

‎docs/sqlalchemy.md‎

Lines changed: 47 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,79 +1084,66 @@ CREATE TABLE orders (
10841084
)
10851085
```
10861086

1087-
#### Querying ARRAY data
1087+
#### Querying ARRAY data with typed SQLAlchemy expressions
10881088

1089-
PyAthena automatically converts ARRAY data between different formats:
1089+
Use `select()` with `ARRAY` or `AthenaArray` columns whose element types are known, either declared explicitly or reflected from Athena, to receive typed Python collections.
1090+
For example, the `events` table above returns `numbers` as a list of integers and `labels` as nested lists of strings.
1091+
SQLAlchemy serializes these result columns as JSON in Athena and converts the returned values according to the column types.
1092+
You do not need to call `json.loads()` on these results.
1093+
This preserves strings such as `"a,b"`, `"001"`, and `"null"` as strings, including within nested arrays.
10901094

1091-
```python
1092-
from sqlalchemy import text
1093-
1094-
# Query ARRAY data using ARRAY constructor
1095-
result = connection.execute(
1096-
text("SELECT ARRAY[1, 2, 3, 4, 5] as item_ids")
1097-
).fetchone()
1098-
1099-
# Access ARRAY data as Python list
1100-
item_ids = result.item_ids # [1, 2, 3, 4, 5]
1101-
```
1095+
#### Direct cursor and textual SQL results
11021096

1103-
#### Complex ARRAY operations
1097+
Direct `cursor.execute()` calls and untyped SQLAlchemy `text()` queries use the cursor's existing conversion behavior.
1098+
The examples in this section use the standard REST cursor with its default converter and no result type hints or custom converters.
1099+
Other cursor implementations have their own conversion behavior.
11041100

1105-
For arrays containing complex data types:
1101+
The standard converter already converts simple ARRAY values to Python lists:
11061102

11071103
```python
1108-
# Arrays with STRUCT elements
1109-
result = connection.execute(
1110-
text("SELECT ARRAY[ROW('Alice', 25), ROW('Bob', 30)] as users")
1111-
).fetchone()
1112-
1113-
users = result.users
1114-
# Use typed SQLAlchemy expressions when you need declared ROW field types.
1115-
1116-
# Using CAST AS JSON for complex ARRAY operations
1117-
result = connection.execute(
1118-
text("SELECT CAST(ARRAY[1, 2, 3] AS JSON) as data")
1119-
).fetchone()
1120-
1121-
# Parse JSON result
1122-
import json
1123-
if isinstance(result.data, str):
1124-
array_data = json.loads(result.data) # [1, 2, 3]
1125-
else:
1126-
array_data = result.data # Already converted to list
1104+
result = cursor.execute("SELECT ARRAY[1, 2, 3] AS numbers").fetchone()
1105+
numbers = result[0] # [1, 2, 3]
11271106
```
11281107

1129-
#### Data format support
1130-
1131-
PyAthena supports multiple ARRAY data formats:
1132-
1133-
**Athena Native Format:**
1108+
This conversion predates the typed SQLAlchemy ARRAY support described above.
1109+
Typed SQLAlchemy ARRAY support does not change the behavior of direct cursor queries or untyped `text()` queries.
11341110

1135-
```python
1136-
# Input: '[1, 2, 3]'
1137-
# Output: [1, 2, 3]
1111+
The standard ARRAY converter first tries JSON parsing, then a limited parser for Athena's native text representation.
1112+
The following examples show its behavior for strings received as ARRAY values:
11381113

1139-
# Input: '[apple, banana, cherry]'
1140-
# Output: ["apple", "banana", "cherry"]
1141-
```
1114+
| ARRAY text | Python result |
1115+
|---|---|
1116+
| `[1, 2, 3]` | `[1, 2, 3]` (`list`) |
1117+
| `[[1, 2], [3, 4]]` | `[[1, 2], [3, 4]]` (nested `list`) |
1118+
| `[[one, two], [three]]` | `"[[one, two], [three]]"` (`str`) |
1119+
| `[a, b=1]` | `"[a, b=1]"` (`str`) |
11421120

1143-
**JSON Format:**
1121+
The numeric examples are valid JSON.
1122+
The unquoted nested string array is not valid JSON, and the native parser does not support nested arrays.
1123+
The last example contains an equals sign that the native parser rejects.
1124+
For these unsupported representations, the converter returns the original string rather than dropping unparsed elements.
1125+
A string result therefore does not necessarily mean the stored ARRAY is invalid.
1126+
Calling `json.loads()` on that native text will not resolve these cases because it is not JSON.
11441127

1145-
```python
1146-
# Input: '[1, 2, 3]'
1147-
# Output: [1, 2, 3]
1128+
Use typed SQLAlchemy SELECT expressions when element types and nested string values must be preserved.
1129+
When writing SQL directly, explicitly request JSON as shown below.
11481130

1149-
# Input: '["apple", "banana", "cherry"]'
1150-
# Output: ["apple", "banana", "cherry"]
1151-
```
1131+
#### Requesting JSON in direct SQL
11521132

1153-
**Complex Nested Arrays:**
1133+
Use `CAST(... AS JSON)` to have Athena return JSON instead of its native ARRAY text representation.
1134+
With the standard REST cursor and default converter, JSON results are already decoded into Python values:
11541135

11551136
```python
1156-
# Input: '[{name=John, age=30}, {name=Jane, age=25}]'
1157-
# Output: [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]
1137+
result = cursor.execute(
1138+
"SELECT CAST(ARRAY[ARRAY['one', 'two'], ARRAY['three']] AS JSON) AS labels"
1139+
).fetchone()
1140+
labels = result[0] # [["one", "two"], ["three"]]
11581141
```
11591142

1143+
The same applies to an untyped SQLAlchemy `text()` query using `awsathena+rest` with the default converter.
1144+
No additional `json.loads()` call is needed in these examples.
1145+
This path returns JSON-derived Python values; use typed SQLAlchemy ARRAY columns when you need conversion according to declared element types such as `Numeric` or `Date`.
1146+
11601147
#### Type definitions
11611148

11621149
AthenaArray supports various item types:
@@ -1178,47 +1165,15 @@ AthenaArray(AthenaArray(Integer)) # ARRAY<ARRAY<INT>>
11781165

11791166
#### Best practices
11801167

1181-
1. **Use appropriate item types** in AthenaArray definitions:
1182-
1183-
```python
1184-
AthenaArray(Integer) # For numeric arrays
1185-
AthenaArray(String) # For string arrays
1186-
AthenaArray(AthenaStruct(...)) # For arrays of structs
1187-
```
1188-
1189-
2. **Use CAST AS JSON** for complex array operations:
1190-
1191-
```sql
1192-
SELECT CAST(complex_array AS JSON) FROM table_name
1193-
```
1194-
1195-
3. **Handle NULL values** appropriately in your application logic:
1168+
1. Declare the ARRAY element type and use typed SQLAlchemy SELECT expressions to preserve nested values and scalar types.
1169+
2. For direct cursor or untyped `text()` queries, request `CAST(... AS JSON)` when the native text parser cannot represent the result reliably.
1170+
3. Handle SQL NULL and empty arrays before accessing an element:
11961171

11971172
```python
1198-
if result.array_column is not None:
1199-
# Process array data
1200-
first_item = result.array_column[0] if result.array_column else None
1173+
# A result from a typed ARRAY SELECT or the JSON query above
1174+
first_label = labels[0] if labels else None
12011175
```
12021176

1203-
#### Migration from RAW strings
1204-
1205-
**Before (raw string handling):**
1206-
1207-
```python
1208-
result = cursor.execute("SELECT array_column FROM table").fetchone()
1209-
raw_data = result[0] # "[1, 2, 3]"
1210-
import json
1211-
parsed_data = json.loads(raw_data)
1212-
```
1213-
1214-
**After (automatic conversion):**
1215-
1216-
```python
1217-
result = cursor.execute("SELECT array_column FROM table").fetchone()
1218-
array_data = result[0] # [1, 2, 3] - automatically converted
1219-
first_item = array_data[0] # Direct access
1220-
```
1221-
12221177
### JSON type support
12231178

12241179
PyAthena provides support for Amazon Athena's JSON data type, enabling you to work with JSON data in your SQLAlchemy applications. The JSON type is primarily used with Data Manipulation Language (DML) operations in Athena.

0 commit comments

Comments
 (0)