Skip to content
Open
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ jobs:
name: ${{ matrix.ruby }} rails-${{ matrix.active-model }} couchbase-${{ matrix.couchbase }}
steps:
- uses: actions/checkout@v3
- run: sudo apt-get update && sudo apt-get install libevent-dev libev-dev python3-httplib2
- run: wget http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.1_amd64.deb
- run: sudo apt install ./libtinfo5_6.3-2ubuntu0.1_amd64.deb
- run: |
sudo apt-get update
sudo apt-get install -y libevent-dev libev-dev python3-httplib2
sudo apt-get install -y libtinfo5 || sudo apt-get install -y libtinfo6
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
Expand Down
41 changes: 41 additions & 0 deletions docusaurus/docs/tutorial-ruby-couchbase-orm/07-sqlpp-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,47 @@ docs = N1QLTest.by_custom_rating_values(key: [[1, 2]]).collect { |ob| ob.name }

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

## 7.8 Prepared Statement Plan Caching

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.

### Default behaviour

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.

### Enabling caching for a specific call

Pass `adhoc: false` directly to the query method to prepare and cache the plan (useful for frequently repeated queries):

```ruby
# Cache the plan for this query
N1QLTest.by_rating(key: 1, adhoc: false)

# Relation query with plan caching
User.where(country: 'FR').with(adhoc: false).to_a
```

### Enabling caching for a specific `n1ql` definition

Set `adhoc: false` in the macro options to always cache the plan for that particular query:

```ruby
n1ql :by_stable_filter, emit_key: [:name], adhoc: false
```

### Changing the global default

Override the thread-local config to change the default for all queries in the current thread:

```ruby
# Enable plan caching for all queries in this thread
CouchbaseOrm::N1ql.config(adhoc: false)
```

### Override priority

From highest to lowest: **per-call kwarg** > **per-`n1ql`-definition option** > **`N1ql.config`** > **default (`true`)**.

## 7.7 Indexing for SQL++

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.
Expand Down
13 changes: 9 additions & 4 deletions lib/couchbase-orm/n1ql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module N1ql
extend ActiveSupport::Concern
NO_VALUE = :no_value_specified
DEFAULT_SCAN_CONSISTENCY = :request_plus
DEFAULT_ADHOC = true
# sanitize for injection query
def self.sanitize(value)
if value.is_a?(String)
Expand All @@ -22,9 +23,10 @@ def self.sanitize(value)

def self.config(new_config = nil)
Thread.current['__couchbaseorm_n1ql_config__'] = new_config if new_config
Thread.current['__couchbaseorm_n1ql_config__'] || {
scan_consistency: DEFAULT_SCAN_CONSISTENCY
}
{
scan_consistency: DEFAULT_SCAN_CONSISTENCY,
adhoc: DEFAULT_ADHOC
}.merge(Thread.current['__couchbaseorm_n1ql_config__'] || {})
end

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

singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier|
opts = options.merge(opts).reverse_merge(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
opts = options.merge(opts).reverse_merge(
scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency],
adhoc: CouchbaseOrm::N1ql.config[:adhoc]
)
values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key)
current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key))
if result_modifier
Expand Down
13 changes: 9 additions & 4 deletions lib/couchbase-orm/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Relation
extend ActiveSupport::Concern

class CouchbaseOrm_Relation
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false, strict_loading: strict_loading = false)
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false, strict_loading: strict_loading = false, query_options: query_options = {})
CouchbaseOrm::logger.debug "CouchbaseOrm_Relation init: #{model} where:#{where.inspect} not:#{_not.inspect} order:#{order.inspect} limit: #{limit} strict_loading: #{strict_loading}"
@model = model
@limit = limit
Expand All @@ -12,6 +12,7 @@ def initialize(model:, where: where = nil, order: order = nil, limit: limit = ni
@order = merge_order(**order) if order
@where = merge_where(where, _not) if where
@strict_loading = strict_loading
@query_options = query_options || {}
CouchbaseOrm::logger.debug "- #{to_s}"
end

Expand Down Expand Up @@ -70,6 +71,10 @@ def strict_loading?
!!@strict_loading
end

def with(opts = {})
CouchbaseOrm_Relation.new(**initializer_arguments.merge(query_options: @query_options.merge(opts)))
end

def first
n1ql_query, params = self.limit(1).to_n1ql_with_params
result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
Expand Down Expand Up @@ -168,7 +173,7 @@ def build_limit
end

def initializer_arguments
{ model: @model, order: @order, where: @where, limit: @limit, strict_loading: @strict_loading }
{ model: @model, order: @order, where: @where, limit: @limit, strict_loading: @strict_loading, query_options: @query_options }
end

def merge_order(*lorder, **horder)
Expand Down Expand Up @@ -238,7 +243,7 @@ def build_update_with_params(params, **cond)
end

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

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

delegate :where, :not, :order, :limit, :all, :strict_loading, :strict_loading?, to: :relation
delegate :where, :not, :order, :limit, :all, :strict_loading, :strict_loading?, :with, to: :relation
end
end
end
19 changes: 19 additions & 0 deletions spec/n1ql_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,25 @@ class N1QLTest < CouchbaseOrm::Base
end
end

it "should use adhoc: true by default (no prepared statement plan caching)" do
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
N1QLTest.by_rating_reverse()
end

it "should allow overriding adhoc per call to enable plan caching" do
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
N1QLTest.by_rating_reverse(adhoc: false)
end

it "should respect N1ql.config adhoc setting" do
default_config = CouchbaseOrm::N1ql.config
CouchbaseOrm::N1ql.config({ adhoc: false })
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
N1QLTest.by_rating_reverse()
ensure
CouchbaseOrm::N1ql.config(default_config)
end

after(:all) do
N1QLTest.delete_all
end
Expand Down
33 changes: 33 additions & 0 deletions spec/relation_spec.rb
Comment thread
pimpin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -501,5 +501,38 @@ def self.active
end
end
end

it "should use adhoc: true by default (no prepared statement plan caching)" do
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
RelationModel.where(active: true).ids
end

describe "adhoc option via with" do
it "should return a relation when calling with(adhoc:)" do
expect(RelationModel.all.with(adhoc: false)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation)
end

it "should pass adhoc: false to query options when set on the relation" do
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
RelationModel.where(active: true).with(adhoc: false).ids
end

it "should override N1ql.config adhoc when set on the relation" do
default_config = CouchbaseOrm::N1ql.config
CouchbaseOrm::N1ql.config(adhoc: false)
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
RelationModel.where(active: true).with(adhoc: true).ids
ensure
CouchbaseOrm::N1ql.config(default_config)
end

it "should be chainable with other relation methods" do
m1 = RelationModel.create!(active: true, age: 10)
_m2 = RelationModel.create!(active: false, age: 20)
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
result = RelationModel.where(active: true).order(:age).with(adhoc: false).to_a
expect(result).to match_array([m1])
end
end
end

Loading