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
92
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 83

def all
  response = run

  while continue?(response)
    @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



399
400
401
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 399

def average(column)
  raise NotImplementedError
end

#begins_with(condition) ⇒ Object

Perform BEGINS_WITH comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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



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

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



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

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

#contains(condition) ⇒ Object

Perform CONTAINS comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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

#continue?(previous_response) ⇒ Boolean (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.

Check if request needs to be continued.

Parameters:

  • previous_response (Response)

    deserialized response from a previous operation

Returns:

  • (Boolean)

Since:

  • 0.1.2



604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 604

def continue?(previous_response)
  return false unless previous_response.last_evaluated_key

  if @options[:limit]
    if @options[:limit] > previous_response.count
      @options[:limit] = @options[:limit] - previous_response.count

      true
    else
      false
    end
  else
    true
  end
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



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

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

  response = run

  while continue?(response)
    @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



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

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



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

def each
  response = run

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

  while continue?(response)
    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



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

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



458
459
460
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 458

def exists?
  !count.zero?
end

#ge(condition) ⇒ Object

Perform GE comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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

#gt(condition) ⇒ Object

Perform GT comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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

#index(name) ⇒ Object

Tells DynamoDB which index to use.

Returns:

  • self

Since:

  • 0.1.0



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

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



434
435
436
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 434

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



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

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



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

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



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

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

#lt(condition) ⇒ Object

Perform LT comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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



412
413
414
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 412

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



423
424
425
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 423

def min(column)
  raise NotImplementedError
end

#negate!Object

This method is not implemented.

Raises:

  • (NotImplementedError)

Since:

  • 0.1.0



492
493
494
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 492

def negate!
  raise NotImplementedError
end

#not_contains(condition) ⇒ Object

Perform NOT_CONTAINS comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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

#not_null(column) ⇒ Object

Perform NOT_NULL comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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

#null(column) ⇒ Object

Perform NULL comparison.

Returns:

  • self

Since:

  • 0.1.0



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

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



377
378
379
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 377

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



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

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



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

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



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

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



445
446
447
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 445

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



592
593
594
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 592

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



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

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



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
581
582
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 547

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



388
389
390
# File 'lib/lotus/model/adapters/dynamodb/query.rb', line 388

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



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

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

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

  self
end