Skip to content

Commit 0f2a6e3

Browse files
authored
Merge pull request #36 from Couchbase-Ecosystem/feat/prepared-stmt-adhoc
feat: enable adhoc option for prepared statement plan caching
2 parents 2357070 + b5a44aa commit 0f2a6e3

6 files changed

Lines changed: 115 additions & 11 deletions

File tree

.github/workflows/test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ jobs:
2525
name: ${{ matrix.ruby }} rails-${{ matrix.active-model }} couchbase-${{ matrix.couchbase }}
2626
steps:
2727
- uses: actions/checkout@v3
28-
- run: sudo apt-get update && sudo apt-get install libevent-dev libev-dev python3-httplib2
29-
- run: wget http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.2_amd64.deb
30-
- run: sudo apt install ./libtinfo5_6.3-2ubuntu0.2_amd64.deb
28+
- run: |
29+
sudo apt-get update
30+
sudo apt-get install -y libevent-dev libev-dev python3-httplib2
31+
sudo apt-get install -y libtinfo5 || sudo apt-get install -y libtinfo6
3132
- uses: ruby/setup-ruby@v1
3233
with:
3334
ruby-version: ${{ matrix.ruby }}

docusaurus/docs/tutorial-ruby-couchbase-orm/07-sqlpp-queries.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,47 @@ docs = N1QLTest.by_custom_rating_values(key: [[1, 2]]).collect { |ob| ob.name }
147147

148148
In the above examples, the `collect` method is used to extract the `name` attribute from each document in the result set.
149149

150+
## 7.8 Prepared Statement Plan Caching
151+
152+
Couchbase Server can cache the query execution plan for a SQL++ query so that subsequent executions skip the planning step. This is controlled by the `adhoc` query option: `adhoc: false` tells the server to prepare and cache the plan on first execution and reuse it on subsequent ones.
153+
154+
### Default behaviour
155+
156+
By default CouchbaseOrm runs queries with `adhoc: true` (the Couchbase SDK default), meaning no plan caching. This preserves the existing behaviour — you opt into plan caching explicitly.
157+
158+
### Enabling caching for a specific call
159+
160+
Pass `adhoc: false` directly to the query method to prepare and cache the plan (useful for frequently repeated queries):
161+
162+
```ruby
163+
# Cache the plan for this query
164+
N1QLTest.by_rating(key: 1, adhoc: false)
165+
166+
# Relation query with plan caching
167+
User.where(country: 'FR').with(adhoc: false).to_a
168+
```
169+
170+
### Enabling caching for a specific `n1ql` definition
171+
172+
Set `adhoc: false` in the macro options to always cache the plan for that particular query:
173+
174+
```ruby
175+
n1ql :by_stable_filter, emit_key: [:name], adhoc: false
176+
```
177+
178+
### Changing the global default
179+
180+
Override the thread-local config to change the default for all queries in the current thread:
181+
182+
```ruby
183+
# Enable plan caching for all queries in this thread
184+
CouchbaseOrm::N1ql.config(adhoc: false)
185+
```
186+
187+
### Override priority
188+
189+
From highest to lowest: **per-call kwarg** > **per-`n1ql`-definition option** > **`N1ql.config`** > **default (`true`)**.
190+
150191
## 7.7 Indexing for SQL++
151192

152193
To optimize the performance of SQL++ queries, it's important to create appropriate indexes on the fields used in the query conditions. Couchbase Server provides a way to create indexes using the Index service.

lib/couchbase-orm/n1ql.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module N1ql
99
extend ActiveSupport::Concern
1010
NO_VALUE = :no_value_specified
1111
DEFAULT_SCAN_CONSISTENCY = :request_plus
12+
DEFAULT_ADHOC = true
1213
# sanitize for injection query
1314
def self.sanitize(value)
1415
if value.is_a?(String)
@@ -22,9 +23,10 @@ def self.sanitize(value)
2223

2324
def self.config(new_config = nil)
2425
Thread.current['__couchbaseorm_n1ql_config__'] = new_config if new_config
25-
Thread.current['__couchbaseorm_n1ql_config__'] || {
26-
scan_consistency: DEFAULT_SCAN_CONSISTENCY
27-
}
26+
{
27+
scan_consistency: DEFAULT_SCAN_CONSISTENCY,
28+
adhoc: DEFAULT_ADHOC
29+
}.merge(Thread.current['__couchbaseorm_n1ql_config__'] || {})
2830
end
2931

3032
module ClassMethods
@@ -57,7 +59,10 @@ def n1ql(name, query_fn: nil, emit_key: [], custom_order: nil, **options)
5759
@indexes[name] = method_opts
5860

5961
singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier|
60-
opts = options.merge(opts).reverse_merge(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
62+
opts = options.merge(opts).reverse_merge(
63+
scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency],
64+
adhoc: CouchbaseOrm::N1ql.config[:adhoc]
65+
)
6166
values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key)
6267
current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key))
6368
if result_modifier

lib/couchbase-orm/relation.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Relation
33
extend ActiveSupport::Concern
44

55
class CouchbaseOrm_Relation
6-
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false, strict_loading: strict_loading = false)
6+
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false, strict_loading: strict_loading = false, query_options: query_options = {})
77
CouchbaseOrm::logger.debug "CouchbaseOrm_Relation init: #{model} where:#{where.inspect} not:#{_not.inspect} order:#{order.inspect} limit: #{limit} strict_loading: #{strict_loading}"
88
@model = model
99
@limit = limit
@@ -12,6 +12,7 @@ def initialize(model:, where: where = nil, order: order = nil, limit: limit = ni
1212
@order = merge_order(**order) if order
1313
@where = merge_where(where, _not) if where
1414
@strict_loading = strict_loading
15+
@query_options = query_options || {}
1516
CouchbaseOrm::logger.debug "- #{to_s}"
1617
end
1718

@@ -70,6 +71,10 @@ def strict_loading?
7071
!!@strict_loading
7172
end
7273

74+
def with(opts = {})
75+
CouchbaseOrm_Relation.new(**initializer_arguments.merge(query_options: @query_options.merge(opts)))
76+
end
77+
7378
def first
7479
n1ql_query, params = self.limit(1).to_n1ql_with_params
7580
result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
@@ -168,7 +173,7 @@ def build_limit
168173
end
169174

170175
def initializer_arguments
171-
{ model: @model, order: @order, where: @where, limit: @limit, strict_loading: @strict_loading }
176+
{ model: @model, order: @order, where: @where, limit: @limit, strict_loading: @strict_loading, query_options: @query_options }
172177
end
173178

174179
def merge_order(*lorder, **horder)
@@ -238,7 +243,7 @@ def build_update_with_params(params, **cond)
238243
end
239244

240245
def build_query_options(positional_parameters: [])
241-
opts = { scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency] }
246+
opts = CouchbaseOrm::N1ql.config.merge(@query_options)
242247
opts[:positional_parameters] = positional_parameters unless positional_parameters.empty?
243248
Couchbase::Options::Query.new(**opts)
244249
end
@@ -261,7 +266,7 @@ def relation
261266

262267
delegate :ids, :update_all, :delete_all, :count, :empty?, :filter, :reduce, :find_by, to: :all
263268

264-
delegate :where, :not, :order, :limit, :all, :strict_loading, :strict_loading?, to: :relation
269+
delegate :where, :not, :order, :limit, :all, :strict_loading, :strict_loading?, :with, to: :relation
265270
end
266271
end
267272
end

spec/n1ql_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,25 @@ class N1QLTest < CouchbaseOrm::Base
196196
end
197197
end
198198

199+
it "should use adhoc: true by default (no prepared statement plan caching)" do
200+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
201+
N1QLTest.by_rating_reverse()
202+
end
203+
204+
it "should allow overriding adhoc per call to enable plan caching" do
205+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
206+
N1QLTest.by_rating_reverse(adhoc: false)
207+
end
208+
209+
it "should respect N1ql.config adhoc setting" do
210+
default_config = CouchbaseOrm::N1ql.config
211+
CouchbaseOrm::N1ql.config({ adhoc: false })
212+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
213+
N1QLTest.by_rating_reverse()
214+
ensure
215+
CouchbaseOrm::N1ql.config(default_config)
216+
end
217+
199218
after(:all) do
200219
N1QLTest.delete_all
201220
end

spec/relation_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,5 +501,38 @@ def self.active
501501
end
502502
end
503503
end
504+
505+
it "should use adhoc: true by default (no prepared statement plan caching)" do
506+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
507+
RelationModel.where(active: true).ids
508+
end
509+
510+
describe "adhoc option via with" do
511+
it "should return a relation when calling with(adhoc:)" do
512+
expect(RelationModel.all.with(adhoc: false)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation)
513+
end
514+
515+
it "should pass adhoc: false to query options when set on the relation" do
516+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
517+
RelationModel.where(active: true).with(adhoc: false).ids
518+
end
519+
520+
it "should override N1ql.config adhoc when set on the relation" do
521+
default_config = CouchbaseOrm::N1ql.config
522+
CouchbaseOrm::N1ql.config(adhoc: false)
523+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
524+
RelationModel.where(active: true).with(adhoc: true).ids
525+
ensure
526+
CouchbaseOrm::N1ql.config(default_config)
527+
end
528+
529+
it "should be chainable with other relation methods" do
530+
m1 = RelationModel.create!(active: true, age: 10)
531+
_m2 = RelationModel.create!(active: false, age: 20)
532+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
533+
result = RelationModel.where(active: true).order(:age).with(adhoc: false).to_a
534+
expect(result).to match_array([m1])
535+
end
536+
end
504537
end
505538

0 commit comments

Comments
 (0)