module FixtureFox class Analyzer attr_reader :ast attr_reader :type attr_reader :idr # Initialized by #call # List of all known tables whether empty or not (PgGraph::Type::Table objects) def tables() @tables.values end # List of non-empty tables (PgGraph::Type::Table objects) def data_tables() @data_tables.values end attr_reader :records # [AstRecord] attr_reader :record_refs # [AstRecordRef] attr_reader :fields # [AstField] # Map from qualified table name to maximum record ID attr_reader :ids # Like ids but includes only tables with records from the sources # attr_reader :referenced_ids # Anchors object. This includes both anchors defined in the sources and # anchors supplied to #initialize attr_reader :anchors # Map from anchor name to anchors defined by the sources attr_reader :defined_anchors # Map from anchor name to anchors referenced by the sources. FIXME: Unused attr_reader :referenced_anchors # True iff types have been assigned def assigned?() @assigned end # True iff types have been checked def checked?() @checked end # True iff data has been generated def generated?() !@idr.nil? end def initialize(type, ast, ids: {}, anchors: Anchors.new(type)) constrain type, PgGraph::Type constrain ast, Ast constrain anchors, Anchors @type = type @ast = ast @tables = {} @data_tables = {} @records = [] @record_refs = [] @fields = [] # List of all fields @ids = Hash.new(0) ids.each { |k,v| @ids[k] = v } # copy but preserve default value @anchors = anchors @defined_anchors = {} @referenced_anchors = {} @assigned = false @checked = false end # TODO: Also generate data def call assign_types check_types self end # Assign types and collect fields, records, and anchors. Note that # #assign_types and #check_types are separate methods to make it possible # to analyze the AST just up to the point where external anchors are needed # for further processing. This is used in the caching mechanism in Postspec def assign_types @assigned = true assign_table_types self end # Check types. Merges anchors: into self.anchors def check_types(anchors: nil) @checked = true @anchors.merge!(anchors) if anchors check_field_types check_record_ref_types self end # Generate IDR. Use self.ids if ids: is nil def generate(ids: nil) ids.each { |k,v| @ids[k] = v } if ids # copy while preserving default value in hash generate_record_ids generate_idr @idr end private def merge_anchor(record) constrain record, AstRecord name = record.anchor&.value if !@anchors.key?(name) @defined_anchors[name] = @anchors.create(name, record.type) end end def assign_table_types ast.tables.each { |ast_table| @type.key?(ast_table.schema.value.downcase) or ast_table.schema.error("Can't find schema '#{ast_table.schema}'") type = ast_table.type = @type[ast_table.schema.value.downcase][ast_table.name.downcase] or ast_table.ident.error( "Can't find ast_table '#{ast_table.ident}' (maybe you forgot to declare a schema?)") @tables[type.uid] = type assign_record_types(ast_table) } end def assign_record_types(ast_table) constrain ast_table, AstTable, AstTableMember ast_table.elements.each { |ast_record| ast_record.type = ast_table.type.record_type @data_tables[ast_record.type.table.uid] ||= ast_record.type.table case ast_record when AstRecordElement records << ast_record merge_anchor(ast_record) if ast_record.anchor assign_field_types(ast_record) when AstReferenceElement record_refs << ast_record else raise "Oops" end } end def assign_field_types(ast_record) ast_record.members.each { |field| field.column = ast_record.type[field.ident.litt.downcase] or field.ident.error("Can't find field '#{ast_record.type.table.name}.#{field.ident}'") field.type = field.column.type case field when AstTableMember assign_record_types(field) when AstRecordMember records << field merge_anchor(field) if field.anchor assign_field_types(field) when AstFieldMember, AstReferenceMember fields << field else raise "Oops" end } end def check_field_types fields.each { |f| case f when AstFieldMember # FIXME: Doesn't handle multidimensional arrays if f.type.array? values = f.value.value klass = f.type.element_type.ruby_class elsif f.type.is_a?(PgGraph::Type::RecordType) if f.column.is_a?(PgGraph::Type::KindRecordColumn) values = [f.value.value] klass = f.column.kind_column.type.ruby_class else f.value.error("Expected a record or a reference, got #{f.value.value.inspect}") end else values = [f.value.value] klass = f.type.ruby_class end values.each { |value| if value.is_a?(klass) || !f.type.array? && (klass == Time || klass == Date) && value.is_a?(String) && value =~ /^\d\d\d\d-\d\d-\d\d(?: \d\d:\d\d(?::\d\d)?)?$/ ; else f.value.error("Data type mismatch - expected #{klass}, got #{value.class}: #{value.inspect}") end } when AstReferenceMember check_ref(f) else raise "Oops" end } end def check_ref(ast_ref) anchor = ast_ref.referenced_anchor = @anchors[ast_ref.reference.value.to_sym] or ast_ref.reference.error("Can't find anchor for reference '#{ast_ref.reference.litt}'") anchor_types = [anchor.type] + (anchor.type.table.sub_table? ? [anchor.type.table.super_table.record_type] : []) anchor_types.any? { |anchor_type| ast_ref.type == anchor_type } or ast_ref.reference.error( "Data type mismatch - expected a reference to a " \ "#{ast_ref.type.identifier} record, got #{anchor.type.identifier}") end def check_record_ref_types record_refs.each { |record| check_ref(record) } end def check_record_kind_ref_types end def generate_record_ids # puts "generate_record_ids" # puts " ids: #{@ids}" @super_table_records = [] # Array of pairs of super_table/id records.each { |ast_record| table = ast_record.type.table table_uid = table.sub_table? ? table.super_table.uid : table.uid # If enclosing record/table is the super_table, then take the ID from there if table.sub_table? parent_table = ast_record.parent.type.table if table.super_table == parent_table # works for both Record and Table objects id = @ids[table.uid] = @ids[parent_table.uid] # Enclosing record/table is not the super_table so generate a new ID # based on the current super_table ID and prepare a new super_table # record else id = @ids[table.uid] = @ids[table.super_table.uid] += 1 @super_table_records << [table.super_table, id] end ast_record.id = id # If record is a (labelled) root record then check if it has already been defined # and take the ID from there elsif name = ast_record.anchor&.value @anchors.key?(name) or raise "Oops" @referenced_anchors[name] ||= @anchors[name] if !@defined_anchors.key?(name) ast_record.id = @anchors[name].id ||= @ids[table_uid] += 1 else # Assign a new ID to the record ast_record.id = @ids[table_uid] += 1 end # Register anchor if name = ast_record.anchor&.value @anchors.key?(name) or raise "Oops" @anchors[name].id = ast_record.id # @referenced_anchors[name] ||= @anchors[name] if !@defined_anchors.key?(name) # ast_record.id = @anchors[name].id ||= @ids[table_uid] += 1 end } end def generate_idr @idr = Idr.new # Build all root tables. This makes sure empty tables are registered ast.tables.each { |t| idr.put(t.schema, t.type.table.name) } # Create implicit super_table records @super_table_records.each { |table, id| idr.put(table.schema.name, table.name, id) } # Create records by filling in the Idr field-by-field records.each { |record| record.members.each { |field| table = nil # To bring table into scope. FIXME: Yt? case field when AstFieldMember # puts "AstFieldMember: #{field}" table = record.type.table column = field.column.postgres_column idr.put(table.schema.name, table.name, record.id, column, field.value.value) when AstReferenceMember # puts "AstReferenceMember: #{field}" table = record.type.table column = field.column.postgres_column idr.put(table.schema.name, table.name, record.id, column, field.referenced_anchor.id) when AstRecordMember # puts "AstRecordMember: #{record.type}" column = field.column this_record_id = field.record.id that_record_id = field.id if column.reference? # The key is on this side table = record.type.table schema = table.schema link_column = column.this_link_column idr.put(schema.name, table.name, this_record_id, link_column, that_record_id) else # The key is on the other side table = column.type.table schema = table.schema link_column = column.that_link_column idr.put(schema.name, table.name, that_record_id, link_column, this_record_id) end when AstTableMember # puts "AstTableMember: #{field}" if field.column.is_a?(PgGraph::Type::MmTableColumn) table = mm_table = field.column.mm_table this_mm_column = field.column.this_mm_column that_mm_column = field.column.that_mm_column field.elements.each { |r| mm_id = @ids[mm_table.path] += 1 this_mm_column_id = record.id that_mm_column_id = r.is_a?(AstReferenceElement) ? r.referenced_anchor.id : r.id idr.put(mm_table.schema.name, mm_table.name, mm_id, this_mm_column, this_mm_column_id) idr.put(mm_table.schema.name, mm_table.name, mm_id, that_mm_column, that_mm_column_id) } else table = record_table = field.column.type.table record_column = field.column field.elements.each { |r| record_id = r.is_a?(AstReferenceElement) ? r.referenced_anchor.id : r.id idr.put( record_table.schema.name, record_table.name, record_id, record_column.that_link_column, record.id) } end else raise "Oops: Unhandled case - #{field.class}" end } } idr.materialized_views = @tables.values.map(&:depending_materialized_views).flatten.uniq end def dump records.sort { |l,r| l.type.identifier <=> r.type.identifier }.each { |r| puts r.type.identifier indent { puts "id: #{r.id}" r.members.each { |f| print "#{f.ident}: #{f.type.identifier}" case f when AstField if f.value.is_a?(Value) print " = #{f.value.to_s}" elsif f.value.is_a?(Reference) print " = #{f.type.table.identifier}[#{f.referenced_anchor.id}]" else raise "Oops" end puts " (#{f.type.class})" when AstRecord puts " = #{r.type.table.identifier}[#{f.id}] (#{f.type.class})" when AstTable print " = [" print f.records.map { |r| "#{r.type.table.identifier}[#{r.id}]" }.join(", ") puts "] (#{f.class}, #{f.type.class})" else puts end } } } end def dump_anchors @anchor_uids.map { |k,v| puts "#{k}: #{v}" } end end end