require 'meta_db/dump.rb' module MetaDb class DbObject # ::attrs defines the set of members of an object. It is used in # initialization (in #init) and in #dump def self.attrs(*attrs) attrs = Array(attrs) if attrs.empty? @attrs else @attrs = attrs end end # Initialize a DbObject. row is a hash with symbolic keys (typically the # result of a database query). Values of keys included in ::attrs are # extracted from the row and passed to the #initialize method of the object # in the same order as in ::attrs def self.init(row) h = [] (@attrs ||= []).each { |a| h << row[a] if row.key?(a) } self.new(*h) end # Unique name within context. This is usually what we understand as the # name of an object but functions have the full signature as "name" attr_reader :name def initialize(name) @name = name end end class Database < DbObject attrs :name, :owner, :schemas # Owner of the database attr_reader :owner # Hash of schemas attr_reader :schemas def initialize(name, owner) super(name) @owner = owner @schemas = {} end end class Schema < DbObject attrs :database, :name, :owner, :tables, :views, :functions, :procedures # Database of the schema attr_reader :database # Owner of the schema attr_reader :owner # Hash of tables (and views) attr_reader :tables # Hash of views def views() @views ||= @tables.select { |_, table| table.view? } end # Hash of functions attr_reader :functions # Hash of procedures attr_reader :procedures def initialize(database, name, owner) super(name) @database = database @owner = owner @tables = {} @functions = {} @procedures = {} @database.schemas[name] = self 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 attr_reader :schema # 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 # Hash of columns attr_reader :columns # 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 # Hash of all constraints attr_reader :constraints # List of primary key constraints (there is only one element) attr_reader :primary_key_constraints # Hash of unique constraints attr_reader :unique_constraints # Hash of check constraints attr_reader :check_constraints # Hash of referential constraints attr_reader :referential_constraints # Hash of triggers attr_reader :triggers def initialize(schema, name, type, is_insertable, is_typed) super(name) @schema = schema @type, @is_insertable, @is_typed = type, is_insertable, is_typed @columns = {} @primary_key_column = :undefined @primary_key_columns = [] @constraints = {} @primary_key_constraints = [] @unique_constraints = {} @check_constraints = {} @referential_constraints = {} @triggers = {} @schema.tables[name] = self end end class View < Table attrs \ :schema, :name, :type, :table?, :view?, :insertable?, :typed?, :columns, :primary_key_columns, :constraints, :referential_constraints, :triggers def table?() false end end class Column < DbObject attrs \ :table, :name, :ordinal, :type, :default, :identity?, :generated?, :nullable?, :updatable?, :primary_key? # Table of the column attr_reader :table # 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 unique (not that this information is not stored in the # Column but in a table constraint) def unique?() table.unique_constraints.values.any? { |constraint| constraint.columns == [self] } end # True if column is the single primary key of the table and false otherwise. Always nil for tables # with multiple primary keys def primary_key?() return nil if table.primary_key_columns.size != 1 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 # True if column is referencing another record def reference?() @reference ||= !references.empty? end # List of referential constraints that involve this column def references @references ||= begin table.referential_constraints.values.select { |constraint| constraint.referencing_columns.include?(self) } end end def initialize(table, name, ordinal, type, default, is_identity, is_generated, is_nullable, is_updatable) super(name) @table = table @type, @ordinal, @default, @is_identity, @is_generated, @is_nullable, @is_updatable = type, ordinal, default, is_identity, is_generated, is_nullable, is_updatable @table.columns[name] = self 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 attr_reader :table # Constraint column. Raise an error if the constraint is multi-column def column columns.size == 1 or raise "Multicolumn constraint" columns.first end # 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(name) @table = table @columns = columns @table.constraints[name] = self end end class PrimaryKeyConstraint < Constraint attrs :table, :name, :columns def initialize(table, name, columns) super columns.each { |c| c.table.primary_key_columns << c } table.primary_key_constraints << self end end class UniqueConstraint < Constraint attrs :table, :name, :columns def initialize(table, name, columns) table.unique_constraints[name] = self end 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 @table.check_constraints[name] = self end end class ReferentialConstraint < Constraint attrs :referencing_table, :name, :referencing_columns, :referenced_constraint # The referencing table 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_constraint.columns end def initialize(referencing_table, name, referencing_columns, referenced_constraint) super(referencing_table, name, referencing_columns) @referenced_constraint = referenced_constraint table.referential_constraints[name] = self end end class Function < DbObject attrs :schema, :name, :owner, :security # Schema of the function attr_reader :schema # Owner of the function attr_reader :owner # Security (:definer or :invoker) attr_reader :security # True if security is 'definer' def suid?() security == 'definer' end # True if this is a function def function?() true end # True if this is a procedure def procedure?() !function? end def initialize(schema, name, owner, security) super(name) @schema = schema @owner = owner @security = security.to_sym if function? schema.functions[name] = self else schema.procedures[name] = self end end end class Procedure < Function attrs :schema, :name, :owner, :security def function?() false end end class Trigger < DbObject attrs :table, :name, :function, :level, :timing, :events # Table of trigger attr_reader :table # Trigger function attr_reader :function # Trigger level (:stmt or :row) attr_reader :level # When trigger is fired (:before, :after, or :instead) attr_reader :timing # Array of events (:insert, :update, :delete, or :truncate) causing the trigger to fire attr_reader :events def initialize(table, name, function, level, timing, events) super(name) @table = table @name = name @function = function @level = level.to_sym @timing = timing.to_sym @events = events.split.map(&:to_sym) @table.triggers[name] = self end end CONSTRAINT_KINDS = { PrimaryKeyConstraint => :primary_key, ReferentialConstraint => :foreign_key, UniqueConstraint => :unique, CheckConstraint => :check } end