Class: DBStruct

Inherits:
Object
  • Object
show all
Extended by:
ErrorHelpers
Includes:
ErrorHelpers
Defined in:
lib/dbstruct.rb,
lib/internal/util.rb,
lib/internal/error.rb,
lib/internal/dbstruct_base.rb

Overview

DBStruct presents a (SQLite3) database table in a way that closely mimics a Ruby Hash of Struct objects.

This is an abstract base class. To use it, you create a subclass using the with method and define the fields to be used. This also associates the new class with a Sequel dataset.

Subinstances behave similarly to Ruby’s Struct class. However, setting or reading a value accesses the corresponding database row.

Each object also knows its database row ID, an integer that can be used to look up this object. If a subinstance outlives its database row, attempting to access the fields will raise a MissingRowError. You can use methods #present? or #deleted? to test if this has happened.

Defined Under Namespace

Modules: ErrorHelpers Classes: BogoHash, Failure, FieldError, MissingRowError, TypeError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ErrorHelpers

check, oops

Constructor Details

#initialize(_id: nil, **kwargs) ⇒ DBStruct

Creates a new instance of class, creates the corresponding database row and initializes the fields. This only works if the class is a subclass of DBStruct; DBStruct itself cannot be instantiated.

If keyword arguments are given, they must correspond to the names of the fields and have values of the correct type or nil. Omitted arguments are equivalent to nil.

The keyword argument _id: is intended for internal use only and should not be used.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/internal/dbstruct_base.rb', line 20

def initialize(_id: nil,      # Internal use only!
               **kwargs)

  check("Attempted to instantiate abstract class DBStruct.") {
    self.class < DBStruct
  }
  
  @rowid = nil

  # Case 1: existing row
  if _id
    check("Using '_id' with other arguments!") { kwargs.empty? }

    @rowid = _id
    check("Row '#{_id}' does not exist!", MissingRowError) { present? }

    return
  end

  @rowid = dataset().insert({})
  set_fields(**kwargs)
end

Instance Attribute Details

#rowidObject (readonly)

Returns the numeric Row ID that is used as the primary key for this row in both the database and any BogoHash instances that may contain it.



7
8
9
# File 'lib/internal/dbstruct_base.rb', line 7

def rowid
  @rowid
end

Class Method Details

.items(*selectors) ⇒ Object

Return a DBStruct::BogoHash containing some or all of the items in this table. If arguments are given, they must correspond to group fields. In that case, the returned BogoHash contains only those items whose values in those field match the argument.

(This is roughly equivalent to using the Sequel where method to narrow a Dataset.)

So

Books.items("non-fiction", "dragons")

will return a BogoHash that contains only entries whose first group field has the value “non-fiction” and whose second group field has the value 42.

nil arguments are treated as wildcards and will match everything. Thus,

Books.items(nil, "dragons")

will return all items whose second group matches “dragons”.



249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/internal/dbstruct_class.rb', line 249

def items(*selectors)
  check_not_base_class()

  check("Too many group specifiers!") {
    selectors.size <= self.groups.size
  }

  ds = get_group_selection_dataset(selectors) or
    return nil

  return DBStruct::BogoHash.new(self, ds, selectors)
end

.transaction(*args, **kwargs, &block) ⇒ Object

Evaluates block within a transaction. These can be safely nested and will (usually) roll back if an exception occurs in the block.

This is trivial convenience wrapper around ‘Sequel::Database#transaction`; see its documentation for details.



289
290
# File 'lib/internal/dbstruct_class.rb', line 289

def transaction(*args, **kwargs, &block) =
self.db.transaction(*args, **kwargs, &block)

.where(*cond, **kwcond, &block) ⇒ Object

Convenience method; equivalent to self.items.where(…).

See DBStruct::BogoHash#where for details.



278
279
# File 'lib/internal/dbstruct_class.rb', line 278

def where(*cond, **kwcond, &block) =
self.items.where(*cond, **kwcond, &block)

.with(db, name, &body) ⇒ Class

Define a concrete DBStruct subclass and create the corresponding table if needed.

The database and table are both accessed via Sequel classes (Sequel::Database and Sequel::Dataset respectively) and so are subject to their constraints and abilities. In addition, the underlying database must be SQLite.

The new class is unnamed unless assigned to a global constant.

If the corresponding table does not exist, it is created from the class’s layout.

If a table with a matching name already exists, it is assumed to have previously been created by this method with this layout. While there is some rudimentary consistency checking done to ensure that the expected columns are there, it is not exhaustive. If it is possible for an incompatible table with the same name to appear (e.g. due to version upgrades), you will need to use other mechanisms to detect this.

The new class’s layout (and corresponding table) is defined via the attached block. It is evaluated via class_eval and so may be used to define methods for the new subclass. In addition, it provides a minimal DSL for defining fields (i.e. database columns).

The DSL adds two methods, field and group:

field :name, type
group :name, type

field declares a new field and matching database column. It will create a database column named name with matching getter and setter methods that will access it. name must be a Symbol.

Note that name must begin with a lower-case letter. This is a limitation imposed by DBStruct itself so that can add extra internal fields. (Currently, the only one used is called _id; this is the numeric primary key used as a unique row ID.)

type may be any Ruby type that Sequel knows how to convert to a database column (see the link below). Unlike Sequel, DBStruct requires actual Ruby classes and will ensure that only objects of that specific type (or nil) may be stored in that field. This is enforced by the setter methods. (Exception: TrueClass and FalseClass are interchangeable.)

group is like field except that the field is also treated as a category when used to select subsets with arguments to items. group calls may be freely intermixed with field calls; however, their order is significant. Positional argumetns to items must match the order of group declarations.

The new class is unnamed unless assigned to a global constant.

Parameters:

  • db (Sequel::Database)

    the database.

  • name (Symbol)

    the name of the dataset (i.e. underlying table)

Returns:

  • (Class)

    the new class.

See Also:



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/internal/dbstruct_class.rb', line 71

def with(db, name, &body)

  check("Argument 'db' is not a valid Sequel::Database") {
    db.is_a?(Sequel::Database)
  }
  
  # It should be pretty straightfoward to support other databases,
  # but I have neither the time nor resources to do so right now.
  check("DBStruct currently only supports SQLite; #{db.database_type} " +
        "is unsupported.") {
      db.database_type == :sqlite
  }

  check("Must provide a block argument") { body }
  
  sc = create_the_subclass(db, name, body)
  create_table_if_needed(sc)
  return sc
end

Instance Method Details

#[](field) ⇒ Object

Equivalent to get_field



115
# File 'lib/internal/dbstruct_base.rb', line 115

def [](field) = get_field(field)

#[]=(field, value) ⇒ Object

Equivalent to set_field



118
119
120
# File 'lib/internal/dbstruct_base.rb', line 118

def []=(field, value)
  return set_field(field, value)
end

#deleted?Boolean

Test if the database row associated with self no longer exists. Inverse of present?

Returns:

  • (Boolean)


73
# File 'lib/internal/dbstruct_base.rb', line 73

def deleted? = dataset().where(_id:@rowid).empty?

#eql?(other) ⇒ Boolean Also known as: ==

Test for equality. Instances are equal if and only if they refer to the same database row and (as implied) are of the same class.

Returns:

  • (Boolean)


126
# File 'lib/internal/dbstruct_base.rb', line 126

def eql?(other) = self.class == other.class && @rowid == other.rowid

#get_field(name) ⇒ Object

Sets the value of the field named by field to the given value. field must be one of the declared fields and value must have the declared class.



78
79
80
81
# File 'lib/internal/dbstruct_base.rb', line 78

def get_field(name)
  check_field(name, nil)
  return dataset[_id: @rowid][name]
end

#hashObject

Return a hash; here because we’ve also overridden #eql?



130
# File 'lib/internal/dbstruct_base.rb', line 130

def hash = [self.class, @rowid].hash

#inspectObject Also known as: to_s

Return a human-friendly description.



135
136
137
138
139
140
141
# File 'lib/internal/dbstruct_base.rb', line 135

def inspect
  max_line = 60

  values = to_h.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")
  values = values[0..max_line] + "..." if values.size > max_line
  return "#{self.class}.new(#{values})"
end

#present?Boolean

Test if the database row associated with self still exists. Inverse of deleted?.

Returns:

  • (Boolean)


69
# File 'lib/internal/dbstruct_base.rb', line 69

def present? = !deleted?

#set_field(name, value) ⇒ Object

Retrieve the value of the field named by symbol field. Raises an exception if field is not one of the declared fields.



85
86
87
88
89
90
91
92
93
# File 'lib/internal/dbstruct_base.rb', line 85

def set_field(name, value)
  check_field(name, value)

  dataset()
    .where(_id: @rowid)
    .update({name => value})

  return value
end

#set_fields(**kwargs) ⇒ Object

Set zero or more fields at once. Keywords must be the names of defined fields.

The update is enclosed in an transaction.



99
100
101
102
103
104
# File 'lib/internal/dbstruct_base.rb', line 99

def set_fields(**kwargs)
  self.class.transaction {
    kwargs.each{|k, v| set_field(k, v)}
  }
  return self
end

#to_aObject

Return the contents as an array of pairs of key and value



109
# File 'lib/internal/dbstruct_base.rb', line 109

def to_a = self.columns.map{|key| [key, get_field(key)] }

#to_hObject

Return the contents as a hash



112
# File 'lib/internal/dbstruct_base.rb', line 112

def to_h = to_a.to_h