Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,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.2_amd64.deb
- run: sudo apt install ./libtinfo5_6.3-2ubuntu0.2_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