Class: PgGraph::Type::Database

Inherits:
Node
  • Object
show all
Defined in:
lib/pg_graph/type/type.rb,
lib/pg_graph.rb,
lib/pg_graph/type/read.rb

Overview

Database is the top-level object and #read is the starting point for loading the type system in PgGraph::Type.new

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Node

#dump, #identifier, #inspect, #inspect_inner, #schema_identifier

Constructor Details

#initialize(name, reflector = PgGraph::Reflector.new) ⇒ Database

Returns a new instance of Database.



74
75
76
77
78
79
80
# File 'lib/pg_graph/type/type.rb', line 74

def initialize(name, reflector = PgGraph::Reflector.new)
  constrain name, String
  constrain reflector, PgGraph::Reflector
  super(nil, name)
  @catalog = PgCatalogSchema.new(self)
  @reflector = reflector
end

Instance Attribute Details

#catalogObject (readonly)

Returns the value of attribute catalog.



64
65
66
# File 'lib/pg_graph/type/type.rb', line 64

def catalog
  @catalog
end

#reflectorObject (readonly)

The reflector object. The reflector object takes a name of a link field and produce the field name in this table and the corrsponding field name in the other table



72
73
74
# File 'lib/pg_graph/type/type.rb', line 72

def reflector
  @reflector
end

Instance Method Details

#dot_lookup(key) ⇒ Object



82
83
84
# File 'lib/pg_graph/type/type.rb', line 82

def dot_lookup(key)
  super || catalog[key]
end

#guidObject



61
# File 'lib/pg_graph/type/type.rb', line 61

def guid() name end

#instantiate(arg = nil) ⇒ Object

:call-seq:

instantiate()
instantiate(hash)
instantiate(yaml)
instantiate(pg_conn)


82
83
84
85
# File 'lib/pg_graph.rb', line 82

def instantiate(arg = nil)
  constrain arg, NilClass, Hash, PgConn
  Data.new(self, arg)
end

#is_a?(klass) ⇒ Boolean

Make Database pretend it is a PgGraph::Type object

Returns:

  • (Boolean)


88
# File 'lib/pg_graph.rb', line 88

def is_a?(arg) arg == PgGraph::Type || super end

#read(arg, reflector = nil, ignore: []) ⇒ Object



93
94
95
96
97
98
99
100
101
# File 'lib/pg_graph/type/type.rb', line 93

def read(arg, reflector = nil, ignore: [])
  constrain arg, PgMeta, PgConn
  @reflector = reflector || @reflector
  case arg
    # #read_meta is a member of Database but defined in read.rb
    when PgMeta; read_meta(arg, ignore: ignore)
    when PgConn; read_meta(PgMeta.new(arg), ignore: ignore)
  end
end

#read_meta(meta, ignore: []) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/pg_graph/type/read.rb', line 6

def read_meta(meta, ignore: [])
  # Array of [record, meta_column] tuples
  field_columns = []
  link_columns = []
  kind_columns = [] # The link side
  id_link_columns = [] # sub tables

  # Temporary arrays of meta tables
  tables = [] # Ordinary tables
  mm_tables = [] # M:M link tables

  # Create schemas and tables and initialize lists of objects
  meta.schemas.values.each { |meta_schema|
    next if ignore.include?(meta_schema.name)
    schema = Schema.new(self, meta_schema.name)
    meta_schema.tables.values.select { |t| t.table? }.each { |meta_table| # FIXME Ignore views for now
      (meta_table.mm_table? ? mm_tables : tables) << meta_table
      table = Table.new(
          schema, meta_table.name,
          mm_table: meta_table.mm_table?,
          depending_materialized_views: meta_table.depending_views.select(&:materialized?))
      record_type = RecordType.new(table)

      # Process columns
      array_columns = []
      meta_table.columns.values.each { |meta_column|
        # Create basic types needed by columns
        type_name = meta_column.type
        if !schema.key?(type_name) && !catalog.key?(type_name)
          if meta_column.array?
            element_name = meta_column.element_type
            dimensions = meta_column.dimensions
            if !schema.key?(element_name) && !catalog.key?(element_name)
              element_type = SimpleType.new(catalog, element_name)
            else
              element_type = schema[element_name] || catalog[element_name]
            end
            ArrayType.new(catalog, type_name, element_type, dimensions)
          else
            SimpleType.new(catalog, type_name)
          end
        end

        # Collect columns
        if meta_column.reference?
          if meta_column.name == "id"
            id_link_columns
          elsif meta_column.name =~ /^(?:.*_)?kind$/
            kind_columns
          else
            link_columns
          end
        else
          field_columns
        end << [record_type, meta_column]
      }
    }
  }

  # Build list of depending tables
  (tables + mm_tables).each { |meta_table|
    table = dot(meta_table.path)
    meta_table.depending_tables.each { |meta_depending_table|
      depending_table = dot(meta_depending_table.path)
      table.depending_tables << depending_table
    }
  }

  # Create postgres columns except kind_columns
  (id_link_columns + link_columns + field_columns).each { |record_type, meta_column|
    next if meta_column.kind?
    type = record_type.schema[meta_column.type] || catalog[meta_column.type]
    SimpleColumn.new(
        record_type, meta_column.name, meta_column.name, type,
        ordinal: meta_column.ordinal,
        **column_options(meta_column))
  }

  # link_fields is a list of [uid, record_column] tuples
  link_fields = []

  # Map from referencing table to referenced table to reference. It is
  # used to detect when the default reverse map doesn't work because of
  # name collisions
  reference_count = {}

  # Create and collect forward-references
  (link_columns + kind_columns).each { |record_type, meta_column|
    reference_count[record_type] ||= {}

    meta_column.references.each { |constraint|
      constraint.referencing_columns.size == 1 or raise Error, "Can't handle multi-column keys (for now)"
      type = dot(constraint.referenced_table.path).type.record_type
      this_link_column = constraint.referencing_columns.first.name
      that_link_column = constraint.referenced_columns.first.name

      # Count references by running the reflector in the link column (this
      # is expensive because we're doing it again later)
      (reference_count[record_type] ||= {})[type] ||= 0
      reference_count[record_type][type] += 1 if reflector.multi?(this_link_column)

      field =
          if meta_column.kind?
            name = reflector.this(meta_column.uid) || meta_column.name
            name = meta_column.name if record_type[name] # Check for duplicates
            !record_type.nil? or raise Error, "Duplicate column name: #{name}" # Check again
            column_type = record_type.schema[meta_column.type] || catalog[meta_column.type]

            parent = (name != meta_column.name ? record_type : nil)
            kind_column = SimpleColumn.new(
                parent, meta_column.name, meta_column.name, column_type,
                ordinal: meta_column.ordinal,
                **column_options(meta_column))

            KindRecordColumn.new(
                record_type, name, meta_column.name, type, this_link_column, that_link_column,
                kind_column, **column_options(meta_column))
          else
            name = reflector.this(meta_column.uid)
            RecordColumn.new(
                record_type, name, meta_column.name, type, this_link_column, that_link_column,
                **column_options(meta_column))
          end
      link_fields << [meta_column.uid, field]
    }
  }

  # Create back-reference fields
  (link_fields).each { |uid, this_column|
    this_record_type = this_column.record_type
    this_table = this_record_type.table
    that_record_type = this_column.type
    next if this_table.mm_table?

    # Field name of back-reference
    if this_column.primary_key? # child record
      this_column.postgres_column == "id" or raise Error, "Primary keys should be named 'id'"
      name = this_record_type.name
    else
      multi = reference_count[this_record_type][that_record_type] > 1
      name = reflector.that(uid, this_column.unique?, multi, table: this_record_type.name)
      name ||= PgGraph.inflector.pluralize(this_column.table.name) if name.nil? && this_column.kind?
    end

    next if !name

    # Check for name collisions
    if that_record_type.key?(name)
      # Check if this is a 1:1 relation with keys on both sides. In that
      # case, the back-reference is already created
      that_column = that_record_type[name]
      if that_column.is_a?(RecordColumn) && that_column.type == this_record_type
        next # Do nothing

      # Check if the reference spans across schemes so we can disambiguate
      # by prefixing the schema name
      elsif this_column.table.schema != that_column.table.schema
        name = "#{this_column.table.schema.name}_#{name}"
      end
    end

    # Final check for name collisions
    !that_record_type.key?(name) or
        raise Error, "Name collision in reverse map for #{this_column.uid}, trying #{name}"

    # Create back-references
    if this_column.unique?
      RecordColumn.new(
          that_record_type, name, nil, this_record_type,
          this_column.that_link_column, this_column.this_link_column)
    else
      TableColumn.new(that_record_type, name, this_table.type,
          this_column.that_link_column, this_column.this_link_column)
    end
  }

  # Setup super/sub_tables
  id_link_columns.each { |this_record_type, meta_column|
    # Create forward and backward references for id links
    constraint = meta_column.references.first
    that_record_type = dot(constraint.referenced_table.path).type.record_type
    SuperRecordColumn.new(this_record_type, that_record_type)
    SubRecordColumn.new(that_record_type, this_record_type)
  }

  # Create N:M or M:M reference fields
  mm_tables.each { |link_meta_table|
    constraint1 = link_meta_table.referential_constraints.values[0]
    constraint2 = link_meta_table.referential_constraints.values[1]
    table1 = dot(constraint1.referenced_table.path)
    table2 = dot(constraint2.referenced_table.path)
    mm_table = dot(link_meta_table.path)

    link_column1 = constraint1.referenced_columns.first.name
    mm_column1 = constraint1.referencing_columns.first.name
    mm_column1_uid = constraint1.referencing_columns.first.uid

    link_column2 = constraint2.referenced_columns.first.name
    mm_column2 = constraint2.referencing_columns.first.name
    mm_column2_uid = constraint2.referencing_columns.first.uid

    column1_name = reflector.that(mm_column1_uid, false, false, table: table2.record_type.name)
    column2_name = reflector.that(mm_column2_uid, false, false, table: table1.record_type.name)

    # FIXME: DAGs over an super table creates problems if reflections
    # doesn't match (eg. role_id/group_id instead of
    # child_group_id/parent_group_id)

    # Detect DAGs. This check is performed after column names have been
    # computed to allow for user defined reflections
#       if column1_name == column2_name && table1 == table2
#         column1_name = reflector.that(mm_column1_uid, false)
#         column2_name = reflector.that(mm_column2_uid, false)
#         puts "BINGO"
#         puts column1_name
#         puts column2_name
#         exit 1
#       else
#       end


#       puts "mm_tables.each"
#       indent {
#         puts "mm_column1_uid: #{mm_column1_uid}"
#         puts "mm_column2_uid: #{mm_column2_uid}"
#         puts "table1: #{table1}"
#         puts "column1_name: #{column1_name}"
#         puts "table2: #{table2}"
#         puts "column2_name: #{column2_name}"
#       }

    if table1.type.record_type.key?(column1_name)
      raise Error, "Duplicate column name in #{table1.identifier}: #{column1_name}"
    end

    if table2.type.record_type.key?(column2_name)
      raise Error, "Duplicate column name in #{table2.identifier}: #{column2_name}"
    end

    klass = link_meta_table.nm_table? ? NmTableColumn : MmTableColumn
    klass.new(
        table1.type.record_type, column1_name, table2.type, mm_table.type,
        link_column1, mm_column1, mm_column2, link_column2)
    klass.new(
        table2.type.record_type, column2_name, table1.type, mm_table.type,
        link_column2, mm_column2, mm_column1, link_column1)
  }

  self
end

#tablesObject

List of all tables in the database



67
# File 'lib/pg_graph/type/type.rb', line 67

def tables() schemas.map(&:tables).flatten end