require 'meta_db/dump.rb' module MetaDb class DbObject # Used to multi-assign values and for dump # # TODO: rename db_attr or 'column', and create read/writer methods def self.attrs(*attrs) attrs = Array(attrs) if attrs.empty? @attrs else @attrs = attrs end end def self.init(row) h = [] self.attrs.each { |a| h << row[a] if row.key?(a) } self.new(*h) end attrs :parent, :name, :children # Name of object. Unique within contianing object. Not nil attr_reader :name # Parent DbObject. Non-nil except for the top-level MetaDb::Database object attr_reader :parent # Hash from name to contained objects attr_reader :children def initialize(parent, name) @name, @parent = name, parent @children = {} parent&.send(:attach, self) end # Unique dot-separated list of names leading from the top-level MetaDb::Database object to # self. Omits the first path-element if strip is true def path(strip: false) r = [] node = self while node r << node.name node = node.parent end r.pop if strip r.reverse.compact.join(".") end # Return child object with the given name def [](name) @children[name] end # Recursively lookup object by dot-separated list of names. If strip is true, the first element # will be stripped from the path def dot(path, strip: false) elems = path.split(".") elems.shift if strip elems.inject(self) { |a,e| a[e] } or raise "Can't lookup '#{path}' in #{self.path}" end # Compare two objects by identity def <=>(r) path <=> r.path end # Mostly for debug def inspect string = StringIO.new stdout = $stdout begin $stdout = string dump ensure $stdout = stdout end string.string end protected def constrain_children(klass) children.values.select { |v| v.is_a?(klass) } end private def attach(child) !@children.key?(child.name) or raise "Duplicate child key: #{child.name.inspect}" child.instance_variable_set(:@parent, self) @children[child.name] = child end def detach(child) @children.key?(child.name) or raise "Non-existing child key: #{child.name.inspect}" child.instance_variable_set(:@parent, nil) @children.delete(child.name) end end class Database < DbObject attrs :name, :owner, :schemas # List of schemas def schemas() @schemas ||= children.values end # Owner of the database attr_reader :owner def initialize(name, owner) super(nil, name) @owner = owner end end class Schema < DbObject attrs :database, :name, :owner, :tables, :views, :functions, :procedures # Database of the schema alias_method :database, :parent # Owner of the schema attr_reader :owner # List of tables (and views) def tables() @tables ||= constrain_children(MetaDb::Table) end # List of views def views() @views ||= constrain_children(MetaDb::View) end # List of functions def functions() @functions ||= constrain_children(MetaDb::Function) end # List of procedures def procedures() @procedures ||= constrain_children(MetaDb::Procedure) end def initialize(database, name, owner) super(database, name) @owner = owner end end class Table < DbObject attrs \ :schema, :name, :type, :table?, :view?, :insertable?, :typed?, :columns, :primary_key_columns, :constraints, :referential_constraints, :triggers # Schema of the table. Redefines #parent alias_method :schema, :parent # Type of table. Either 'BASE TABLE' or 'VIEW' attr_reader :type # True iff table is a real table and not a view def table?() true end # True iff table is a view def view?() !table? end # True if the table/view is insertable def insertable?() @is_insertable end # True if the table/view is typed def typed?() @is_typed end # List of columns. Columns are sorted by ordinal def columns() @columns ||= constrain_children(MetaDb::Column).sort end # The primary key column. nil if the table has multiple primary key columns def primary_key_column return @primary_key_column if @primary_key_column != :undefined if primary_key_columns.size == 1 @primary_key_column = primary_key_columns.first else @primary_key_column = nil end end # List of primary key columns # # Note: Assigned by PrimaryKeyConstraint#initialize attr_reader :primary_key_columns # List of constraints def constraints() @constraints ||= constrain_children(MetaDb::Constraint) end # List of referential constraints def referential_constraints() @referential_constraints ||= constrain_children(MetaDb::ReferentialConstraint) end # List of triggers def triggers() @triggers ||= constrain_children(MetaDb::Trigger) end def initialize(schema, name, type, is_insertable, is_typed) super(schema, name) @type, @is_insertable, @is_typed = type, is_insertable, is_typed @primary_key_column = :undefined @primary_key_columns = [] end end class View < Table def table?() false end end class Column < DbObject attrs \ :table, :ordinal, :name, :type, :default, :identity?, :generated?, :nullable?, :updatable?, :primary_key? # Table of the column alias_method :table, :parent # Ordinal number of the column attr_reader :ordinal # Type of the column attr_reader :type # Default value attr_reader :default # True if column is an identity column def identity?() @is_identity end # True if column is auto generated def generated?() @is_generated end # True if column is nullable def nullable?() @is_nullable end # True if column is updatable def updatable?() @is_updatable end # True if column is the single primary key of the table. Always false for tables # with multiple primary keys def primary_key?() table.table? && self == table.primary_key_column end # True is column is (part of) the primary key of the table def primary_key_column?() table.table? && table.primary_key_columns.include?(self) end def initialize(table, ordinal, name, type, default, is_identity, is_generated, is_nullable, is_updatable) super(table, name) @type, @ordinal, @default, @is_identity, @is_generated, @is_nullable, @is_updatable = type, ordinal, default, is_identity, is_generated, is_nullable, is_updatable end # Compare columns by table and ordinal def <=>(other) if other.is_a?(Column) && table == other.table ordinal <=> other.ordinal else super end end end class Constraint < DbObject attrs :table, :name, :columns # Table of the constraint alias_method :table, :parent # List of columns in the constraint. Empty for CheckConstraint objects attr_reader :columns # Constraint kind. Either :primary_key, :foreign_key, :unique, or :check def kind() CONSTRAINT_KINDS[self.class] end def initialize(table, name, columns) super(table, name) @columns = columns end end class PrimaryKeyConstraint < Constraint attrs :table, :name, :columns def initialize(table, name, columns) super columns.each { |c| c.table.primary_key_columns << c } end end class UniqueConstraint < Constraint attrs :table, :name, :columns end # Note that #columns is always empty for check constraints class CheckConstraint < Constraint attrs :table, :name, :expression # SQL check expression attr_reader :expression # Half-baked SQL-to-ruby expression transpiler def ruby_expression # Very simple @ruby ||= sql.sub(/\((.*)\)/, "\\1").gsub(/\((\w+) IS NOT NULL\)/, "!\\1.nil?").gsub(/ OR /, " || ") end def initialize(table, name, expression) super(table, name, []) @expression = expression end end class ReferentialConstraint < Constraint attrs :referencing_table, :name, :referencing_columns, :referenced_constraint # The referencing tabla alias_method :referencing_table, :table # The referencing columns. Can't be empty alias_method :referencing_columns, :columns # The referenced constraint attr_reader :referenced_constraint # The referenced table def referenced_table() referenced_constraint.table end # The referenced columns def referenced_columns() referenced_constraints.columns end def initialize(referencing_table, name, referencing_columns, referenced_constraint) super(referencing_table, name, referencing_columns) @referenced_constraint = referenced_constraint end end class Function < DbObject end class Procedure < DbObject end class Trigger < DbObject end CONSTRAINT_KINDS = { PrimaryKeyConstraint => :primary_key, ReferentialConstraint => :foreign_key, UniqueConstraint => :unique, CheckConstraint => :check } end