activerecord-turbopuffer-adapter 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc9b47b80bd694b53052b54ca2647989e6bfd911c1a0f2e59056d61cfccf075e
4
- data.tar.gz: a306ba71a2c62db295b438c6583f61a765d425a8f2888c328b831df31bf9840b
3
+ metadata.gz: 8bb295502835167a50a215f96707b208574ac3e6641b3cd9d02c50646b2335b6
4
+ data.tar.gz: '09c4fcdaf392f394619a7a1b786a0be832e248edd7449a5d49e804908fd4d94d'
5
5
  SHA512:
6
- metadata.gz: f73376471edf444dfc0f2c5dd702d2727a280c307f440da3a367d7d594074a9654e8b20fa58c19c2c6e57a4ab393dbacf17bb8044be82867e9fe7528d5b78874
7
- data.tar.gz: 8988590d661ac5c872df12f3e467f939bdb71f291d9485eab31227d3ea3afa3773f8ad8d885f2a91e3b8d3856e92835fc1a2381ceee79978185206b6f52e5479
6
+ metadata.gz: d822446b93294cbca0db8d9cb13a373588e160805d6dc1f1d8e9e88b3d6695ebff58e3cb94fae1715bf0e42850cafb6b88b807b369cc48d174dbeab893e7c35a
7
+ data.tar.gz: a22568481e63de3ca8a83a7367a59ac9b1e57853ed2315873c2948cd17a0bf4d4a348e77188e8d2f9deefcc25484b8e128e028948dbe11c4064adbc1909e1ffe
data/README.md CHANGED
@@ -120,6 +120,24 @@ Document
120
120
 
121
121
  > Eventual reads are cheaper but may not include writes from the last few seconds. Set a default with `turbopuffer_consistency "eventual"` in a model or `consistency: eventual` in `database.yml`. Strong is the default.
122
122
 
123
+ ### Errors
124
+
125
+ Turbopuffer errors are raised as the ActiveRecord exceptions a Rails app already handles.
126
+
127
+ ```ruby
128
+ begin
129
+ Document.where(published: true).to_a
130
+ rescue ActiveRecord::ConnectionFailed
131
+ # turbopuffer is unreachable
132
+ rescue ActiveRecord::StatementTimeout
133
+ # the request timed out
134
+ rescue ActiveRecord::DatabaseConnectionError
135
+ # the API key is invalid
136
+ rescue ActiveRecord::StatementInvalid => e
137
+ e.cause # => the underlying Turbopuffer::Errors::APIError, e.g. a bad request or rate limit
138
+ end
139
+ ```
140
+
123
141
  ### Using alongside Postgres
124
142
 
125
143
  While something of a lark, aspirationally the idea of this gem is to make turbopuffer conveniently usable as the primary db in a Rails app. In practice, however, using turbopuffer alongside a traditional primary db (such as postgres) is a supported, potentially more practical solution.
@@ -32,12 +32,17 @@ module ActiveRecord
32
32
  m.register_type "float", Type::Float.new
33
33
  m.register_type "bool", Type::Boolean.new
34
34
  m.register_type "datetime", ::Turbopuffer::ActiveRecord::Type::DateTime.new
35
+ m.register_type "bytes", ::Turbopuffer::ActiveRecord::Type::Bytes.new
36
+ m.register_type "{}f16", ::Turbopuffer::ActiveRecord::Type::SparseVector.new
35
37
  m.register_type(%r{\A\[\].+\z}) do |type|
36
38
  ::Turbopuffer::ActiveRecord::Type::Array.new(m.lookup(type.delete_prefix("[]")))
37
39
  end
38
40
  m.register_type(%r{\A\[\d+\]f(?:16|32)\z}) do |type|
39
41
  ::Turbopuffer::ActiveRecord::Type::Vector.new(type[/\d+/].to_i)
40
42
  end
43
+ m.register_type(%r{\A\[\d+\]i8\z}) do |type|
44
+ ::Turbopuffer::ActiveRecord::Type::Vector.new(type[/\d+/].to_i, integer: true)
45
+ end
41
46
  end
42
47
  end
43
48
 
@@ -96,7 +101,7 @@ module ActiveRecord
96
101
 
97
102
  def type_cast(value)
98
103
  case value
99
- when Array
104
+ when Array, Hash
100
105
  value
101
106
  else
102
107
  super
@@ -363,6 +368,19 @@ module ActiveRecord
363
368
  def reconnect
364
369
  connect
365
370
  end
371
+
372
+ def translate_exception(exception, message:, sql:, binds:)
373
+ case exception
374
+ when Turbopuffer::Errors::APITimeoutError
375
+ ActiveRecord::StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
376
+ when Turbopuffer::Errors::APIConnectionError
377
+ ActiveRecord::ConnectionFailed.new(message, sql: sql, binds: binds, connection_pool: @pool)
378
+ when Turbopuffer::Errors::AuthenticationError, Turbopuffer::Errors::PermissionDeniedError
379
+ ActiveRecord::DatabaseConnectionError.new(message)
380
+ else
381
+ super
382
+ end
383
+ end
366
384
  end
367
385
  end
368
386
  end
@@ -2,7 +2,7 @@ module Turbopuffer
2
2
  module ActiveRecord
3
3
  module Schema
4
4
  class Attribute
5
- TYPES = %r{\A(?:\[\])?(?:string|uuid|int|uint|float|bool|datetime)\z|\A\[\d+\]f(?:16|32)\z}
5
+ TYPES = %r{\A(?:\[\])?(?:string|uuid|int|uint|float|bool|datetime)\z|\Abytes\z|\A\[\d+\](?:f16|f32|i8)\z|\A\{\}f16\z}
6
6
 
7
7
  attr_reader :name, :type, :filterable, :full_text_search, :ann, :glob, :regex, :notnull
8
8
 
@@ -115,6 +115,7 @@ module Turbopuffer
115
115
  attrs[:ann] = true if attribute.ann
116
116
  attrs[:glob] = true if attribute.glob
117
117
  attrs[:regex] = true if attribute.regex
118
+ attrs[:sparse_knn] = { distance_metric: "dot_product" } if attribute.type == "{}f16"
118
119
 
119
120
  schema[attribute.name] = attrs
120
121
  end
@@ -0,0 +1,21 @@
1
+ require "base64"
2
+
3
+ module Turbopuffer
4
+ module ActiveRecord
5
+ module Type
6
+ class Bytes < ::ActiveModel::Type::Value
7
+ def serialize(value)
8
+ return if value.nil?
9
+
10
+ Base64.strict_encode64(value)
11
+ end
12
+
13
+ def deserialize(value)
14
+ return if value.nil?
15
+
16
+ Base64.decode64(value)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Turbopuffer
2
+ module ActiveRecord
3
+ module Type
4
+ class SparseVector < ::ActiveModel::Type::Value
5
+ include ::ActiveModel::Type::Helpers::Mutable
6
+
7
+ def serialize(value)
8
+ return if value.nil?
9
+
10
+ value.to_h { |key, weight| [ key.to_s, Float(weight) ] }
11
+ end
12
+
13
+ def deserialize(value)
14
+ return if value.nil?
15
+
16
+ value.to_h.transform_keys(&:to_s)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,15 +4,16 @@ module Turbopuffer
4
4
  class Vector < ::ActiveModel::Type::Value
5
5
  include ::ActiveModel::Type::Helpers::Mutable
6
6
 
7
- def initialize(dimensions)
7
+ def initialize(dimensions, integer: false)
8
8
  @dimensions = dimensions
9
+ @integer = integer
9
10
  super()
10
11
  end
11
12
 
12
13
  def serialize(value)
13
14
  return if value.nil?
14
15
 
15
- vector = ::Array.wrap(value).map { |component| Float(component) }
16
+ vector = ::Array.wrap(value).map { |component| component(component) }
16
17
 
17
18
  unless vector.size == @dimensions
18
19
  raise ArgumentError, "expected a vector of #{@dimensions} dimensions, got #{vector.size}"
@@ -24,6 +25,19 @@ module Turbopuffer
24
25
  def deserialize(value)
25
26
  value
26
27
  end
28
+
29
+ private
30
+
31
+ def component(value)
32
+ number = Float(value)
33
+ return number unless @integer
34
+
35
+ unless number == number.to_i
36
+ raise ArgumentError, "expected an integer vector component, got #{value.inspect}"
37
+ end
38
+
39
+ number.to_i
40
+ end
27
41
  end
28
42
  end
29
43
  end
@@ -1,7 +1,9 @@
1
1
  require "turbopuffer/active_record/type/date_time"
2
2
  require "turbopuffer/active_record/type/unsigned_integer"
3
+ require "turbopuffer/active_record/type/bytes"
3
4
  require "turbopuffer/active_record/type/array"
4
5
  require "turbopuffer/active_record/type/vector"
6
+ require "turbopuffer/active_record/type/sparse_vector"
5
7
 
6
8
  module Turbopuffer
7
9
  module ActiveRecord
@@ -1,5 +1,5 @@
1
1
  module Turbopuffer
2
2
  module ActiveRecord
3
- VERSION = "0.1.3"
3
+ VERSION = "0.1.5"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-turbopuffer-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Monette
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-22 00:00:00.000000000 Z
10
+ date: 2026-09-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '8.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: base64
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: railties
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +95,9 @@ files:
81
95
  - lib/turbopuffer/active_record/schema/model.rb
82
96
  - lib/turbopuffer/active_record/type.rb
83
97
  - lib/turbopuffer/active_record/type/array.rb
98
+ - lib/turbopuffer/active_record/type/bytes.rb
84
99
  - lib/turbopuffer/active_record/type/date_time.rb
100
+ - lib/turbopuffer/active_record/type/sparse_vector.rb
85
101
  - lib/turbopuffer/active_record/type/unsigned_integer.rb
86
102
  - lib/turbopuffer/active_record/type/vector.rb
87
103
  - lib/turbopuffer/active_record/version.rb