Skip to content

Commit 5e98948

Browse files
committed
feat: Add validation for incompatible R*Tree table options
- Add validate_rtree_options!/1 to explicitly reject incompatible options - R*Tree virtual tables do not support :strict, :random_rowid, :without_rowid - Add comprehensive tests for single and multiple incompatible options - Update AGENTS.md to document incompatible options - Update CHANGELOG.md to document validation behavior Addresses feedback about options being silently ignored - now raises clear ArgumentError with specific incompatible options listed.
1 parent 338c42a commit 5e98948

4 files changed

Lines changed: 72 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,7 +1107,7 @@ distance_sql = EctoLibSql.Native.vector_distance_cos("description_embedding", qu
11071107

11081108
### R*Tree Spatial Indexing
11091109

1110-
R*Tree is a specialized spatial index for efficient multi-dimensional range queries. Perfect for geospatial data, collision detection, and time-series queries.
1110+
R*Tree is a specialised spatial index for efficient multi-dimensional range queries. Perfect for geospatial data, collision detection, and time-series queries.
11111111

11121112
#### Creating R*Tree Tables
11131113

@@ -1134,6 +1134,7 @@ end
11341134
- Remaining columns come in min/max pairs (2D, 3D, 4D, or 5D)
11351135
- Total columns must be odd (3, 5, 7, 9, or 11)
11361136
- Minimum 3 columns (id + 1 dimension), maximum 11 columns (id + 5 dimensions)
1137+
- R*Tree tables are virtual tables and do not support standard table options like `:strict`, `:random_rowid`, or `:without_rowid`
11371138

11381139
#### 2D Example: Geographic Boundaries
11391140

@@ -1210,7 +1211,7 @@ result = Ecto.Adapters.SQL.query!(
12101211
SELECT id FROM events
12111212
WHERE max_x >= 90 AND min_x <= 110
12121213
AND max_y >= 190 AND min_y <= 210
1213-
AND max_time >= #{query_start} AND min_time <= #{end_time}
1214+
AND max_time >= #{query_start} AND min_time <= #{query_end}
12141215
"""
12151216
)
12161217
```

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- **Table creation**: Use `:rtree => true` option in Ecto migrations
1515
- **Dimensions supported**: 1D to 5D (3 to 11 columns total including ID)
1616
- **Column structure**: First column must be `id` (integer primary key), followed by min/max coordinate pairs
17-
- **Validation**: Automatic validation of column count (odd numbers only), first-column requirements (must be 'id'), and dimensional constraints
17+
- **Validation**: Automatic validation of column count (odd numbers only), first-column requirements (must be 'id'), dimensional constraints, and incompatible table options
18+
- **Table options**: R*Tree virtual tables reject standard table options (`:strict`, `:random_rowid`, `:without_rowid`) with clear error messages
1819
- **Use cases**: Geographic bounding boxes, collision detection, time-range queries, spatial indexing
1920
- **Migration example**:
2021
```elixir
@@ -28,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2829
```
2930
- **Query patterns**: Point containment, bounding box intersection, range queries
3031
- **Virtual table syntax**: Generates `CREATE VIRTUAL TABLE ... USING rtree(...)` DDL
31-
- **Implementation**: New `create_rtree_table/3` and `validate_rtree_columns!/1` helpers in `connection.ex`
32+
- **Implementation**: New `create_rtree_table/3`, `validate_rtree_options!/1`, and `validate_rtree_columns!/1` helpers in `connection.ex`
3233
- **Comprehensive test coverage** in `test/rtree_test.exs` covering 2D/3D tables, validation, queries, and CRUD operations
3334
- **Documentation**: Full guide in AGENTS.md with examples for geographic data, time-series, and hybrid vector+spatial search
3435
- **Comparison guide**: R*Tree vs Vector Search decision matrix in documentation

lib/ecto/adapters/libsql/connection.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ defmodule Ecto.Adapters.LibSql.Connection do
186186

187187
# Check if this is an R*Tree virtual table
188188
if table.options && Keyword.get(table.options, :rtree, false) do
189+
# Validate that no incompatible options are set with :rtree
190+
validate_rtree_options!(table.options)
189191
create_rtree_table(table_name, if_not_exists, columns)
190192
else
191193
# Standard table creation
@@ -502,6 +504,25 @@ defmodule Ecto.Adapters.LibSql.Connection do
502504
]
503505
end
504506

507+
defp validate_rtree_options!(options) do
508+
# R*Tree virtual tables are incompatible with standard table options
509+
# Check for any non-:rtree options that would be silently ignored
510+
incompatible_options =
511+
Keyword.keys(options)
512+
|> Enum.reject(&(&1 == :rtree))
513+
514+
unless Enum.empty?(incompatible_options) do
515+
options_str = Enum.map_join(incompatible_options, ", ", &inspect/1)
516+
517+
raise ArgumentError,
518+
"R*Tree virtual tables do not support standard table options. " <>
519+
"Found incompatible options: #{options_str}. " <>
520+
"R*Tree tables can only use the :rtree option."
521+
end
522+
523+
:ok
524+
end
525+
505526
defp validate_rtree_columns!(columns) do
506527
# R*Tree requires odd number of columns (3 to 11)
507528
# First column is ID, then min/max pairs

test/rtree_test.exs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,51 @@ defmodule Ecto.RTreeTest do
208208
execute_ddl({:create, table, columns})
209209
end
210210
end
211+
212+
test "rejects R*Tree table with incompatible options" do
213+
import Ecto.Adapters.LibSql.Connection
214+
215+
# Test with :strict option
216+
table = %Ecto.Migration.Table{
217+
name: :invalid_rtree,
218+
prefix: nil,
219+
options: [rtree: true, strict: true]
220+
}
221+
222+
columns = [
223+
{:add, :id, :integer, [primary_key: true]},
224+
{:add, :min_lat, :float, []},
225+
{:add, :max_lat, :float, []},
226+
{:add, :min_lng, :float, []},
227+
{:add, :max_lng, :float, []}
228+
]
229+
230+
assert_raise ArgumentError, ~r/do not support standard table options/, fn ->
231+
execute_ddl({:create, table, columns})
232+
end
233+
234+
# Test with :random_rowid option
235+
table = %Ecto.Migration.Table{
236+
name: :invalid_rtree,
237+
prefix: nil,
238+
options: [rtree: true, random_rowid: true]
239+
}
240+
241+
assert_raise ArgumentError, ~r/Found incompatible options: :random_rowid/, fn ->
242+
execute_ddl({:create, table, columns})
243+
end
244+
245+
# Test with multiple incompatible options
246+
table = %Ecto.Migration.Table{
247+
name: :invalid_rtree,
248+
prefix: nil,
249+
options: [rtree: true, strict: true, random_rowid: true]
250+
}
251+
252+
assert_raise ArgumentError, ~r/do not support standard table options/, fn ->
253+
execute_ddl({:create, table, columns})
254+
end
255+
end
211256
end
212257

213258
describe "R*Tree queries" do

0 commit comments

Comments
 (0)