activerecord-turbopuffer-adapter 0.1.7 → 0.1.8
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 +4 -4
- data/README.md +14 -0
- data/lib/active_record/connection_adapters/turbopuffer_adapter.rb +7 -2
- data/lib/arel/visitors/turbopuffer.rb +24 -11
- data/lib/turbopuffer/active_record/namespace_scope.rb +15 -0
- data/lib/turbopuffer/active_record/schema/model.rb +25 -0
- data/lib/turbopuffer/active_record/schema.rb +1 -0
- data/lib/turbopuffer/active_record/version.rb +1 -1
- data/lib/turbopuffer/active_record.rb +14 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7e4579b0235c712aef39dedae94abdb3e8053ef5b04fff2e46ee38c0fd5710a
|
|
4
|
+
data.tar.gz: dadc48af421f105ed95d7964b7929ced1dd613217fdda1b88b00a58c291867cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6037d2898aa914ed3990f238ac00b86c4b710d630fd3aa7c9dd3af9200f768736b9207cf8cedf079c01d60fd8fce614cdafdaa5873241b1c7a47fa1f6bb4b9df
|
|
7
|
+
data.tar.gz: 8b6c7caf5e1b82fbed33da7e5e06e1232cf9603aab035b6a9ef8540223047020429927721928911c80512c778fbdef5a52b1628ca0110ac4414d0419f18a9aed
|
data/README.md
CHANGED
|
@@ -132,6 +132,20 @@ Document
|
|
|
132
132
|
|
|
133
133
|
> 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.
|
|
134
134
|
|
|
135
|
+
### Namespaces per tenant
|
|
136
|
+
|
|
137
|
+
turbopuffer recommends one namespace per tenant: a query can only ever see that tenant's documents, and search covers just the documents that matter rather than everyone's. `namespace` scopes a model to any namespace, and records remember where they were loaded from, so `update!`, `reload` and `destroy` go back to the same place.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Document.namespace("documents-#{tenant.id}").create!(title: "Hello")
|
|
141
|
+
Document.namespace("documents-#{tenant.id}").where(published: true)
|
|
142
|
+
|
|
143
|
+
doc = Document.namespace("documents-#{tenant.id}").find(id)
|
|
144
|
+
doc.update!(published: true)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The name is used as given (under `namespace_prefix` if one is set). Without `namespace`, a model's table name is its namespace.
|
|
148
|
+
|
|
135
149
|
### Errors
|
|
136
150
|
|
|
137
151
|
Turbopuffer errors are raised as the ActiveRecord exceptions a Rails app already handles.
|
|
@@ -299,8 +299,7 @@ module ActiveRecord
|
|
|
299
299
|
def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch:)
|
|
300
300
|
tpuf_query = sql.is_a?(Array) ? sql.first : sql
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
namespace = raw_connection.namespace(namespace_name)
|
|
302
|
+
namespace = raw_connection.namespace(namespace_name_for(tpuf_query))
|
|
304
303
|
|
|
305
304
|
result = case tpuf_query.op
|
|
306
305
|
when :insert then turbopuffer_insert(namespace, tpuf_query)
|
|
@@ -318,6 +317,12 @@ module ActiveRecord
|
|
|
318
317
|
result
|
|
319
318
|
end
|
|
320
319
|
|
|
320
|
+
def namespace_name_for(query)
|
|
321
|
+
override = query.namespace_override || ::Turbopuffer::ActiveRecord.namespace_override
|
|
322
|
+
|
|
323
|
+
[ @config[:namespace_prefix], override || query.namespace ].compact_blank.join("-")
|
|
324
|
+
end
|
|
325
|
+
|
|
321
326
|
def arel_visitor
|
|
322
327
|
Arel::Visitors::Turbopuffer.new
|
|
323
328
|
end
|
|
@@ -11,7 +11,8 @@ module Arel::Visitors
|
|
|
11
11
|
include_attributes: @include_attributes,
|
|
12
12
|
aggregate_by: @aggregate_by,
|
|
13
13
|
upsert_rows: @upsert_rows,
|
|
14
|
-
consistency: @consistency
|
|
14
|
+
consistency: @consistency,
|
|
15
|
+
namespace_override: @namespace_override
|
|
15
16
|
}
|
|
16
17
|
end
|
|
17
18
|
def to_s = JSON.generate(to_h)
|
|
@@ -21,10 +22,10 @@ module Arel::Visitors
|
|
|
21
22
|
alias == eql?
|
|
22
23
|
|
|
23
24
|
attr_reader :op, :namespace, :filters, :top_k, :rank_by, :include_attributes, :upsert_rows, :aggregate_by, :group_by,
|
|
24
|
-
:consistency
|
|
25
|
+
:consistency, :namespace_override
|
|
25
26
|
|
|
26
27
|
def initialize(op:, namespace:, filters: nil, top_k: nil, rank_by: nil, include_attributes: nil,
|
|
27
|
-
upsert_rows: nil, aggregate_by: nil, group_by: nil, consistency: nil)
|
|
28
|
+
upsert_rows: nil, aggregate_by: nil, group_by: nil, consistency: nil, namespace_override: nil)
|
|
28
29
|
@op = op
|
|
29
30
|
@namespace = namespace
|
|
30
31
|
@filters = filters
|
|
@@ -35,6 +36,7 @@ module Arel::Visitors
|
|
|
35
36
|
@group_by = group_by
|
|
36
37
|
@upsert_rows = upsert_rows
|
|
37
38
|
@consistency = consistency
|
|
39
|
+
@namespace_override = namespace_override
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -124,10 +126,19 @@ module Arel::Visitors
|
|
|
124
126
|
include_attributes: attributes.flat_map { |p| visit(p) },
|
|
125
127
|
group_by: core.groups.map { |g| visit(g) },
|
|
126
128
|
aggregate_by: aggregate && visit(aggregate),
|
|
127
|
-
consistency: consistency_hint(core.optimizer_hints)
|
|
129
|
+
consistency: consistency_hint(core.optimizer_hints),
|
|
130
|
+
namespace_override: namespace_annotation(core.comment)
|
|
128
131
|
)
|
|
129
132
|
end
|
|
130
133
|
|
|
134
|
+
def namespace_annotation(comment)
|
|
135
|
+
return unless comment
|
|
136
|
+
|
|
137
|
+
prefix = ::Turbopuffer::ActiveRecord::NAMESPACE_ANNOTATION
|
|
138
|
+
annotation = comment.values.reverse.find { |value| value.start_with?(prefix) }
|
|
139
|
+
annotation&.delete_prefix(prefix)
|
|
140
|
+
end
|
|
141
|
+
|
|
131
142
|
def consistency_hint(hints)
|
|
132
143
|
return unless hints
|
|
133
144
|
|
|
@@ -152,18 +163,20 @@ module Arel::Visitors
|
|
|
152
163
|
rows = o.values ? visit(o.values) : [ [] ]
|
|
153
164
|
|
|
154
165
|
TurbopufferQuery.new(
|
|
155
|
-
op:
|
|
156
|
-
namespace:
|
|
157
|
-
filters:
|
|
158
|
-
upsert_rows:
|
|
166
|
+
op: :update,
|
|
167
|
+
namespace: visit(o.relation),
|
|
168
|
+
filters: conjoin(o.wheres.map { |w| visit(w) }),
|
|
169
|
+
upsert_rows: rows,
|
|
170
|
+
namespace_override: namespace_annotation(o.comment)
|
|
159
171
|
)
|
|
160
172
|
end
|
|
161
173
|
|
|
162
174
|
def visit_Arel_Nodes_DeleteStatement(o)
|
|
163
175
|
TurbopufferQuery.new(
|
|
164
|
-
op:
|
|
165
|
-
namespace:
|
|
166
|
-
filters:
|
|
176
|
+
op: :delete,
|
|
177
|
+
namespace: visit(o.relation),
|
|
178
|
+
filters: conjoin(o.wheres.map { |w| visit(w) }),
|
|
179
|
+
namespace_override: namespace_annotation(o.comment)
|
|
167
180
|
)
|
|
168
181
|
end
|
|
169
182
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Turbopuffer
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module NamespaceScope
|
|
4
|
+
def turbopuffer_namespace
|
|
5
|
+
annotation = annotate_values.reverse.find { |value| value.start_with?(NAMESPACE_ANNOTATION) }
|
|
6
|
+
annotation&.delete_prefix(NAMESPACE_ANNOTATION)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def exec_queries(...) = ::Turbopuffer::ActiveRecord.with_namespace(turbopuffer_namespace) { super }
|
|
10
|
+
def insert_all(...) = ::Turbopuffer::ActiveRecord.with_namespace(turbopuffer_namespace) { super }
|
|
11
|
+
def insert_all!(...) = ::Turbopuffer::ActiveRecord.with_namespace(turbopuffer_namespace) { super }
|
|
12
|
+
def upsert_all(...) = ::Turbopuffer::ActiveRecord.with_namespace(turbopuffer_namespace) { super }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -13,6 +13,27 @@ module Turbopuffer
|
|
|
13
13
|
class_attribute :_turbopuffer_consistency, instance_accessor: false, default: nil
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def turbopuffer_namespace = @turbopuffer_namespace
|
|
17
|
+
|
|
18
|
+
def update_columns(...) = with_turbopuffer_namespace { super }
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def init_internals
|
|
23
|
+
@turbopuffer_namespace = ::Turbopuffer::ActiveRecord.namespace_override ||
|
|
24
|
+
self.class.current_scope.try(:turbopuffer_namespace)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def _create_record(...) = with_turbopuffer_namespace { super }
|
|
29
|
+
def _update_row(...) = with_turbopuffer_namespace { super }
|
|
30
|
+
def _delete_row(...) = with_turbopuffer_namespace { super }
|
|
31
|
+
def _find_record(...) = with_turbopuffer_namespace { super }
|
|
32
|
+
|
|
33
|
+
def with_turbopuffer_namespace(&block)
|
|
34
|
+
::Turbopuffer::ActiveRecord.with_namespace(@turbopuffer_namespace, &block)
|
|
35
|
+
end
|
|
36
|
+
|
|
16
37
|
class << self
|
|
17
38
|
def namespaces
|
|
18
39
|
::ActiveRecord::Base.descendants.select do |model|
|
|
@@ -82,6 +103,10 @@ module Turbopuffer
|
|
|
82
103
|
select(*turbopuffer_attributes.map(&:name))
|
|
83
104
|
end
|
|
84
105
|
|
|
106
|
+
def namespace(name)
|
|
107
|
+
annotate("#{NAMESPACE_ANNOTATION}#{name}").extending(::Turbopuffer::ActiveRecord::NamespaceScope)
|
|
108
|
+
end
|
|
109
|
+
|
|
85
110
|
def glob(pattern, case_sensitive: true)
|
|
86
111
|
::Turbopuffer::ActiveRecord::Glob.new(pattern, case_sensitive:)
|
|
87
112
|
end
|
|
@@ -4,6 +4,20 @@ module Turbopuffer
|
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
|
|
7
|
+
NAMESPACE_ANNOTATION = "turbopuffer_namespace="
|
|
8
|
+
|
|
9
|
+
def self.namespace_override
|
|
10
|
+
ActiveSupport::IsolatedExecutionState[:turbopuffer_namespace]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.with_namespace(name)
|
|
14
|
+
previous = namespace_override
|
|
15
|
+
ActiveSupport::IsolatedExecutionState[:turbopuffer_namespace] = name
|
|
16
|
+
yield
|
|
17
|
+
ensure
|
|
18
|
+
ActiveSupport::IsolatedExecutionState[:turbopuffer_namespace] = previous
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
# Teaches Active Record that `adapter: turbopuffer` in database.yml maps to
|
|
8
22
|
# our adapter. The railtie calls this during boot; test harnesses and
|
|
9
23
|
# non-Rails consumers call it themselves.
|
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.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Richard Monette
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -27,16 +27,16 @@ dependencies:
|
|
|
27
27
|
name: base64
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
32
|
+
version: '0.2'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
39
|
+
version: '0.2'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: railties
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- lib/arel/visitors/turbopuffer.rb
|
|
90
90
|
- lib/turbopuffer/active_record.rb
|
|
91
91
|
- lib/turbopuffer/active_record/glob.rb
|
|
92
|
+
- lib/turbopuffer/active_record/namespace_scope.rb
|
|
92
93
|
- lib/turbopuffer/active_record/railtie.rb
|
|
93
94
|
- lib/turbopuffer/active_record/schema.rb
|
|
94
95
|
- lib/turbopuffer/active_record/schema/attribute.rb
|