Class: Superstore::Adapters::JsonbAdapter

Inherits:
AbstractAdapter show all
Defined in:
lib/superstore/adapters/jsonb_adapter.rb

Defined Under Namespace

Classes: QueryBuilder

Constant Summary collapse

PRIMARY_KEY_COLUMN =
'id'.freeze
OJ_OPTIONS =
{mode: :compat}
JSON_FUNCTIONS =
{
  # SELECT jsonb_slice('{"b": 2, "c": 3, "a": 4}', '{b, c}');
  'jsonb_slice(data jsonb, keys text[])' => %{
    SELECT json_object_agg(key, value)::jsonb
    FROM (
      SELECT * FROM jsonb_each(data)
    ) t
    WHERE key =ANY(keys);
  },

  # SELECT jsonb_merge('{"a": 1}', '{"b": 2, "c": 3, "a": 4}');
  'jsonb_merge(data jsonb, merge_data jsonb)' => %{
    SELECT json_object_agg(key, value)::jsonb
    FROM (
      WITH to_merge AS (
        SELECT * FROM jsonb_each(merge_data)
      )
      SELECT *
      FROM jsonb_each(data)
      WHERE key NOT IN (SELECT key FROM to_merge)
      UNION ALL
      SELECT * FROM to_merge
    ) t;
  },

  # SELECT jsonb_delete('{"b": 2, "c": 3, "a": 4}', '{b, c}');
  'jsonb_delete(data jsonb, keys text[])' => %{
    SELECT json_object_agg(key, value)::jsonb
    FROM (
      SELECT * FROM jsonb_each(data)
      WHERE key <>ALL(keys)
    ) t;
  },
}

Instance Attribute Summary

Attributes inherited from AbstractAdapter

#config

Instance Method Summary collapse

Methods inherited from AbstractAdapter

#batch, #batching?, #execute_batchable, #initialize

Constructor Details

This class inherits a constructor from Superstore::Adapters::AbstractAdapter

Instance Method Details

#active_record_klassObject



99
100
101
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 99

def active_record_klass
  @active_record_klass ||= ActiveRecord::Base
end

#active_record_klass=(klass) ⇒ Object



95
96
97
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 95

def active_record_klass=(klass)
  @active_record_klass = klass
end

#connectionObject



91
92
93
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 91

def connection
  active_record_klass.connection
end

#create_ids_where_clause(ids) ⇒ Object



184
185
186
187
188
189
190
191
192
193
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 184

def create_ids_where_clause(ids)
  ids = ids.first if ids.is_a?(Array) && ids.one?

  if ids.is_a?(Array)
    id_list = ids.map { |id| quote(id) }.join(',')
    "#{primary_key_column} IN (#{id_list})"
  else
    "#{primary_key_column} = #{quote(ids)}"
  end
end

#create_table(table_name, options = {}) ⇒ Object



172
173
174
175
176
177
178
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 172

def create_table(table_name, options = {})
  ActiveRecord::Migration.create_table table_name, id: false do |t|
    t.string :id, null: false
    t.jsonb :document, null: false
  end
  connection.execute "ALTER TABLE \"#{table_name}\" ADD CONSTRAINT #{table_name}_pkey PRIMARY KEY (id)"
end

#define_jsonb_functions!Object



243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 243

def define_jsonb_functions!
  JSON_FUNCTIONS.each do |signature, body|
    connection.execute %{
      CREATE OR REPLACE FUNCTION public.#{signature}
      RETURNS jsonb
      IMMUTABLE
      LANGUAGE sql
      AS $$
        #{body}
      $$;
    }
  end
end

#delete(table, ids) ⇒ Object



160
161
162
163
164
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 160

def delete(table, ids)
  statement = "DELETE FROM #{table} WHERE #{create_ids_where_clause(ids)}"

  execute_batchable statement
end

#drop_table(table_name) ⇒ Object



180
181
182
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 180

def drop_table(table_name)
  ActiveRecord::Migration.drop_table table_name
end

#execute(statement) ⇒ Object



103
104
105
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 103

def execute(statement)
  connection.execute statement
end

#execute_batch(statements) ⇒ Object



166
167
168
169
170
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 166

def execute_batch(statements)
  connection.transaction do
    execute(statements * ";\n")
  end
end

#fields_to_postgres_array(fields) ⇒ Object



199
200
201
202
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 199

def fields_to_postgres_array(fields)
  quoted_fields = fields.map { |field| quote(field) }.join(',')
  "ARRAY[#{quoted_fields}]"
end

#insert(table, id, attributes) ⇒ Object



136
137
138
139
140
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 136

def insert(table, id, attributes)
  not_nil_attributes = attributes.reject { |key, value| value.nil? }
  statement = "INSERT INTO #{table} (#{primary_key_column}, document) VALUES (#{quote(id)}, #{to_quoted_jsonb(not_nil_attributes)})"
  execute_batchable statement
end

#primary_key_columnObject



87
88
89
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 87

def primary_key_column
  PRIMARY_KEY_COLUMN
end

#quote(value) ⇒ Object



195
196
197
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 195

def quote(value)
  connection.quote(value)
end

#scroll(scope, batch_size) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 120

def scroll(scope, batch_size)
  statement   = QueryBuilder.new(self, scope).to_query
  cursor_name = "cursor_#{SecureRandom.hex(6)}"
  fetch_sql   = "FETCH FORWARD #{batch_size} FROM #{cursor_name}"

  connection.transaction do
    connection.execute "DECLARE #{cursor_name} NO SCROLL CURSOR FOR (#{statement})"

    while (batch = connection.execute(fetch_sql)).any?
      batch.each do |result|
        yield result[primary_key_column], Oj.compat_load(result['document'])
      end
    end
  end
end

#select(scope) ⇒ Object



112
113
114
115
116
117
118
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 112

def select(scope)
  statement = QueryBuilder.new(self, scope).to_query

  connection.execute(statement).each do |result|
    yield result[primary_key_column], Oj.compat_load(result['document'])
  end
end

#to_ids(scope) ⇒ Object



107
108
109
110
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 107

def to_ids(scope)
  statement = QueryBuilder.new(self, scope.select('id')).to_query
  connection.select_values(statement)
end

#to_quoted_jsonb(data) ⇒ Object



205
206
207
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 205

def to_quoted_jsonb(data)
  "#{quote(Oj.dump(data, OJ_OPTIONS))}::JSONB"
end

#update(table, id, attributes) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/superstore/adapters/jsonb_adapter.rb', line 142

def update(table, id, attributes)
  return if attributes.empty?

  not_nil_attributes = attributes.reject { |key, value| value.nil? }
  nil_attributes = attributes.select { |key, value| value.nil? }

  if not_nil_attributes.any? && nil_attributes.any?
    value_update = "jsonb_merge(jsonb_delete(document, #{fields_to_postgres_array(nil_attributes.keys)}), #{to_quoted_jsonb(not_nil_attributes)})"
  elsif not_nil_attributes.any?
    value_update = "jsonb_merge(document, #{to_quoted_jsonb(not_nil_attributes)})"
  elsif nil_attributes.any?
    value_update = "jsonb_delete(document, #{fields_to_postgres_array(nil_attributes.keys)})"
  end

  statement = "UPDATE #{table} SET document = #{value_update} WHERE #{primary_key_column} = #{quote(id)}"
  execute_batchable statement
end