Skip to content
Open
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
6 changes: 5 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ AllCops:
- 'spec/**/*'
- 'test/**/*'
- 'bin/**/*'
- 'lib/simple/store.old/**/*'
- 'spec.old/**/*'
- 'tasks/release.rake'
- '*.gemspec'
- 'Gemfile'
- 'Rakefile'

- 'scripts/*'
- 'config/*'

Metrics/LineLength:
Max: 120

Expand Down
2 changes: 2 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ end

Reload.load_file "config/console-init.rb"

require "benchmark"

require "irb"
require "irb/completion"

Expand Down
3 changes: 3 additions & 0 deletions lib/simple-store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# rubocop:disable Naming/FileName

require "simple/store"
6 changes: 6 additions & 0 deletions lib/simple/sql/helpers/immutable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def respond_to_missing?(method_name, include_private = false)
super
end

def to_hash
@hsh
end

def inspect
"<Immutable: #{@hsh.inspect}>"
end
Expand All @@ -65,6 +69,8 @@ def ==(other)

if $PROGRAM_NAME == __FILE__

# rubocop:disable Metrics/AbcSize

require "test-unit"

class Simple::SQL::Helpers::Immutable::TestCase < Test::Unit::TestCase
Expand Down
92 changes: 80 additions & 12 deletions lib/simple/sql/helpers/row_converter.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,99 @@
require_relative "./immutable"
require_relative "../reflection"

module Simple::SQL::Helpers::RowConverter
SELF = self

Reflection = ::Simple::SQL::Reflection

# returns an array of converted records
def self.convert_row(records, into:, associations: nil, fq_table_name: nil)
hsh = records.first
return records unless hsh

converter = if into == :struct
StructConverter.for(attributes: hsh.keys, associations: associations)
elsif into == :immutable
ImmutableConverter.new(type: into, associations: associations)
elsif into.respond_to?(:new_from_row)
TypeConverter2.new(type: into, associations: associations, fq_table_name: fq_table_name)
else
TypeConverter.new(type: into, associations: associations)
end

converter = build_converter(hsh, into: into, associations: associations, fq_table_name: fq_table_name)
records.map { |record| converter.convert_row(record) }
end

# This method builds and returns a converter object which provides a convert_row method.
# which in turn is able to convert a Hash (the row as read from the query) into the
# target type +into+
#
# The into: parameter designates the target type. The following scenarios are supported:
#
# 1. Converting into Structs (into == :struct)
#
# Converts records into a dynamically created struct. This has the advantage of being
# really fast.
#
# 2. Converting into Immutables (into == :immutable)
#
# Converts records into a Immutable object. The resulting objects implement getters for all
# existing attributes, including embedded arrays and hashes, but do not offer setters -
# resulting in de-facto readonly objects.
#
# 3. Custom targets based on table name
#
# If an object is passed in that implements a "from_complete_row" method, and the query
# results are complete - i.e. all columns in the underlying table (via fq_table_name)
# are in the resulting records - this mode uses the +into+ object's +from_complete_row+
# method to convert the records. This mode is used by Simple::Store.
#
# If the records are not complete, and contain more than a single value this mode behaves
# like <tt>:immutable</tt>.
#
# If the records are not complete and contain only a single value this mode returns that
# value for each record.
#
# 4. Other custom types
#
# If an object is passed in that does not implement "from_complete_row" the object's
# +new+ method is called on each row's hash. This allows to run queries against all classes
# that can be built from a Hash, for example OpenStructs.
def self.build_converter(hsh, into:, associations: nil, fq_table_name: nil)
case into
when :struct
StructConverter.for(attributes: hsh.keys, associations: associations)
when :immutable
ImmutableConverter.new(type: into, associations: associations)
else
build_custom_type_converter(hsh, into: into, associations: associations, fq_table_name: fq_table_name)
end
end

def self.build_custom_type_converter(hsh, into:, associations: nil, fq_table_name: nil)
# If the into object does not provide a :from_complete_row method, we'll use
# a converter that creates objects via ".new"
unless into.respond_to?(:from_complete_row)
return TypeConverter.new(type: into, associations: associations)
end

# If the query results are complete we'll use the into object
required_columns = Reflection.columns(fq_table_name)
actual_columns = hsh.keys.map(&:to_s)

if (required_columns - actual_columns).empty?
return CompleteRowConverter.new(type: into, associations: associations, fq_table_name: fq_table_name)
end

# If the query only has a single value we'll extract the value
return SingleValueConverter.new if hsh.count == 1

# Otherwise we'll fall back to :immutable
ImmutableConverter.new(type: into, associations: associations)
end

def self.convert(record, into:) # :nodoc:
ary = convert_row([record], into: into)
ary.first
end

class SingleValueConverter
def convert_row(hsh)
hsh.values.first
end
end

class TypeConverter #:nodoc:
def initialize(type:, associations:)
@type = type
Expand Down Expand Up @@ -64,15 +132,15 @@ def build_row_in_target_type(hsh)
end
end

class TypeConverter2 < TypeConverter #:nodoc:
class CompleteRowConverter < TypeConverter #:nodoc:
def initialize(type:, associations:, fq_table_name:)
super(type: type, associations: associations)
@fq_table_name = fq_table_name
end

def convert_row(hsh)
hsh = convert_associations(hsh) if @associations
@type.new_from_row hsh, fq_table_name: @fq_table_name
@type.from_complete_row hsh, fq_table_name: @fq_table_name
end
end

Expand Down
162 changes: 162 additions & 0 deletions lib/simple/store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
require "simple-sql"

module Simple; end
module Simple::Store; end

require_relative "store/errors"
require_relative "store/model"
require_relative "store/metamodel"
require_relative "store/registry"
require_relative "store/storage"
require_relative "store/create"
require_relative "store/delete"
require_relative "store/find"
require_relative "store/validation"
require_relative "store/update"

# A simple document store
#
# The document store allows one to create, update, destroy records of specific
# types. In doing so validations are applied at the right times.
#
# A type has these attributes:
#
# - a table name.
# - a fixed list of attributes. A basic list is derived from the database layout.
# - a fixed list of associations. A basic list is derived from the database layout.
#
# Custom types provide their own list of attributes and associations. Both attributes
# and associations are registered with a name and a type.
#
# All attributes that do not exist as columns in the table are called "dynamic"
# attributes and read from a meta_data JSONB attribute. Type conversions are done to
# convert from and to types that are not supported by JSON (mostly dates and boolean.)
#
module Simple::Store
extend self

# Runs a query and returns the first result row of a query.
#
# Examples:
#
# - <tt>Simple::Store.ask "SELECT * FROM users WHERE email=$? LIMIT 1", "foo@local"</tt>
def ask(*args, into: nil)
into ||= Storage
Simple::SQL.ask(*args, into: into)
end

# Runs a query and returns all resulting row of a query, as Simple::Store::Model objects.
#
# Examples:
#
# - <tt>Simple::Store.all "SELECT * FROM users WHERE email=$?", "foo@local"</tt>
def all(*args, into: nil)
into ||= Storage
Simple::SQL.all(*args, into: into)
end

# build one or more objects, verify, and save.
#
# Parameters:
#
# - metamodel: the type [Metamodel, String]
# - values: a Hash or an Array of Hashes
#
# Returns the newly created model or an array of newly created models.
#
# If validation fails for at least one of the objects no objects will be created.
def create!(metamodel, values, on_conflict: nil)
metamodel = Metamodel.resolve(metamodel)

if values.is_a?(Array)
Create.create!(metamodel, values, on_conflict: on_conflict)
else
Create.create!(metamodel, [values], on_conflict: on_conflict).first
end
end

# build one or more objects
#
# Parameters:
#
# - metamodel: the type [Metamodel, String]
# - values: a Hash or an Array of Hashes
#
# Returns the built model or an array of built models.
def build(metamodel, values)
metamodel = Metamodel.resolve(metamodel)

if values.is_a?(Array)
Create.build(metamodel, values)
else
Create.build(metamodel, [values]).first
end
end

# Validate the passed in models.
#
# Raises an error if any model is invalid.
def validate!(models)
Validation.validate!(Array(models))
end

# Delete one or more models from the database.
#
# def delete!(models)
# def delete!(metamodels, ids)
#
# Returns a copy of the deleted models
def delete!(*args)
Delete.delete args, force: true
end

# def delete(models)
# def delete(metamodels, ids)
def delete(*args)
Delete.delete args, force: false
end

# -- finding ----------------------------------------------------------------

# Reload a model fro the database
def reload(model)
raise ArgumentError, "Cannot reload a unsaved model (of type #{model.metamodel.name.inspect})" if model.new_record?

find(model.metamodel, model.id)
end

# Find a nuber of models in the database
#
# Parameters:
#
# - metamodels: a Metamodel, String, or an Array of Metamodels and/or Strings
# - ids: an ID value or an array of ids.
#
# Returns a array of newly built models.
def find(metamodels, ids)
expect! ids => [Array, Integer]

metamodels = Metamodel.resolve(metamodels)

if ids.is_a?(Integer)
Simple::Store::Find.find!(metamodels, [ids]).first
else
Simple::Store::Find.find!(metamodels, ids)
end
end

# verify, and save one or more models.
def save!(models)
return save!([models]).first unless models.is_a?(Array)

validate! models

models.map do |model|
if model.new_record?
Create.create_model_during_save(model)
else
Update.update_model(model)
end
end
end
end
39 changes: 39 additions & 0 deletions lib/simple/store/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# require "active_support/core_ext/string/inflections"

module Simple::Store::Create
Storage = Simple::Store::Storage

extend self

def create_model_during_save(model)
created_model = create!(model.metamodel, [model.to_hash], on_conflict: nil).first
model.send(:set_id_by_trusted_caller, created_model.id)
created_model
end

def create!(metamodel, values, on_conflict:)
models = build(metamodel, values)

validate_on_create! models
Simple::Store.validate! models

records = Storage.convert_to_storage_representation(metamodel, models)
Simple::SQL.insert(metamodel.table_name, records, into: Storage, on_conflict: on_conflict)
end

def build(metamodel, values)
values.map do |value|
metamodel.build({}).assign(value)
end
end

private

def validate_on_create!(models)
return validate_on_create!([models]) unless models.is_a?(Array)

models.each do |model|
raise ArgumentError, "You cannot pass in an :id attribute" unless model.id.nil?
end
end
end
Loading