Class: Lotus::Model::Adapters::Dynamodb::Query

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Enumerable
Defined in:
lib/lotus/model/adapters/dynamodb/query.rb

Overview

Query DynamoDB table with a powerful API.

All the methods are chainable, it allows advanced composition of options.

This works as a lazy filtering mechanism: the records are fetched from the DynamoDB only when needed.

It implements Ruby’s ‘Enumerable` and borrows some methods from `Array`. Expect a query to act like them.

Examples:


query.where(language: 'ruby')
     .and(framework: 'lotus')
     .all

# the records are fetched only when we invoke #all

Since:

  • 0.1.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dataset, collection, &blk) ⇒ Query

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initialize a query

Parameters:

Since:

  • 0.1.0



67
68
69
70
71
72
73
74
75
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 67

def initialize(dataset, collection, &blk)
  @dataset    = dataset
  @collection = collection

  @operation  = :scan
  @options    = {}

  instance_eval(&blk) if block_given?
end

Instance Attribute Details

#operationObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



50
51
52
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 50

def operation
  @operation
end

#optionsObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



56
57
58
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 56

def options
  @options
end

Instance Method Details

#allArray

Resolves the query by fetching records from the database and translating them into entities.

Returns:

  • (Array)

    a collection of entities

Since:

  • 0.1.0



83
84
85
86
87
88
89
90
91
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 83

def all
  response = run
  while !@options[:limit] && response.last_evaluated_key
    @options[:exclusive_start_key] = response.last_evaluated_key
    response = run(response)
  end

  @collection.deserialize(response.entities)
end

#average(column) ⇒ Object Also known as: avg

This method is not implemented. DynamoDB does not provide average.

Parameters:

  • column (Symbol)

    the column name

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



397
398
399
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 397

def average(column)
  raise NotImplementedError
end

#begins_with(condition) ⇒ Object

Perform BEGINS_WITH comparison.

Returns:

  • self

Since:

  • 0.1.0



263
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 263

def begins_with(condition); comparison(condition, 'BEGINS_WITH'); end

#comparison(condition, operator) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Perform comparison operation.

Returns:

  • self

Since:

  • 0.1.0



285
286
287
288
289
290
291
292
293
294
295
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 285

def comparison(condition, operator)
  key = key_for_condition(condition)
  serialized = serialize_condition(condition, operator: operator)

  if serialized
    @options[key] ||= {}
    @options[key].merge!(serialized)
  end

  self
end

#consistentObject

Tells DynamoDB to use consistent reads.

Returns:

  • self

Since:

  • 0.1.0



499
500
501
502
503
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 499

def consistent
  query
  @options[:consistent_read] = true
  self
end

#contains(condition) ⇒ Object

Perform CONTAINS comparison.

Returns:

  • self

Since:

  • 0.1.0



249
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 249

def contains(condition); comparison(condition, 'CONTAINS'); end

#countFixnum

Returns a count of the records for the current conditions.

Examples:


query.where(author_id: 23).count # => 5

Returns:

  • (Fixnum)

Since:

  • 0.1.0



471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 471

def count
  @options[:select] = "COUNT"
  @options.delete(:attributes_to_get)

  response = run

  while !@options[:limit] && response.last_evaluated_key
    @options[:exclusive_start_key] = response.last_evaluated_key
    response = run(response)
  end

  response.count
end

#desc(*columns) ⇒ Object

Specify the descending order of the records, sorted by the range key.

Returns:

  • self

See Also:

Since:

  • 0.1.0



344
345
346
347
348
349
350
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 344

def desc(*columns)
  warn "DynamoDB only supports order by range_key" if columns.any?

  query
  @options[:scan_index_forward] = false
  self
end

#eachInteger

Iterates over fetched records.

Returns:

  • (Integer)

    total count of records

Since:

  • 0.1.1



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 98

def each
  response = run
  entities = @collection.deserialize(response.entities)
  entities.each { |x| yield(x) }

  while !@options[:limit] && response.last_evaluated_key
    response.entities = []

    @options[:exclusive_start_key] = response.last_evaluated_key
    response = run(response)

    entities = @collection.deserialize(response.entities)
    entities.each { |x| yield(x) }
  end

  response.count
end

#exclude(condition) ⇒ Object Also known as: not, ne

Logical negation of a #where condition.

It accepts a ‘Hash` with only one pair. The key must be the name of the column expressed as a `Symbol`. The value is the one used by the internal filtering logic.

Examples:

Fixed value


query.exclude(language: 'java')

Multiple conditions


query.exclude(language: 'java')
     .exclude(company: 'enterprise')

Parameters:

  • condition (Hash)

Returns:

  • self

Since:

  • 0.1.0



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 201

def exclude(condition)
  key = key_for_condition(condition)
  serialized = serialize_condition(condition, negate: true)

  if serialized
    @options[key] ||= {}
    @options[key].merge!(serialized)
  end

  self
end

#exists?TrueClass, FalseClass Also known as: exist?

Checks if at least one record exists for the current conditions.

Examples:


query.where(author_id: 23).exists? # => true

Returns:

  • (TrueClass, FalseClass)

Since:

  • 0.1.0



456
457
458
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 456

def exists?
  !count.zero?
end

#ge(condition) ⇒ Object

Perform GE comparison.

Returns:

  • self

Since:

  • 0.1.0



235
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 235

def ge(condition); comparison(condition, 'GE'); end

#gt(condition) ⇒ Object

Perform GT comparison.

Returns:

  • self

Since:

  • 0.1.0



242
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 242

def gt(condition); comparison(condition, 'GT'); end

#index(name) ⇒ Object

Tells DynamoDB which index to use.

Returns:

  • self

Since:

  • 0.1.0



510
511
512
513
514
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 510

def index(name)
  query
  @options[:index_name] = name.to_s
  self
end

#interval(column) ⇒ Object

This method is not implemented. DynamoDB does not provide interval.

Parameters:

  • column (Symbol)

    the column name

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



432
433
434
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 432

def interval(column)
  raise NotImplementedError
end

#key_for_condition(condition) ⇒ Symbol (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Return proper options key for a given condition.

Parameters:

  • condition (Hash)

    the condition

Returns:

  • (Symbol)

    the key

Since:

  • 0.1.0



525
526
527
528
529
530
531
532
533
534
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 525

def key_for_condition(condition)
  if @dataset.key?(condition.keys.first, @options[:index_name])
    query
    :key_conditions
  elsif operation == :scan
    :scan_filter
  else
    :query_filter
  end
end

#le(condition) ⇒ Object

Perform LE comparison.

Returns:

  • self

Since:

  • 0.1.0



221
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 221

def le(condition); comparison(condition, 'LE'); end

#limit(number) ⇒ Object

Limit the number of records to return.

Examples:


query.limit(1)

Parameters:

  • number (Fixnum)

Returns:

  • self

Since:

  • 0.1.0



363
364
365
366
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 363

def limit(number)
  @options[:limit] = number
  self
end

#lt(condition) ⇒ Object

Perform LT comparison.

Returns:

  • self

Since:

  • 0.1.0



228
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 228

def lt(condition); comparison(condition, 'LT'); end

#max(column) ⇒ Object

This method is not implemented. DynamoDB does not provide max.

Parameters:

  • column (Symbol)

    the column name

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



410
411
412
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 410

def max(column)
  raise NotImplementedError
end

#min(column) ⇒ Object

This method is not implemented. DynamoDB does not provide min.

Parameters:

  • column (Symbol)

    the column name

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



421
422
423
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 421

def min(column)
  raise NotImplementedError
end

#negate!Object

This method is not implemented.

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



490
491
492
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 490

def negate!
  raise NotImplementedError
end

#not_contains(condition) ⇒ Object

Perform NOT_CONTAINS comparison.

Returns:

  • self

Since:

  • 0.1.0



256
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 256

def not_contains(condition); comparison(condition, 'NOT_CONTAINS'); end

#not_null(column) ⇒ Object

Perform NOT_NULL comparison.

Returns:

  • self

Since:

  • 0.1.0



277
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 277

def not_null(column); comparison({ column => '' }, 'NOT_NULL'); end

#null(column) ⇒ Object

Perform NULL comparison.

Returns:

  • self

Since:

  • 0.1.0



270
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 270

def null(column); comparison({ column => '' }, 'NULL'); end

#offset(number) ⇒ Object

This method is not implemented. DynamoDB does not provide offset.

Parameters:

  • number (Fixnum)

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



375
376
377
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 375

def offset(number)
  raise NotImplementedError
end

#orObject

Sets DynamoDB ConditionalOperator to ‘OR`. This works query-wide so this method has no arguments.

Returns:

  • self

Since:

  • 0.1.0



176
177
178
179
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 176

def or
  @options[:conditional_operator] = "OR"
  self
end

#order(*columns) ⇒ Object Also known as: asc

Specify the ascending order of the records, sorted by the range key.

Returns:

  • self

See Also:

Since:

  • 0.1.0



327
328
329
330
331
332
333
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 327

def order(*columns)
  warn "DynamoDB only supports order by range_key" if columns.any?

  query
  @options[:scan_index_forward] = true
  self
end

#queryObject

Set operation to be query instead of scan.

Returns:

  • self

Since:

  • 0.1.0



121
122
123
124
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 121

def query
  @operation = :query
  self
end

#range(column) ⇒ Object

This method is not implemented. DynamoDB does not provide range.

Parameters:

  • column (Symbol)

    the column name

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



443
444
445
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 443

def range(column)
  raise NotImplementedError
end

#run(previous_response = nil) ⇒ Array (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Apply all the options and return a filtered collection.

Parameters:

  • previous_response (Response) (defaults to: nil)

    deserialized response from a previous operation

Returns:

  • (Array)

Since:

  • 0.1.0



590
591
592
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 590

def run(previous_response = nil)
  @dataset.public_send(operation, @options, previous_response)
end

#select(*columns) ⇒ Object

Select only the specified columns.

By default a query selects all the mapped columns.

Examples:

Single column


query.select(:name)

Multiple columns


query.select(:name, :year)

Parameters:

  • columns (Array<Symbol>)

Returns:

  • self

Since:

  • 0.1.0



314
315
316
317
318
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 314

def select(*columns)
  @options[:select] = "SPECIFIC_ATTRIBUTES"
  @options[:attributes_to_get] = columns.map(&:to_s)
  self
end

#serialize_condition(condition, operator: nil, negate: false) ⇒ Hash (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Serialize given condition for DynamoDB API.

Parameters:

  • condition (Hash)

    the condition

  • negate (Boolean) (defaults to: false)

    true when negative condition

Returns:

  • (Hash)

    the serialized condition

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 545

def serialize_condition(condition, operator: nil, negate: false)
  column, value = condition.keys.first, condition.values.first

  operator ||= case
  when value.is_a?(Array)
    negate ? nil : "IN"
  when value.is_a?(Range)
    negate ? nil : "BETWEEN"
  else
    negate ? "NE" : "EQ"
  end

  # Operator is not supported
  raise NotImplementedError unless operator

  values ||= case
  when value.is_a?(Range)
    [value.first, value.last]
  else
    [value].flatten
  end

  serialized = {
    column.to_s => {
      comparison_operator: operator,
    },
  }

  if !["NULL", "NOT_NULL"].include?(operator)
    serialized[column.to_s][:attribute_value_list] = values.map do |v|
      @dataset.format_attribute(column, v)
    end
  end

  serialized
end

#sum(column) ⇒ Object

This method is not implemented. DynamoDB does not provide sum.

Parameters:

  • column (Symbol)

    the column name

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



386
387
388
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 386

def sum(column)
  raise NotImplementedError
end

#where(condition) ⇒ Object Also known as: eq, in, between

Adds a condition that behaves like SQL ‘WHERE`.

It accepts a ‘Hash` with only one pair. The key must be the name of the column expressed as a `Symbol`. The value is the one used by the internal filtering logic.

Examples:

Fixed value


query.where(language: 'ruby')

Array


query.where(id: [1, 3])

Range


query.where(year: 1900..1982)

Multiple conditions


query.where(language: 'ruby')
     .where(framework: 'lotus')

Parameters:

  • condition (Hash)

Returns:

  • self

Since:

  • 0.1.0



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 154

def where(condition)
  key = key_for_condition(condition)
  serialized = serialize_condition(condition)

  if serialized
    @options[key] ||= {}
    @options[key].merge!(serialized)
  end

  self
end