├── check_type_code.sh ├── typecheck.rb ├── active-record ├── sql-strings.rb └── db_types.rb └── sequel └── db_types.rb /check_type_code.sh: -------------------------------------------------------------------------------- 1 | pry typecheck.rb 2 | -------------------------------------------------------------------------------- /typecheck.rb: -------------------------------------------------------------------------------- 1 | require 'rdl' 2 | require 'types/core' 3 | require 'rails' 4 | require 'active_record' 5 | require 'active_record/relation' 6 | require 'sequel' 7 | require './active-record/db_types' 8 | require './sequel/db_types' 9 | 10 | RDL.check_type_code 11 | -------------------------------------------------------------------------------- /active-record/sql-strings.rb: -------------------------------------------------------------------------------- 1 | require 'sql-parser' 2 | 3 | # Mocking the SQLVistor internal class behavior to do our own stuff 4 | class ASTVisitor 5 | def initialize(table, targs) 6 | @table = table # default table name, if we don't have a qualified column name 7 | @targs = targs 8 | end 9 | 10 | def visit(node) 11 | node.accept(self) 12 | end 13 | 14 | def binary_op(o) 15 | table, column = visit(o.left) 16 | ident = visit(o.right) 17 | query_type = @targs[ident] || @targs.last 18 | schema_type = RDL::Globals.ar_db_schema[table.classify.to_sym].params[0].elts[column.to_sym] 19 | query_type = query_type.elts[column.to_sym] if query_type.is_a? RDL::Type::FiniteHashType 20 | # puts query_type, schema_type 21 | raise RDL::Typecheck::StaticTypeError, "type error" unless query_type <= schema_type 22 | end 23 | 24 | alias_method :visit_Greater, :binary_op 25 | alias_method :visit_Equals, :binary_op 26 | alias_method :visit_Less, :binary_op 27 | alias_method :visit_GreaterOrEquals, :binary_op 28 | 29 | def visit_And(o) 30 | visit(o.left) 31 | visit(o.right) 32 | end 33 | 34 | def visit_Subquery(o) 35 | raise RDL::Typecheck::StaticTypeError, "only works with SELECT queries now" unless o.query_specification.is_a? SQLParser::Statement::Select 36 | select_query = o.query_specification 37 | raise RDL::Typecheck::StaticTypeError, "expected only 1 column in SELECT sub queries" unless select_query.list.is_a? SQLParser::Statement::SelectList and select_query.list.columns.length == 1 38 | column = select_query.list.columns[0].name 39 | table = select_query.table_expression.from_clause.tables[0].name 40 | visitor = ASTVisitor.new table, @targs 41 | search_cond = select_query.table_expression.where_clause.search_condition 42 | visitor.visit(search_cond) 43 | RDL::Type::GenericType.new(RDL::Type::NominalType.new(Array), RDL::Globals.ar_db_schema[table.classify.to_sym].params[0].elts[column.to_sym]) 44 | end 45 | 46 | def visit_In(o) 47 | table, column = visit(o.left) 48 | ident = visit(o.right) 49 | # TODO: add a case where ident is an integer and targs doesn't have named params, but just ?-ed params 50 | if ident.is_a? Integer and @targs.last.is_a? RDL::Type::FiniteHashType 51 | # query_params is a finite hash 52 | query_params = @targs.last.elts 53 | # this is a hack, assumes keys are in order, which isn't necessarily true 54 | query_type = query_params[query_params.keys[ident - 1]] 55 | elsif ident.is_a? RDL::Type::GenericType 56 | query_type = ident 57 | else 58 | # puts "(TODO) Unexpected" 59 | end 60 | 61 | schema_type = RDL::Globals.ar_db_schema[table.classify.to_sym].params[0].elts[column.to_sym] 62 | # IN works with arrays 63 | promoted = if query_type.is_a?(RDL::Type::TupleType) then query_type.promote else query_type end 64 | if promoted.is_a? RDL::Type::GenericType 65 | # base type is Array, maybe add check? 66 | raise RDL::Typecheck::StaticTypeError, "type error" unless promoted.params[0] <= schema_type 67 | else 68 | raise RDL::Typecheck::StaticTypeError, "some other type after promotion" 69 | end 70 | end 71 | 72 | def visit_Not(o) 73 | visit(o.value) 74 | end 75 | 76 | def visit_Integer(o) 77 | return o.value 78 | end 79 | 80 | def visit_QualifiedColumn(o) 81 | [o.table.name, o.column.name] 82 | end 83 | 84 | def visit_Column(o) 85 | [@table, o.name] 86 | end 87 | 88 | def visit_InValueList(o) 89 | o.values.value 90 | end 91 | end 92 | 93 | def handle_sql_strings(trec, targs) 94 | parser = SQLParser::Parser.new 95 | 96 | case trec 97 | when RDL::Type::GenericType 98 | if trec.base.klass == ActiveRecord_Relation 99 | handle_sql_strings trec.params[0], targs 100 | elsif trec.base.klass == JoinTable 101 | # works only for the base class right now, need to extend for the params as well 102 | base_klass = trec.params[0] 103 | joined_with = trec.params[1] 104 | case joined_with 105 | when RDL::Type::UnionType 106 | joined_with.types.each do |klass| 107 | # add the joining association column on this 108 | sql_query = "SELECT * FROM `#{base_klass.name.tableize}` INNER JOIN `#{klass.name.tableize}` ON a.id = b.a_id WHERE #{build_string_from_precise_string(targs)}" 109 | # puts sql_query 110 | begin 111 | ast = parser.scan_str(sql_query) 112 | rescue Racc::ParseError => e 113 | # puts "There was a parse error with above query, moving on" 114 | return 115 | end 116 | search_cond = ast.query_expression.table_expression.where_clause.search_condition 117 | visitor = ASTVisitor.new base_klass.name.tableize, targs 118 | visitor.visit(search_cond) 119 | end 120 | else 121 | # TODO 122 | # puts "== TODO ==" 123 | end 124 | else 125 | # puts "UNEXPECTED #{trec}, #{targs}" 126 | end 127 | when RDL::Type::NominalType 128 | base_klass = trec 129 | sql_query = "SELECT * FROM `#{base_klass.name.tableize}` WHERE #{build_string_from_precise_string(targs)}" 130 | # puts sql_query 131 | ast = parser.scan_str(sql_query) 132 | search_cond = ast.query_expression.table_expression.where_clause.search_condition 133 | visitor = ASTVisitor.new base_klass.name.tableize, targs 134 | visitor.visit(search_cond) 135 | when RDL::Type::SingletonType 136 | base_klass = trec 137 | sql_query = "SELECT * FROM `#{base_klass.val.to_s.tableize}` WHERE #{build_string_from_precise_string(targs)}" 138 | # puts sql_query 139 | ast = parser.scan_str(sql_query) 140 | search_cond = ast.query_expression.table_expression.where_clause.search_condition 141 | visitor = ASTVisitor.new base_klass.val.to_s.tableize, targs 142 | visitor.visit(search_cond) 143 | else 144 | # puts "UNEXPECTED #{trec}, #{targs}" 145 | end 146 | end 147 | 148 | def build_string_from_precise_string(args) 149 | str = args[0] 150 | raise "Bad type!" unless str.is_a? RDL::Type::PreciseStringType 151 | # TODO: handles only non-interpolated strings for now 152 | base_query = str.vals[0] 153 | 154 | # Get rid of SQL functions here, that just ends up confusing the parser anyway 155 | base_query.gsub!('LOWER(', '(') 156 | 157 | counter = 1 158 | if args[1].is_a? RDL::Type::FiniteHashType 159 | # the query has named params 160 | args[1].elts.keys.each { |k| base_query.gsub!(":#{k}", counter.to_s); counter += 1 } 161 | else 162 | # the query has ? symbols 163 | args[1..-1].each { |t| base_query.sub!('?', counter.to_s); counter += 1} 164 | end 165 | base_query 166 | end 167 | -------------------------------------------------------------------------------- /sequel/db_types.rb: -------------------------------------------------------------------------------- 1 | class SequelDB 2 | extend RDL::Annotate 3 | 4 | type 'self.[]', '(Symbol) -> ``gen_output_type(targs[0])``', wrap: false 5 | type '[]', '(Symbol) -> ``gen_output_type(targs[0])``', wrap: false 6 | type :transaction, "() { () -> %any } -> self", wrap: false 7 | 8 | 9 | type RDL::Globals, 'self.seq_db_schema', "()-> Hash", wrap: false, effect: [:+, :+] 10 | 11 | def self.gen_output_type(targ) 12 | case targ 13 | when RDL::Type::SingletonType 14 | t = RDL::Globals.seq_db_schema[RDL.type_cast(targ.val, "Symbol", force: true)] 15 | raise "no schema for table #{targ}" if t.nil? 16 | new_t = t.elts.clone.merge({__selected: RDL::Globals.types[:nil], __last_joined: targ, __all_joined: targ, __orm: RDL::Globals.types[:false] }) 17 | new_fht = RDL::Type::FiniteHashType.new(new_t, nil) 18 | return RDL::Type::GenericType.new(RDL::Type::NominalType.new(Table), new_fht) 19 | else 20 | raise "unexpected type" 21 | end 22 | end 23 | 24 | RDL.type SequelDB, 'self.gen_output_type', "(RDL::Type::Type) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 25 | end 26 | 27 | module Sequel::Mysql2; end 28 | 29 | class Sequel::Mysql2::Database 30 | extend RDL::Annotate 31 | ## This class is identical to SequelDB (above), except its name. 32 | ## Necessary to support different apps. 33 | 34 | type 'self.[]', '(Symbol) -> ``gen_output_type(targs[0])``', wrap: false 35 | type '[]', '(Symbol) -> ``gen_output_type(targs[0])``', wrap: false 36 | type :transaction, "() { () -> %any } -> self", wrap: false 37 | 38 | 39 | type RDL::Globals, 'self.seq_db_schema', "()-> Hash", wrap: false, effect: [:+, :+] 40 | 41 | def self.gen_output_type(targ) 42 | case targ 43 | when RDL::Type::SingletonType 44 | t = RDL::Globals.seq_db_schema[RDL.type_cast(targ.val, "Symbol", force: true)] 45 | raise "no schema for table #{targ}" if t.nil? 46 | new_t = t.elts.clone.merge({__selected: RDL::Globals.types[:nil], __last_joined: targ, __all_joined: targ, __orm: RDL::Globals.types[:false] }) 47 | new_fht = RDL::Type::FiniteHashType.new(new_t, nil) 48 | return RDL::Type::GenericType.new(RDL::Type::NominalType.new(Table), new_fht) 49 | else 50 | raise "unexpected type #{targ.class}" 51 | end 52 | end 53 | RDL.type Sequel::Mysql2::Database, 'self.gen_output_type', "(RDL::Type::Type) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 54 | 55 | end 56 | 57 | 58 | module Sequel 59 | extend RDL::Annotate 60 | 61 | type 'self.sqlite', '() -> DBVal', wrap: false 62 | 63 | type 'self.[]', '(Symbol) -> ``gen_output_type(targs[0])``', wrap: false 64 | type 'self.qualify', '(Symbol, Symbol) -> ``qualify_output_type(targs)``', wrap: false 65 | 66 | def self.gen_output_type(targ) 67 | case targ 68 | when RDL::Type::SingletonType 69 | RDL::Type::GenericType.new(RDL::Type::NominalType.new(SeqIdent), targ) 70 | else 71 | raise "unexpected type" 72 | end 73 | end 74 | RDL.type Sequel, 'self.gen_output_type', "(RDL::Type::Type) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 75 | 76 | def self.qualify_output_type(targs) 77 | raise "unexpected types" unless targs.all? { |a| a.is_a?(RDL::Type::SingletonType) } 78 | RDL::Type::GenericType.new(RDL::Type::NominalType.new(SeqQualIdent), targs[0], targs[1]) 79 | end 80 | RDL.type Sequel, 'self.qualify_output_type', "(Array) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 81 | end 82 | class SeqIdent 83 | extend RDL::Annotate 84 | type_params [:t], :all? 85 | type :[], '(Symbol) -> ``gen_output_type(trec, targs[0])``', wrap: false 86 | 87 | def self.gen_output_type(trec, targ) 88 | case trec 89 | when RDL::Type::GenericType 90 | param = trec.params[0] 91 | case targ 92 | when RDL::Type::SingletonType 93 | return RDL::Type::GenericType.new(RDL::Type::NominalType.new(SeqQualIdent), param, targ) 94 | else 95 | raise "expected singleton" 96 | end 97 | else 98 | raise "unexpected trec type" 99 | end 100 | end 101 | RDL.type SeqIdent, 'self.gen_output_type', "(RDL::Type::Type, RDL::Type::Type) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 102 | 103 | end 104 | 105 | class SeqQualIdent 106 | extend RDL::Annotate 107 | type_params [:table, :column], :all? 108 | 109 | end 110 | 111 | class Table 112 | extend RDL::Annotate 113 | type_params [:t], :all? 114 | 115 | type :join, "(Symbol, %any) -> ``join_ret_type(trec, targs)``", wrap: false 116 | type :join, "(Symbol, %any, %any) -> ``join_ret_type(trec, targs)``", wrap: false 117 | 118 | RDL.rdl_alias :Table, :inner_join, :join 119 | RDL.rdl_alias :Table, :left_join, :join 120 | RDL.rdl_alias :Table, :left_outer_join, :join 121 | 122 | def self.get_schema(hash) 123 | hash.select { |key, val| ![:__last_joined, :__all_joined, :__selected, :__orm].member?(key) } 124 | end 125 | RDL.type Table, 'self.get_schema', "(Hash<%any, RDL::Type::Type>) -> Hash<%any, RDL::Type::Type>", typecheck: :type_code, wrap: false, effect: [:+, :+] 126 | 127 | def self.get_all_joined(t) 128 | case t 129 | when RDL::Type::SingletonType 130 | sing = RDL.type_cast(t, "RDL::Type::SingletonType", force: true) 131 | raise "unexpected type #{t} in __all_joined clause" unless sing.val.is_a?(Symbol) 132 | return [sing.val] 133 | when RDL::Type::UnionType 134 | all = t.types.map { |subt| 135 | raise "unexpected type #{subt} in union type within __all_joined clause" unless subt.is_a?(RDL::Type::SingletonType) && RDL.type_cast(subt, "RDL::Type::SingletonType", force: true).val.is_a?(Symbol) 136 | RDL.type_cast(subt, "RDL::Type::SingletonType", force: true).val 137 | } 138 | return all 139 | when nil 140 | return RDL.type_cast([], "Array", force: true) 141 | else 142 | raise "unexpected type #{t} in __all_joined clause" 143 | end 144 | end 145 | RDL.type Table, 'self.get_all_joined', "(RDL::Type::Type) -> Array", typecheck: :type_code, wrap: false, effect: [:+, :+] 146 | 147 | def self.join_ret_type(trec, targs) 148 | raise RDL::Typecheck::StaticTypeError, "Unexpected number of arguments to `join`." unless targs.size == 2 149 | targ1, targ2 = *targs 150 | raise RDL::Typecheck::StaticTypeError, "Unexpected second argument type #{targ2} to `join`." unless targ2.is_a?(RDL::Type::FiniteHashType) 151 | arg_join_column = RDL.type_cast(RDL.type_cast(targ2, "RDL::Type::FiniteHashType").elts.keys[0], "Symbol", force: true) ## column name of arg table which is joined on 152 | rec_join_column = RDL.type_cast(RDL.type_cast(targ2, "RDL::Type::FiniteHashType").elts[arg_join_column], "Symbol", force: true) ## column name of receiver table which is joined on 153 | case trec 154 | when RDL::Type::GenericType 155 | raise RDL::Typecheck::StaticTypeError, "unexpceted generic type in call to join" unless trec.base.name == "Table" 156 | receiver_param = RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts 157 | receiver_schema = get_schema(receiver_param) 158 | join_source_schema = get_schema(RDL::Globals.seq_db_schema[RDL.type_cast(receiver_param[:__last_joined], "RDL::Type::SingletonType", force: true).val].elts) 159 | rec_all_joined = get_all_joined(receiver_param[:__all_joined]) 160 | 161 | case rec_join_column 162 | when RDL::Type::SingletonType 163 | ## given symbol for second column to join on 164 | if rec_join_column.to_s.include?("__") 165 | ## qualified column name in old versions of sequel. 166 | check_qual_column(rec_join_column, rec_all_joined) 167 | else 168 | raise RDL::Typecheck::StaticTypeError, "No column #{rec_join_column} for receiver in call to `join`." if join_source_schema[rec_join_column.val].nil? 169 | end 170 | when RDL::Type::GenericType 171 | ## given qualified column, e.g. Sequel[:people][:name] 172 | raise RDL::Typecheck::StaticTypeError, "unexpected generic type #{rec_join_column}" unless rec_join_column.base.name == "SeqQualIdent" 173 | qual_table, qual_column = rec_join_column.params.map { |t| t.val } 174 | raise RDL::Typecheck::StaticTypeError, "qualified table #{qual_table} is not joined in receiver table, and so its columns cannot be joined on" unless rec_all_joined.include?(qual_table) 175 | qual_table_schema = get_schema(RDL::Globals.seq_db_schema[qual_table].elts) 176 | raise RDL::Typecheck::StaticTypeError, "No column #{qual_column} in table #{qual_table}." if qual_table_schema[qual_column].nil? 177 | else 178 | raise "Unexpected column #{rec_join_column} to join on" 179 | end 180 | case targ1 181 | when RDL::Type::SingletonType 182 | val = RDL.type_cast(targ1.val, "Symbol", force: true) 183 | raise RDL::Typecheck::StaticTypeError, "Expected Symbol for first argument to `join`." unless val.is_a?(Symbol) 184 | table_name = val 185 | table_schema = RDL::Globals.seq_db_schema[table_name] 186 | raise "No schema found for table #{table_name}." unless table_schema 187 | arg_schema = get_schema(table_schema.elts) ## look up table schema for argument 188 | raise RDL::Typecheck::StaticTypeError, "No column #{arg_join_column} for arg in call to `join`." if arg_schema[arg_join_column].nil? 189 | result_schema = receiver_schema.merge(arg_schema).merge({ __all_joined: RDL::Type::UnionType.new(*[receiver_param[:__all_joined], targ1]), __last_joined: targ1, __selected: receiver_param[:__selected], __orm: receiver_param[:__orm] }) ## resulting schema as hash 190 | result_fht = RDL::Type::FiniteHashType.new(result_schema, nil) ## resulting schema as FiniteHashType 191 | 192 | return RDL::Type::GenericType.new(trec.base, result_fht) 193 | when RDL::Type::NominalType 194 | ## TODO: this will catch case that first argument is a non-singleton Symbol 195 | raise "not implemented, likely not needed in practice" 196 | else 197 | raise RDL::Typecheck::StaticTypeError, "Unexpected type of first argument to `join`." 198 | end 199 | when RDL::Type::NominalType 200 | raise RDL::Typecheck::StaticTypeError unless trec.name == "Table" 201 | else 202 | raise RDL::Typecheck::StaticTypeError, "Unexpected receiver type in call to `join`." 203 | end 204 | end 205 | RDL.type Table, 'self.join_ret_type', "(RDL::Type::Type, Array) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 206 | 207 | 208 | type :insert, "(``insert_arg_type(trec, targs)``) -> Integer", wrap: false 209 | type :insert, "(``insert_arg_type(trec, targs, true)``, %any) -> Integer", wrap: false 210 | type :where, "(``where_arg_type(trec, targs)``) -> self", wrap: false 211 | type :where, "(``where_arg_type(trec, targs, true)``, %any) -> self", wrap: false 212 | type :exclude, "(``where_arg_type(trec, targs)``) -> self", wrap: false 213 | type :exclude, "(``where_arg_type(trec, targs, true)``, %any) -> self", wrap: false 214 | type :[], "(``where_arg_type(trec, targs)``) -> ``first_output(trec)``", wrap: false 215 | type :first, "() -> ``first_output(trec)``", wrap: false 216 | type :first, "(``if targs[0] then where_arg_type(trec, targs) else RDL::Globals.types[:bot] end``) -> ``first_output(trec)``", wrap: false 217 | type :get, '(``get_input(trec)``) -> ``get_output(trec, targs)``', wrap: false 218 | type :order, '(``order_input(trec, targs)``) -> self', wrap: false 219 | type Sequel, 'self.desc', '(%any) -> ``targs[0]``', wrap: false ## args will ultimately be checked by `order` 220 | type :select_map, '(Symbol) -> ``select_map_output(trec, targs, :select_map)``', wrap: false 221 | type :pluck, '(Symbol) -> ``select_map_output(trec, targs, :select_map)``', wrap: false 222 | type :any?, "() -> %bool", wrap: false 223 | type :select, "(*%any) -> ``select_map_output(trec, targs, :select)``", wrap: false 224 | type :all, "() -> ``all_output(trec)``", wrap: false 225 | type Sequel, 'self.lit', "(%any) -> String", wrap: false 226 | type :server, "(Symbol) -> self", wrap: false 227 | type :empty?, '() -> %bool', wrap: false 228 | type :update, "(``insert_arg_type(trec, targs)``) -> Integer", wrap: false 229 | type :count, "() -> Integer", wrap: false 230 | type :map, "() { (``map_block_input(trec)``) -> x } -> Array", wrap: false 231 | type :each, "() { (``map_block_input(trec)``) -> x } -> self", wrap: false 232 | type :import, "(``import_arg_type(trec, targs)``, Array) -> Array", wrap: false 233 | 234 | def self.order_input(trec, targs) 235 | case trec 236 | when RDL::Type::GenericType 237 | trp0 = RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true) 238 | sym_keys = get_schema(trp0.elts).keys 239 | all_joined = get_all_joined(trp0.elts[:__all_joined]) 240 | targs.each { |a| 241 | case a 242 | when RDL::Type::SingletonType 243 | return RDL::Globals.types[:bot] unless sym_keys.include?(a.val) 244 | when RDL::Type::GenericType 245 | return RDL::Globals.types[:bot] unless a.base.name == "SeqQualIdent" 246 | check_qual_column(a, all_joined) 247 | end 248 | } 249 | return RDL::Type::VarargType.new(RDL::Type::UnionType.new(*targs)) 250 | else 251 | raise "unexpected type #{trec}" 252 | end 253 | end 254 | RDL.type Table, 'self.order_input', "(RDL::Type::Type, Array) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 255 | 256 | def self.map_block_input(trec) 257 | schema = get_schema(RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts) 258 | RDL::Type::FiniteHashType.new(schema, nil) 259 | end 260 | RDL.type Table, 'self.map_block_input', "(RDL::Type::GenericType) -> RDL::Type::FiniteHashType", typecheck: :type_code, wrap: false, effect: [:+, :+] 261 | 262 | def self.all_output(trec) 263 | f = first_output(trec) 264 | if f.is_a?(RDL::Type::FiniteHashType) 265 | trp0 = RDL.type_cast(RDL.type_cast(trec, "RDL::Type::GenericType").params[0], "RDL::Type::FiniteHashType", force: true) 266 | selected = trp0.elts[:__selected] 267 | all_joined = get_all_joined(trp0.elts[:__all_joined]) 268 | if !(selected == RDL::Globals.types[:nil]) 269 | ## something is selected 270 | sel_arr = RDL.type_cast(selected.is_a?(RDL::Type::UnionType) ? RDL.type_cast(selected, "RDL::Type::UnionType").types : [selected], "Array>", force: true) 271 | new_hash = Hash[sel_arr.map { |sel| 272 | if sel.val.to_s.include?("__") 273 | t = check_qual_column(sel.val, all_joined) 274 | _, col_name = sel.val.to_s.split "__" 275 | col_name = col_name.to_sym 276 | [col_name, t] 277 | else 278 | raise "no selected column found" unless (t = RDL.type_cast(f, "RDL::Type::FiniteHashType", force: true).elts[sel.val]) 279 | [sel.val, t] 280 | end 281 | }] 282 | new_hash_type = RDL::Type::FiniteHashType.new(RDL.type_cast(new_hash, "Hash<%any, RDL::Type::Type>", force: true), nil) 283 | return RDL::Type::GenericType.new(RDL::Globals.types[:array], new_hash_type) 284 | else 285 | return RDL::Type::GenericType.new(RDL::Globals.types[:array], f) 286 | end 287 | else 288 | return RDL::Type::GenericType.new(RDL::Globals.types[:array], f) 289 | end 290 | end 291 | RDL.type Table, 'self.all_output', "(RDL::Type::Type) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 292 | 293 | def self.select_map_output(trec, targs, meth) 294 | case trec 295 | when RDL::Type::GenericType 296 | raise RDL::Typecheck::StaticTypeError, 'unexpected type' unless trec.base.name == "Table" 297 | receiver_param = RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts 298 | all_joined = get_all_joined(receiver_param[:__all_joined]) 299 | 300 | map_types = targs.map { |arg| 301 | case arg 302 | when RDL::Type::SingletonType 303 | column = RDL.type_cast(arg.val, "Symbol", force: true) 304 | raise "unexpected arg type #{arg}" unless column.is_a?(Symbol) 305 | raise "Ambiguous column identifier #{arg}." unless unique_ids?([column], receiver_param[:__all_joined]) 306 | if column.to_s.include?("__") 307 | check_qual_column(column, all_joined) 308 | else 309 | raise "No column #{column} in receiver table." unless receiver_param[column] 310 | receiver_param[column] 311 | end 312 | when RDL::Type::GenericType 313 | raise "unexpected arg type #{arg}" unless arg.base.name == "SeqQualIdent" 314 | check_qual_column(arg, all_joined) 315 | else 316 | raise "unexpected arg type #{arg}" 317 | end 318 | } 319 | 320 | targs.each { |arg| 321 | case arg 322 | when RDL::Type::SingletonType 323 | column = RDL.type_cast(arg.val, "Symbol", force: true) 324 | raise "unexpected arg type #{arg}" unless column.is_a?(Symbol) 325 | raise "Ambiguous column identifier #{arg}." unless unique_ids?([column], receiver_param[:__all_joined]) 326 | if column.to_s.include?("__") 327 | map_types = map_types + [check_qual_column(column, all_joined)] 328 | else 329 | raise "No column #{column} in receiver table." unless receiver_param[column] 330 | map_types = map_types + [receiver_param[column]] 331 | end 332 | when RDL::Type::GenericType 333 | raise "unexpected arg type #{arg}" unless arg.base.name == "SeqQualIdent" 334 | map_types = map_types + [check_qual_column(arg, all_joined)] 335 | else 336 | raise "unexpected arg type #{arg}" 337 | end 338 | } 339 | if meth == :select 340 | result_schema = receiver_param.clone.merge({ __selected: RDL::Type::UnionType.new(*targs).canonical }) 341 | return RDL::Type::GenericType.new(trec.base, RDL::Type::FiniteHashType.new(result_schema, nil)) 342 | elsif meth == :select_map 343 | if targs.size >1 344 | return RDL::Type::GenericType.new(RDL::Globals.types[:array], RDL::Type::TupleType.new(*map_types)) 345 | else 346 | return RDL::Type::GenericType.new(RDL::Globals.types[:array], map_types[0]) 347 | end 348 | else 349 | raise 'unexpected' 350 | end 351 | else 352 | raise 'unexpected type #{trec}' 353 | end 354 | end 355 | RDL.type Table, 'self.select_map_output', "(RDL::Type::Type, Array, Symbol) -> RDL::Type::GenericType", typecheck: :type_code, wrap: false, effect: [:+, :+] 356 | 357 | def self.get_input(trec) 358 | case trec 359 | when RDL::Type::GenericType 360 | sym_keys = get_schema(RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts).keys 361 | RDL::Type::UnionType.new(*RDL.type_cast(sym_keys.map { |k| RDL::Type::SingletonType.new(k) }, "Array")) 362 | else 363 | raise 'unexpected type #{trec}' 364 | end 365 | end 366 | RDL.type Table, 'self.get_input', "(RDL::Type::Type) -> RDL::Type::UnionType", typecheck: :type_code, wrap: false, effect: [:+, :+] 367 | 368 | def self.get_output(trec, targs) 369 | RDL.type_cast(RDL.type_cast(trec, "RDL::Type::GenericType").params[0], "RDL::Type::FiniteHashType", force: true).elts[RDL.type_cast(targs[0], "RDL::Type::SingletonType", force: true).val] 370 | end 371 | RDL.type Table, 'self.get_output', "(RDL::Type::Type, Array) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 372 | 373 | def self.first_output(trec) 374 | case trec 375 | when RDL::Type::GenericType 376 | raise RDL::Typecheck::StaticTypeError, 'unexpected type' unless trec.base.name == "Table" 377 | receiver_param = RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts 378 | if !(receiver_param[:__orm] == RDL::Globals.types[:false]) 379 | receiver_param[:__orm] 380 | else 381 | RDL::Type::FiniteHashType.new(get_schema(receiver_param), nil) 382 | end 383 | else 384 | raise 'unexpected type #{trec}' 385 | end 386 | end 387 | RDL.type Table, 'self.first_output', "(RDL::Type::Type) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 388 | 389 | def self.insert_arg_type(trec, targs, tuple=false) 390 | raise "Cannot insert/update for joined table." if RDL.type_cast(RDL.type_cast(trec, "RDL::Type::GenericType").params[0], "RDL::Type::FiniteHashType", force: true).elts[:__all_joined].is_a?(RDL::Type::UnionType) 391 | if tuple 392 | schema_arg_tuple_type(trec, targs, :insert) 393 | else 394 | schema_arg_type(trec, targs, :insert) 395 | end 396 | end 397 | RDL.type Table, 'self.insert_arg_type', "(RDL::Type::Type, Array, ?%bool) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 398 | 399 | def self.import_arg_type(trec, targs) 400 | raise "Cannot import for joined table." if RDL.type_cast(RDL.type_cast(trec, "RDL::Type::GenericType").params[0], "RDL::Type::FiniteHashType", force: true).elts[:__all_joined].is_a?(RDL::Type::UnionType) 401 | raise "Expected tuple for first arg to `import`, got #{targs[0]} instead." unless targs[0].is_a?(RDL::Type::TupleType) 402 | arg1 = targs[1] 403 | case arg1 404 | when RDL::Type::TupleType 405 | arg1.params.each { |t| schema_arg_tuple_type(trec, [targs[0], t], :import) } ## check each individual tuple inside second arg tuple 406 | when RDL::Type::GenericType 407 | raise "expected Array, got #{arg1}" unless (arg1.base == RDL::Globals.types[:array]) 408 | raise "`import` type not yet implemented for type #{arg1}" unless arg1.params[0].is_a?(RDL::Type::TupleType) 409 | schema_arg_tuple_type(trec, [targs[0], arg1.params[0]], :import) 410 | else 411 | raise "Not yet implemented for type #{arg1}." 412 | end 413 | return targs[0] 414 | end 415 | RDL.type Table, 'self.import_arg_type', "(RDL::Type::Type, Array) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 416 | 417 | def self.get_nominal_where_type(type) 418 | ## `where` can accept arrays/tuples and tables with a single column selected 419 | ## this method just extracts the parameter type 420 | case type 421 | when RDL::Type::GenericType 422 | if type.base == RDL::Globals.types[:array] 423 | type.params[0] 424 | elsif type.base == RDL::Type::NominalType.new(Table) 425 | schema = RDL.type_cast(type.params[0], "RDL::Type::FiniteHashType", force: true).elts 426 | sel = schema[:__selected] 427 | raise "Call to where expects table with a single column selected, got #{type}" unless sel.is_a?(RDL::Type::SingletonType) 428 | nominal = schema[RDL.type_cast(sel, "RDL::Type::SingletonType", force: true).val] 429 | raise "No type found for column #{sel} in call to `where`." unless nominal 430 | nominal 431 | else 432 | type 433 | end 434 | when RDL::Type::TupleType 435 | type = type.promote.params[0] 436 | raise "`where` passed tuple containing different types." if type.is_a?(RDL::Type::UnionType) 437 | type 438 | else 439 | type 440 | end 441 | end 442 | 443 | RDL.type Table, 'self.get_nominal_where_type', "(RDL::Type::Type) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 444 | 445 | def self.schema_arg_type(trec, targs, meth) 446 | return RDL::Type::NominalType.new(Hash) if targs.size != 1 447 | case trec 448 | when RDL::Type::GenericType 449 | raise RDL::Typecheck::StaticTypeError, 'unexpected type' unless trec.base.name == "Table" 450 | receiver_param = RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts 451 | receiver_schema = get_schema(receiver_param) 452 | all_joined = get_all_joined(receiver_param[:__all_joined]) 453 | arg0 = targs[0] 454 | case arg0 455 | when RDL::Type::FiniteHashType 456 | insert_hash = arg0.elts 457 | insert_hash.each { |column_name, type| 458 | cn = RDL.type_cast(column_name, "Symbol", force: true) 459 | if cn.to_s.include?("__") && (meth == :where) 460 | check_qual_column(cn, all_joined, type) 461 | else 462 | raise RDL::Typecheck::StaticTypeError, "No column #{column_name} for receiver #{trec}." unless receiver_schema.has_key?(cn) 463 | type = get_nominal_where_type(type) if (meth == :where) 464 | raise RDL::Typecheck::StaticTypeError, "Incompatible column types #{type} and #{receiver_schema[column_name]} for column #{cn} in call to #{meth}." unless RDL::Type::Type.leq(type, receiver_schema[cn]) 465 | end 466 | } 467 | return arg0 468 | else 469 | return arg0 if (meth==:where) && targs[0] <= RDL::Globals.types[:string] 470 | raise "TODO WITH #{trec} AND #{targs} AND #{meth}" 471 | end 472 | when RDL::Type::NominalType 473 | ##TODO 474 | else 475 | 476 | end 477 | end 478 | RDL.type Table, 'self.schema_arg_type', "(RDL::Type::Type, Array, Symbol) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 479 | 480 | ## [+ column_name +] if a symbol or SeqQualIdent Generic type of the qualified column name, e.g. :person__age 481 | ## [+ all_joined +] is an array of symbols of joined tables (must check if qualifying table is a member) 482 | ## [+ type +] is optional RDL type. If given, we check that it matches the type of the column in the schema. 483 | ## returns type of given column 484 | def self.check_qual_column(column_name, all_joined, type=nil) 485 | case column_name 486 | when RDL::Type::GenericType 487 | cn = RDL.type_cast(column_name, "RDL::Type::GenericType", force: true) 488 | raise "Expected qualified column type." unless cn.base.name == "SeqQualIdent" 489 | qual_table, qual_column = cn.params.map { |t| RDL.type_cast(t, "RDL::Type::SingletonType", force: true).val } 490 | else 491 | ## symbol with name including underscores 492 | qual_table, qual_column = RDL.type_cast(column_name, "Symbol", force: true).to_s.split "__" 493 | qual_table = if qual_table.start_with?(":") then qual_table[1..-1].to_sym else qual_table.to_sym end 494 | qual_column = qual_column.to_sym 495 | end 496 | raise RDL::Typecheck::StaticTypeError, "qualified table #{qual_table} is not joined in receiver table, cannot reference its columns" unless all_joined.include?(qual_table) 497 | qual_table_schema = get_schema(RDL::Globals.seq_db_schema[qual_table].elts) 498 | raise RDL::Typecheck::StaticTypeError, "No column #{qual_column} in table #{qual_table}." if qual_table_schema[qual_column].nil? 499 | if type 500 | types = (if type.is_a?(RDL::Type::UnionType) then RDL.type_cast(type, "RDL::Type::UnionType", force: true).types else [type] end) 501 | types.each { |t| 502 | t = RDL.type_cast(t, "RDL::Type::GenericType", force: true).params[0] if t.is_a?(RDL::Type::GenericType) && (RDL.type_cast(t, "RDL::Type::GenericType", force: true).base == RDL::Globals.types[:array]) ## only happens if meth is where, don't need to check 503 | raise RDL::Typecheck::StaticTypeError, "Incompatible column types. Given #{t} but expected #{qual_table_schema[qual_column]} for column #{column_name}." unless RDL::Type::Type.leq(t, qual_table_schema[qual_column]) 504 | } 505 | end 506 | return qual_table_schema[qual_column] 507 | end 508 | RDL.type Table, 'self.check_qual_column', "(Symbol or RDL::Type::GenericType, Array, ?RDL::Type::Type) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 509 | 510 | def self.schema_arg_tuple_type(trec, targs, meth) 511 | return RDL::Type::NominalType.new(Array) if targs.size != 2 512 | case trec 513 | when RDL::Type::GenericType 514 | raise RDL::Typecheck::StaticTypeError, 'unexpected type' unless trec.base.name == "Table" 515 | receiver_param = RDL.type_cast(trec.params[0], "RDL::Type::FiniteHashType", force: true).elts 516 | all_joined = get_all_joined(receiver_param[:__all_joined]) 517 | receiver_schema = get_schema(receiver_param) 518 | if targs[0].is_a?(RDL::Type::TupleType) && targs[1].is_a?(RDL::Type::TupleType) 519 | RDL.type_cast(targs[0], "RDL::Type::TupleType", force: true).params.each_with_index { |column_name, i| 520 | cn = RDL.type_cast(column_name, "RDL::Type::SingletonType", force: true) 521 | raise "Expected singleton symbol in call to insert, got #{column_name}" unless cn.is_a?(RDL::Type::SingletonType) && cn.val.is_a?(Symbol) 522 | type = RDL.type_cast(targs[1], "RDL::Type::TupleType", force: true).params[i] 523 | if cn.val.to_s.include?("__") && (meth == :where) 524 | check_qual_column(cn.val, all_joined, type) 525 | else 526 | raise RDL::Typecheck::StaticTypeError, "No column #{column_name} for receiver in call to `insert`." unless receiver_schema.has_key?(cn.val) 527 | type = get_nominal_where_type(type) if (meth == :where) 528 | raise RDL::Typecheck::StaticTypeError, "Incompatible column types." unless RDL::Type::Type.leq(type, receiver_schema[cn.val]) 529 | end 530 | } 531 | return targs[0] 532 | else 533 | raise "not yet implemented for types #{targs[0]} and #{targs[1]}" 534 | end 535 | else 536 | raise 'not yet implemented' 537 | end 538 | end 539 | RDL.type Table, 'self.schema_arg_tuple_type', "(RDL::Type::Type, Array, Symbol) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 540 | 541 | def self.where_arg_type(trec, targs, tuple=false) 542 | trp0 = RDL.type_cast(RDL.type_cast(trec, "RDL::Type::GenericType").params[0], "RDL::Type::FiniteHashType", force: true) 543 | if trp0.elts[:__all_joined].is_a?(RDL::Type::UnionType) 544 | arg0 = targs[0] 545 | case arg0 546 | when RDL::Type::TupleType 547 | raise "Unexpected column type." unless arg0.params.all? { |t| t.is_a?(RDL::Type::SingletonType) && RDL.type_cast(t, "RDL::Type::SingletonType", force: true).val.is_a?(Symbol) } 548 | raise "Ambigious identifier in call to where." unless unique_ids?(arg0.params.map { |t| RDL.type_cast(t, "RDL::Type::SingletonType", force: true).val }, trp0.elts[:__all_joined]) 549 | when RDL::Type::FiniteHashType 550 | raise "Ambigious identifier in call to where." unless unique_ids?(RDL.type_cast(arg0.elts.keys, "Array", force: true), trp0.elts[:__all_joined]) 551 | else 552 | raise "unexpected arg type #{arg0}" 553 | end 554 | end 555 | if tuple 556 | schema_arg_tuple_type(trec, targs, :where) 557 | else 558 | schema_arg_type(trec, targs, :where) 559 | end 560 | end 561 | RDL.type Table, 'self.where_arg_type', "(RDL::Type::Type, Array, ?%bool) -> RDL::Type::Type", typecheck: :type_code, wrap: false, effect: [:+, :+] 562 | 563 | def self.unique_ids?(ids, joined) 564 | j = get_all_joined(joined) 565 | count = Hash[ids.map { |id| [id, 0] }] 566 | 567 | j.each { |t1| 568 | schema1 = RDL::Globals.seq_db_schema[t1] 569 | raise "schema not found" unless schema1 570 | j.each { |t2| 571 | schema2 = RDL::Globals.seq_db_schema[t2] 572 | ids.each { |id| 573 | return false if schema1.elts.has_key?(id) && schema2.elts.has_key?(id) && (t1 != t2) 574 | } 575 | } 576 | } 577 | return true 578 | end 579 | RDL.type Table, 'self.unique_ids?', "(Array, RDL::Type::Type) -> %bool", typecheck: :type_code, wrap: false, effect: [:+, :+] 580 | 581 | end 582 | -------------------------------------------------------------------------------- /active-record/db_types.rb: -------------------------------------------------------------------------------- 1 | require_relative 'sql-strings.rb' 2 | 3 | NESTED_JOINS = false 4 | 5 | class ActiveRecord::Base 6 | extend RDL::Annotate 7 | 8 | 9 | type Object, :try, "(Symbol) -> Object", wrap: false 10 | type Object, :present?, "() -> %bool", wrap: false 11 | type :initialize, '(``DBType.rec_to_schema_type(trec, true)``) -> self', wrap: false 12 | type 'self.create', '(``DBType.rec_to_schema_type(trec, true)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 13 | type 'self.create!', '(``DBType.rec_to_schema_type(trec, true)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 14 | type :initialize, '() -> self', wrap: false 15 | type 'self.create', '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 16 | type 'self.create!', '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 17 | type :attribute_names, "() -> Array", wrap: false 18 | type :to_json, "(?{ only: Array }) -> String", wrap: false 19 | type :update_column, '(``uc_first_arg(trec)``, ``uc_second_arg(trec, targs)``) -> %bool', wrap: false 20 | type :[], '(Symbol) -> ``access_output(trec, targs)``', wrap: false 21 | type RDL::Globals, 'self.ar_db_schema', "() -> Hash<%any, RDL::Type::GenericType>", wrap: false, effect: [:+, :+] 22 | type String, :singularize, "() -> String", wrap: false, effect: [:+, :+] 23 | type String, :camelize, "() -> String", wrap: false, effect: [:+, :+] 24 | type String, :pluralize, "() -> String", wrap: false, effect: [:+, :+] 25 | type String, :underscore, "() -> String", wrap: false, effect: [:+, :+] 26 | 27 | def self.access_output(trec, targs) 28 | case trec 29 | when RDL::Type::NominalType 30 | tname = trec.name.to_sym 31 | tschema = RDL.type_cast(RDL::Globals.ar_db_schema[tname].params[0], "RDL::Type::FiniteHashType", force: true).elts 32 | raise "Schema not found." unless tschema 33 | arg = targs[0] 34 | case arg 35 | when RDL::Type::SingletonType 36 | col = arg.val 37 | ret = tschema[col] 38 | ret = RDL::Globals.types[:nil] unless ret 39 | return ret 40 | else 41 | raise "TODO" 42 | end 43 | else 44 | raise 'unexpected type' 45 | end 46 | end 47 | RDL.type ActiveRecord::Base, 'self.access_output', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 48 | 49 | def self.uc_first_arg(trec) 50 | case trec 51 | when RDL::Type::NominalType 52 | tname = trec.name.to_sym 53 | tschema = RDL.type_cast(RDL::Globals.ar_db_schema[tname].params[0], "RDL::Type::FiniteHashType", force: true).elts 54 | raise "Schema not found." unless tschema 55 | typs = RDL.type_cast(tschema.keys, "Array", force: true).reject { |k| k == :__associations}.map { |k| RDL::Type::SingletonType.new(k) } 56 | return RDL::Type::UnionType.new(*RDL.type_cast(typs, "Array")) 57 | else 58 | raise "unexpected type" 59 | end 60 | end 61 | RDL.type ActiveRecord::Base, 'self.uc_first_arg', "(RDL::Type::Type) -> RDL::Type::UnionType", wrap: false, typecheck: :type_code, effect: [:+, :+] 62 | 63 | def self.uc_second_arg(trec, targs) 64 | case trec 65 | when RDL::Type::NominalType 66 | tname = trec.name.to_sym 67 | tschema = RDL.type_cast(RDL::Globals.ar_db_schema[tname].params[0], "RDL::Type::FiniteHashType", force: true).elts 68 | raise "Schema not found." unless tschema 69 | raise "Unexpected first arg type." unless targs[0].is_a?(RDL::Type::SingletonType) && RDL.type_cast(targs[0], "RDL::Type::SingletonType").val.is_a?(Symbol) 70 | return tschema[RDL.type_cast(targs[0], "RDL::Type::SingletonType").val] 71 | else 72 | raise "unexpected type" 73 | end 74 | end 75 | RDL.type ActiveRecord::Base, 'self.uc_second_arg', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 76 | 77 | 78 | end 79 | 80 | module ActiveRecord::AutosaveAssociation 81 | extend RDL::Annotate 82 | type :reload, "() -> %any", wrap: false 83 | end 84 | 85 | module ActiveRecord::Transactions 86 | extend RDL::Annotate 87 | type :destroy, '() -> self', wrap: false 88 | type :save, '(?{ validate: %bool }) -> %bool', wrap: false 89 | end 90 | 91 | module ActiveRecord::Suppressor 92 | extend RDL::Annotate 93 | 94 | type :save!, '() -> %bool', wrap: false 95 | end 96 | 97 | module ActiveRecord::Core::ClassMethods 98 | extend RDL::Annotate 99 | ## Types from this module are used when receiver is ActiveRecord::Base 100 | 101 | type :find, '(Integer or String) -> ``DBType.find_output_type(trec, targs)``', wrap: false 102 | type :find, '(Array) -> ``DBType.find_output_type(trec, targs)``', wrap: false 103 | type :find, '(Integer, Integer, *Integer) -> ``DBType.find_output_type(trec, targs)``', wrap: false 104 | type :find_by, '(``DBType.find_input_type(trec, targs)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 105 | ## TODO: find_by's with conditions given as string 106 | end 107 | 108 | module ActiveRecord::FinderMethods 109 | extend RDL::Annotate 110 | ## Types from this module are used when receiver is ActiveRecord_Relation 111 | 112 | type :find, '(Integer or String) -> ``DBType.find_output_type(trec, targs)``', wrap: false 113 | type :find, '(Array) -> ``DBType.find_output_type(trec, targs)``', wrap: false 114 | type :find, '(Integer, Integer, *Integer) -> ``DBType.find_output_type(trec, targs)``', wrap: false 115 | type :find_by, '(``DBType.find_input_type(trec, targs)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 116 | type :first, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 117 | type :first!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 118 | type :first, '(Integer) -> ``DBType.rec_to_array(trec)``', wrap: false 119 | type :last, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 120 | type :last!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 121 | type :last, '(Integer) -> ``DBType.rec_to_array(trec)``', wrap: false 122 | type :take, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 123 | type :take!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 124 | type :take, '(Integer) -> ``DBType.rec_to_array(trec)``', wrap: false 125 | type :exists?, '(``DBType.exists_input_type(trec, targs)``) -> %bool', wrap: false 126 | end 127 | 128 | module ActiveRecord::Querying 129 | extend RDL::Annotate 130 | ## Types from this module are used when receiver is ActiveRecord::Base 131 | 132 | type :first, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 133 | type :first!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 134 | type :first, '(Integer) -> ``DBType.rec_to_array(trec)``', wrap: false 135 | type :last, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 136 | type :last!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 137 | type :last, '(Integer) -> ``DBType.rec_to_array(trec)``', wrap: false 138 | type :take, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 139 | type :take!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 140 | type :take, '(Integer) -> ``DBType.rec_to_array(trec)``', wrap: false 141 | type :exists?, '(``DBType.exists_input_type(trec, targs)``) -> %bool', wrap: false 142 | 143 | type :where, '(``DBType.where_input_type(trec, targs)``) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 144 | type :where, '(String, *%any) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 145 | type :where, '() -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord::QueryMethods::WhereChain), DBType.rec_to_nominal(trec))``', wrap: false 146 | 147 | 148 | type :joins, '(``DBType.joins_one_input_type(trec, targs)``) -> ``DBType.joins_output(trec, targs)``', wrap: false 149 | type :joins, '(``DBType.joins_multi_input_type(trec, targs)``, %any, *%any) -> ``DBType.joins_output(trec, targs)``', wrap: false 150 | type :group, '(*Symbol or String) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 151 | type :select, '(Symbol or String or Array, *Symbol or String or Array) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 152 | type :select, '() { (self) -> %bool } -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 153 | type :order, '(%any) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 154 | type :includes, '(``DBType.joins_one_input_type(trec, targs)``) -> ``DBType.joins_output(trec, targs)``', wrap: false 155 | type :includes, '(``DBType.joins_multi_input_type(trec, targs)``, %any, *%any) -> ``DBType.joins_output(trec, targs)``', wrap: false 156 | type :limit, '(Integer) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 157 | type :count, '() -> Integer', wrap: false 158 | type :count, '(``DBType.count_input(trec, targs)``) -> Integer', wrap: false 159 | type :sum, '(``DBType.count_input(trec, targs)``) -> Integer', wrap: false 160 | type :destroy_all, '() -> ``DBType.rec_to_array(trec)``', wrap: false 161 | type :delete_all, '() -> Integer', wrap: false 162 | type :references, '(Symbol, *Symbol) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 163 | end 164 | 165 | module ActiveRecord::QueryMethods 166 | extend RDL::Annotate 167 | ## Types from this module are used when receiver is ActiveRecord_relation 168 | 169 | type :where, '(``DBType.where_input_type(trec, targs)``) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 170 | type :where, '(String, *%any) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 171 | #type :where, '(String, *String) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), trec.params[0])``', wrap: false 172 | type :where, '() -> ``DBType.where_noarg_output_type(trec)``', wrap: false 173 | 174 | type :joins, '(``DBType.joins_one_input_type(trec, targs)``) -> ``DBType.joins_output(trec, targs)``', wrap: false 175 | type :joins, '(``DBType.joins_multi_input_type(trec, targs)``, %any, *%any) -> ``DBType.joins_output(trec, targs)``', wrap: false 176 | type :group, '(*Symbol or String) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 177 | type :select, '(Symbol or String or Array, *Symbol or String or Array) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 178 | type :select, '() { (``RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0]``) -> %bool } -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 179 | type :order, '(%any) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 180 | type :includes, '(``DBType.joins_one_input_type(trec, targs)``) -> ``DBType.joins_output(trec, targs)``', wrap: false 181 | type :includes, '(``DBType.joins_multi_input_type(trec, targs)``, %any, *%any) -> ``DBType.joins_output(trec, targs)``', wrap: false 182 | type :limit, '(Integer) -> ``trec``', wrap: false 183 | type :references, '(Symbol, *Symbol) -> self', wrap: false 184 | end 185 | 186 | 187 | class ActiveRecord::QueryMethods::WhereChain 188 | extend RDL::Annotate 189 | type_params [:t], :dummy 190 | 191 | type :not, '(``DBType.not_input_type(trec, targs)``) -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), RDL.type_cast(trec, "RDL::Type::GenericType", force: true).params[0])``', wrap: false 192 | 193 | end 194 | 195 | module ActiveRecord::Delegation 196 | extend RDL::Annotate 197 | 198 | type :+, '(%any) -> ``DBType.plus_output_type(trec, targs)``', wrap: false 199 | 200 | end 201 | 202 | class JoinTable 203 | extend RDL::Annotate 204 | type_params [:orig, :joined], :dummy 205 | ## type param :orig will be nominal type of base table in join 206 | ## type param :joined will be a union type of all joined tables, or just a nominal type if there's only one 207 | 208 | ## this class is meant to only be the type parameter of ActiveRecord_Relation or WhereChain, expressing multiple joined tables instead of just a single table 209 | end 210 | 211 | 212 | 213 | module ActiveRecord::Scoping::Named::ClassMethods 214 | extend RDL::Annotate 215 | type :all, '() -> ``RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), DBType.rec_to_nominal(trec))``', wrap: false 216 | 217 | end 218 | 219 | module ActiveRecord::Persistence 220 | extend RDL::Annotate 221 | type :update!, '(``DBType.rec_to_schema_type(trec, true)``) -> %bool', wrap: false 222 | end 223 | 224 | module ActiveRecord::Calculations 225 | extend RDL::Annotate 226 | type :count, '() -> Integer', wrap: false 227 | type :count, '(``DBType.count_input(trec, targs)``) -> Integer', wrap: false 228 | type :sum, '(``DBType.count_input(trec, targs)``) -> Integer', wrap: false 229 | end 230 | 231 | class ActiveRecord_Relation 232 | ## In practice, this is actually a private class nested within 233 | ## each ActiveRecord::Base, e.g. Person::ActiveRecord_Relation. 234 | ## Using this class just for type checking. 235 | extend RDL::Annotate 236 | include ActiveRecord::QueryMethods 237 | include ActiveRecord::FinderMethods 238 | include ActiveRecord::Calculations 239 | include ActiveRecord::Delegation 240 | 241 | type_params [:t], :dummy 242 | 243 | type :each, '() -> Enumerator', wrap: false 244 | type :each, '() { (t) -> %any } -> Array', wrap: false 245 | type :empty?, '() -> %bool', wrap: false 246 | type :present?, '() -> %bool', wrap: false 247 | type :create, '(``DBType.rec_to_schema_type(trec, true)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 248 | type :create, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 249 | type :create!, '(``DBType.rec_to_schema_type(trec, true)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 250 | type :create!, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 251 | type :new, '(``DBType.rec_to_schema_type(trec, true)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 252 | type :new, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 253 | type :build, '(``DBType.rec_to_schema_type(trec, true)``) -> ``DBType.rec_to_nominal(trec)``', wrap: false 254 | type :build, '() -> ``DBType.rec_to_nominal(trec)``', wrap: false 255 | type :destroy_all, '() -> ``DBType.rec_to_array(trec)``', wrap: false 256 | type :delete_all, '() -> Integer', wrap: false 257 | type :map, '() { (t) -> u } -> Array' 258 | type :all, '() -> self', wrap: false ### kind of a silly method, always just returns self 259 | type :collect, "() { (t) -> u } -> Array", wrap: false 260 | type :find_each, "() { (t) -> x } -> nil", wrap: false 261 | type :to_a, "() -> ``DBType.rec_to_array(trec)``", wrap: false 262 | type :[], "(Integer) -> t", wrap: false 263 | type :size, "() -> Integer", wrap: false 264 | type :update_all, '(``DBType.rec_to_schema_type(trec, true)``) -> Integer', wrap: false 265 | end 266 | 267 | 268 | class DBType 269 | ## given a type (usually representing a receiver type in a method call), this method returns the nominal type version of that type. 270 | ## if the given type represents a joined table, then we return the nominal type version of the *base* of the joined table. 271 | ## [+ t +] is the type for which we want the nominal type. 272 | def self.rec_to_nominal(t) 273 | case t 274 | when RDL::Type::SingletonType 275 | val = RDL.type_cast(t.val, "Class", force: true) 276 | raise RDL::Typecheck::StaticTypeError, "Expected class singleton type, got #{val} instead." unless val.is_a?(Class) 277 | return RDL::Type::NominalType.new(val) 278 | when RDL::Type::GenericType 279 | raise RDL::Typecheck::StaticTypeError, "got unexpected type #{t}" unless t.base.klass == ActiveRecord_Relation 280 | param = t.params[0] 281 | case param 282 | when RDL::Type::GenericType 283 | ## should be JoinTable 284 | ## When getting an indivual record from a join table, record will be of type of the base table in the join 285 | raise RDL::Typecheck::StaticTypeError, "got unexpected type #{param}" unless param.base.klass == JoinTable 286 | return param.params[0] 287 | when RDL::Type::NominalType 288 | return param 289 | else 290 | raise RDL::Typecheck::StaticTypeError, "got unexpected type #{t.params[0]}" 291 | end 292 | end 293 | end 294 | RDL.type DBType, 'self.rec_to_nominal', "(RDL::Type::Type) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 295 | 296 | def self.rec_to_array(trec) 297 | RDL::Type::GenericType.new(RDL::Globals.types[:array], rec_to_nominal(trec)) 298 | end 299 | RDL.type DBType, 'self.rec_to_array', "(RDL::Type::Type) -> RDL::Type::GenericType", wrap: false, typecheck: :type_code, effect: [:+, :+] 300 | 301 | ## given a receiver type in various kinds of query calls, returns the accepted finite hash type input, 302 | ## or a union of types if the receiver represents joined tables. 303 | ## [+ trec +] is the type of the receiver in the method call. 304 | ## [+ check_col +] is a boolean indicating whether or not the column types (i.e., values in the finite hash type) will be checked. 305 | def self.rec_to_schema_type(trec, check_col, takes_array=false) 306 | case trec 307 | when RDL::Type::GenericType 308 | raise "Unexpected type #{trec}." unless (trec.base.klass == ActiveRecord_Relation) || (trec.base.klass == ActiveRecord::QueryMethods::WhereChain) 309 | param = trec.params[0] 310 | case param 311 | when RDL::Type::GenericType 312 | ## should be JoinTable 313 | raise "unexpected type #{trec}" unless param.base.klass == JoinTable 314 | base_name = RDL.type_cast(param.params[0], "RDL::Type::NominalType", force: true).klass.to_s.singularize.to_sym ### singularized symbol name of first param in JoinTable, which is base table of the joins 315 | type_hash = table_name_to_schema_type(base_name, check_col, takes_array).elts 316 | pp1 = param.params[1] 317 | case pp1 318 | when RDL::Type::NominalType 319 | ## just one table joined to base table 320 | joined_name = pp1.klass.to_s.singularize.to_sym 321 | joined_type = RDL::Type::OptionalType.new(table_name_to_schema_type(joined_name, check_col, takes_array)) 322 | type_hash = type_hash.merge({ joined_name.to_s.pluralize.underscore.to_sym => joined_type }) 323 | when RDL::Type::UnionType 324 | ## multiple tables joined to base table 325 | joined_hash = RDL.type_cast(Hash[pp1.types.map { |t| 326 | joined_name = RDL.type_cast(t, "RDL::Type::NominalType", force: true).klass.to_s.singularize.to_sym 327 | joined_type = table_name_to_schema_type(joined_name, check_col, takes_array) 328 | [joined_name.to_s.pluralize.underscore.to_sym, joined_type] 329 | } 330 | ], "Hash", force: true) 331 | else 332 | raise "unexpected type #{trec}" 333 | end 334 | return RDL::Type::FiniteHashType.new(type_hash, nil) 335 | when RDL::Type::NominalType 336 | tname = param.klass.to_s.to_sym 337 | return table_name_to_schema_type(tname, check_col, takes_array) 338 | else 339 | raise RDL::Typecheck::StaticTypeError, "Unexpected type parameter in #{trec}." 340 | end 341 | when RDL::Type::SingletonType 342 | val = RDL.type_cast(trec.val, 'Class', force: true) 343 | raise RDL::Typecheck::StaticTypeError, "Unexpected receiver type #{trec}." unless val.is_a?(Class) 344 | tname = val.to_s.to_sym 345 | return table_name_to_schema_type(tname, check_col, takes_array) 346 | when RDL::Type::NominalType 347 | tname = trec.name.to_sym 348 | return table_name_to_schema_type(tname, check_col, takes_array) 349 | else 350 | raise RDL::Typecheck::StaticTypeError, "Unexpected receiver type #{trec}." 351 | end 352 | end 353 | RDL.type DBType, 'self.rec_to_schema_type', "(RDL::Type::Type, %bool, ?%bool) -> RDL::Type::FiniteHashType", wrap: false, typecheck: :type_code, effect: [:+, :+] 354 | 355 | ## turns a given table name into the appropriate finite hash type based on table schema, with optional or top-type values 356 | ## [+ tname +] is the table name as a symbol 357 | ## [+ check_col +] is a boolean indicating whether or not column types will eventually be checked 358 | def self.table_name_to_schema_type(tname, check_col, takes_array=false) 359 | #h = RDL.type_cast({}, "Hash<%any, RDL::Type::Type>", force: true) 360 | ttype = RDL::Globals.ar_db_schema[tname] 361 | raise RDL::Typecheck::StaticTypeError, "No table type for #{tname} found." unless ttype 362 | tschema = RDL.type_cast(ttype.params[0], "RDL::Type::FiniteHashType", force: true).elts.except(:__associations) 363 | h = Hash[tschema.map { |k, v| 364 | if check_col 365 | v = RDL::Type::UnionType.new(v, RDL::Type::GenericType.new(RDL::Globals.types[:array], v)) if takes_array 366 | [k, RDL::Type::OptionalType.new(v)] 367 | else 368 | [k, RDL::Type::OptionalType.new(RDL::Globals.types[:top])] 369 | end 370 | }] 371 | RDL::Type::FiniteHashType.new(RDL.type_cast(h, "Hash<%any, RDL::Type::Type>", force: true), nil) 372 | end 373 | RDL.type DBType, 'self.table_name_to_schema_type', "(Symbol, %bool, ?%bool) -> RDL::Type::FiniteHashType", wrap: false, typecheck: :type_code, effect: [:+, :+] 374 | 375 | def self.where_input_type(trec, targs) 376 | handle_sql_strings(trec, targs) if targs[0].is_a? RDL::Type::PreciseStringType 377 | tschema = rec_to_schema_type(trec, true, true) 378 | return RDL::Type::UnionType.new(tschema, RDL::Globals.types[:string], RDL::Globals.types[:array]) ## no indepth checking for string or array cases 379 | end 380 | RDL.type Object, 'self.handle_sql_strings', "(RDL::Type::Type, Array) -> %any", wrap: false, effect: [:+, :+] 381 | RDL.type DBType, 'self.where_input_type', "(RDL::Type::Type, Array) -> RDL::Type::UnionType", wrap: false, typecheck: :type_code, effect: [:+, :+] 382 | 383 | def self.find_input_type(trec, targs) 384 | handle_sql_strings(trec, targs) if targs[0].is_a? RDL::Type::PreciseStringType 385 | rec_to_schema_type(trec, true) 386 | end 387 | 388 | def self.where_noarg_output_type(trec) 389 | case trec 390 | when RDL::Type::SingletonType 391 | ## where called directly on class 392 | RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord::QueryMethods::WhereChain), rec_to_nominal(trec)) 393 | when RDL::Type::GenericType 394 | ## where called on ActiveRecord_Relation 395 | raise RDL::Typecheck::StaticTypeError, "Unexpected receiver type #{trec}." unless trec.base.klass == ActiveRecord_Relation 396 | return RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord::QueryMethods::WhereChain), trec.params[0]) 397 | else 398 | raise RDL::Typecheck::StaticTypeError, "Unexpected receiver type #{trec}." 399 | end 400 | end 401 | RDL.type DBType, 'self.where_noarg_output_type', "(RDL::Type::Type) -> RDL::Type::GenericType", wrap: false, typecheck: :type_code, effect: [:+, :+] 402 | 403 | def self.not_input_type(trec, targs) 404 | tschema = rec_to_schema_type(trec, true) 405 | return RDL::Type::UnionType.new(tschema, RDL::Globals.types[:string], RDL::Globals.types[:array]) ## no indepth checking for string or array cases 406 | end 407 | RDL.type DBType, 'self.not_input_type', "(RDL::Type::Type, Array) -> RDL::Type::UnionType", wrap: false, typecheck: :type_code, effect: [:+, :+] 408 | 409 | def self.exists_input_type(trec, targs) 410 | raise "Unexpected number of arguments to ActiveRecord::Base#exists?." unless targs.size <= 1 411 | case targs[0] 412 | when RDL::Type::FiniteHashType 413 | typ = rec_to_schema_type(trec, false) 414 | else 415 | ## any type can be accepted, only thing we're intersted in is when a hash is given 416 | ## TODO: what if we get a nominal Hash type? 417 | typ = targs[0] 418 | end 419 | return RDL::Type::OptionalType.new(RDL::Type::UnionType.new(RDL::Globals.types[:integer], RDL::Globals.types[:string], typ)) 420 | end 421 | RDL.type DBType, 'self.exists_input_type', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 422 | 423 | 424 | def self.find_output_type(trec, targs) 425 | case targs.size 426 | when 0 427 | raise RDL::Typecheck::StaticTypeError, "No arguments given to ActiveRecord::Base#find." 428 | when 1 429 | arg0 = targs[0] 430 | case arg0 431 | when RDL::Globals.types[:integer], RDL::Globals.types[:string] 432 | DBType.rec_to_nominal(trec) 433 | when RDL::Type::SingletonType 434 | # expecting symbol or integer here 435 | case arg0.val 436 | when Integer 437 | DBType.rec_to_nominal(trec) 438 | when Symbol 439 | ## TODO 440 | ## Actually, this is deprecated in later versions 441 | raise RDL::Typecheck::StaticTypeError, "Unexpected arg type #{arg0} in call to ActiveRecord::Base#find." 442 | else 443 | raise RDL::Typecheck::StaticTypeError, "Unexpected arg type #{arg0} in call to ActiveRecord::Base#find." 444 | end 445 | when RDL::Type::GenericType 446 | RDL::Type::GenericType.new(RDL::Globals.types[:array], DBType.rec_to_nominal(trec)) 447 | when RDL::Type::TupleType 448 | RDL::Type::GenericType.new(RDL::Globals.types[:array], DBType.rec_to_nominal(trec)) 449 | else 450 | raise RDL::Typecheck::StaticTypeError, "Unexpected arg type #{arg0} in call to ActiveRecord::Base#find." 451 | end 452 | else 453 | DBType.rec_to_nominal(trec) 454 | end 455 | end 456 | RDL.type DBType, 'self.find_output_type', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 457 | 458 | def self.joins_one_input_type(trec, targs) 459 | return RDL::Globals.types[:top] unless targs.size == 1 ## trivial case, won't be matched 460 | case trec 461 | when RDL::Type::SingletonType 462 | base_klass = RDL.type_cast(trec, "RDL::Type::SingletonType").val 463 | when RDL::Type::GenericType 464 | raise "Unexpected type #{trec}." unless (RDL.type_cast(trec, "RDL::Type::GenericType").base.klass == ActiveRecord_Relation) 465 | param = RDL.type_cast(trec, "RDL::Type::GenericType").params[0] 466 | case param 467 | when RDL::Type::GenericType 468 | raise "Unexpected type #{trec}." unless (param.base.klass == JoinTable) 469 | base_klass = RDL.type_cast(param.params[0], "RDL::Type::NominalType", force: true).klass 470 | when RDL::Type::NominalType 471 | base_klass = param.klass 472 | else 473 | raise "unexpected parameter type in #{trec}" 474 | end 475 | else 476 | raise "unexpected receiver type #{trec}" 477 | end 478 | arg0 = targs[0] 479 | case arg0 480 | when RDL::Type::SingletonType 481 | sym = RDL.type_cast(arg0, "RDL::Type::SingletonType").val 482 | raise RDL::Typecheck::StaticTypeError, "Unexpected arg type #{trec} in call to joins." unless sym.is_a?(Symbol) 483 | raise RDL::Typecheck::StaticTypeError, "#{trec} has no association to #{arg0}, cannot perform joins." unless associated_with?(RDL.type_cast(base_klass, "Symbol", force: true), sym) 484 | return arg0 485 | when RDL::Type::FiniteHashType 486 | RDL.type_cast(RDL.type_cast(arg0, "RDL::Type::FiniteHashType").elts, "Hash", force: true).each { |key, val| 487 | raise RDL::Typecheck::StaticTypeError, "Unexpected hash arg type #{arg0} in call to joins." unless key.is_a?(Symbol) && val.is_a?(RDL::Type::SingletonType) && RDL.type_cast(val, "RDL::Type::SingletonType").val.is_a?(Symbol) 488 | val_sym = RDL.type_cast(val, "RDL::Type::SingletonType").val 489 | raise RDL::Typecheck::StaticTypeError, "#{trec} has no association to #{key}, cannot perform joins." unless associated_with?(RDL.type_cast(base_klass, "Symbol", force: true), key) 490 | key_klass = key.to_s.singularize.camelize 491 | raise RDL::Typecheck::StaticTypeError, "#{key} has no association to #{val_sym}, cannot perform joins." unless associated_with?(key_klass, val_sym) 492 | } 493 | return arg0 494 | else 495 | raise RDL::Typecheck::StaticTypeError, "Unexpected arg type #{arg0} in call to joins." 496 | end 497 | end 498 | RDL.type DBType, 'self.joins_one_input_type', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 499 | 500 | def self.joins_multi_input_type(trec, targs) 501 | return RDL::Globals.types[:top] unless targs.size > 1 ## trivial case, won't be matched 502 | targs.each { |arg| 503 | joins_one_input_type(trec, [arg]) 504 | } 505 | return targs[0] ## since this method is called as first argument in type 506 | end 507 | RDL.type DBType, 'self.joins_multi_input_type', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 508 | 509 | def self.associated_with?(rec, sym) 510 | tschema = RDL::Globals.ar_db_schema[rec.to_s.to_sym] 511 | raise RDL::Typecheck::StaticTypeError, "No table type for #{rec} found." unless tschema 512 | schema = RDL.type_cast(tschema.params[0], "RDL::Type::FiniteHashType", force: true).elts 513 | assoc = schema[:__associations] 514 | raise RDL::Typecheck::StaticTypeError, "Table #{rec} has no associations, cannot perform joins." unless assoc 515 | RDL.type_cast(RDL.type_cast(assoc, "RDL::Type::FiniteHashType").elts, "Hash", force: true).each { |key, value| 516 | case value 517 | when RDL::Type::SingletonType 518 | return true if RDL.type_cast(value.val, "Object", force: true) == sym ## no need to change any plurality here 519 | when RDL::Type::UnionType 520 | ## for when rec has multiple of the same kind of association 521 | value.types.each { |t| 522 | raise "Unexpected type #{t}." unless t.is_a?(RDL::Type::SingletonType) && (RDL.type_cast(t, "RDL::Type::SingletonType").val.class == Symbol) 523 | return true if RDL.type_cast(t, "RDL::Type::SingletonType").val == sym 524 | } 525 | else 526 | raise RDL::Typecheck::StaticTypeError, "Unexpected association type #{value}" 527 | end 528 | } 529 | return false 530 | end 531 | RDL.type DBType, 'self.associated_with?', "(Class or Symbol or String, Symbol) -> %bool", wrap: false, typecheck: :type_code, effect: [:+, :+] 532 | 533 | def self.get_joined_args(targs) 534 | arg_types = RDL.type_cast([], "Array", force: true) 535 | targs.each { |arg| 536 | case arg 537 | when RDL::Type::SingletonType 538 | raise RDL::Typecheck::StaticTypeError, "Unexpected joins arg type #{arg}" unless (RDL.type_cast(arg.val, "Object", force: true).class == Symbol) 539 | arg_types = arg_types + [RDL::Type::NominalType.new(RDL.type_cast(arg.val, "Symbol", force: true).to_s.singularize.camelize)] 540 | when RDL::Type::FiniteHashType 541 | hsh = arg.elts 542 | raise 'not supported' unless hsh.size == 1 543 | key, val = RDL.type_cast(hsh.first, "[Symbol, RDL::Type::SingletonType]", force: true) 544 | val = val.val 545 | arg_types = arg_types + [RDL::Type::UnionType.new(RDL::Type::NominalType.new(key.to_s.singularize.camelize), RDL::Type::NominalType.new(val.to_s.singularize.camelize))] 546 | else 547 | raise "Unexpected arg type #{arg} to joins." 548 | end 549 | } 550 | if arg_types.size > 1 551 | return RDL::Type::UnionType.new(*arg_types) 552 | elsif arg_types.size == 1 553 | return arg_types[0] 554 | else 555 | raise "oops, didn't expect to get here." 556 | end 557 | end 558 | RDL.type DBType, 'self.get_joined_args', "(Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 559 | 560 | def self.joins_output(trec, targs) 561 | arg_type = get_joined_args(targs) 562 | case trec 563 | when RDL::Type::SingletonType 564 | joined = arg_type 565 | when RDL::Type::GenericType 566 | raise "Unexpected type #{trec}." unless (trec.base.klass == ActiveRecord_Relation) 567 | param = trec.params[0] 568 | case param 569 | when RDL::Type::GenericType 570 | raise "Unexpected type #{trec}." unless (param.base.klass == JoinTable) 571 | joined = RDL::Type::UnionType.new(param.params[1], arg_type) 572 | when RDL::Type::NominalType 573 | joined = arg_type 574 | else 575 | raise "unexpected parameter type in #{trec}" 576 | end 577 | else 578 | raise "unexpected type #{trec}" 579 | end 580 | jt = RDL::Type::GenericType.new(RDL::Type::NominalType.new(JoinTable), rec_to_nominal(trec), joined) 581 | ret = RDL::Type::GenericType.new(RDL::Type::NominalType.new(ActiveRecord_Relation), jt) 582 | return ret 583 | end 584 | RDL.type DBType, 'self.joins_output', "(RDL::Type::Type, Array) -> RDL::Type::Type", wrap: false, typecheck: :type_code, effect: [:+, :+] 585 | 586 | def self.plus_output_type(trec, targs) 587 | typs = RDL.type_cast([], "Array", force: true) 588 | [trec, targs[0]].each { |t| 589 | case t 590 | when RDL::Type::GenericType 591 | raise "Expected ActiveRecord_Relation." unless t.base.name == "ActiveRecord_Relation" 592 | param0 = t.params[0] 593 | case param0 594 | when RDL::Type::GenericType 595 | raise "Unexpected paramter type in #{t}." unless param0.base.name == "JoinTable" 596 | typs = typs + [param0.params[0]] ## base of join table 597 | typs = typs + [param0.params[1]] ## joined tables 598 | when RDL::Type::NominalType 599 | typs = typs + [param0] 600 | else 601 | raise "unexpected paramater type in #{t}" 602 | end 603 | else 604 | raise "unexpected type #{t}" 605 | end 606 | } 607 | RDL::Type::GenericType.new(RDL::Type::NominalType.new(Array), RDL::Type::UnionType.new(*typs)) 608 | end 609 | RDL.type DBType, 'self.plus_output_type', "(RDL::Type::Type, Array) -> RDL::Type::GenericType", wrap: false, typecheck: :type_code, effect: [:+, :+] 610 | 611 | def self.count_input(trec, targs) 612 | hash_type = rec_to_schema_type(trec, true)## Bug found here. orginally had: rec_to_schema_type(trec, targs).elts 613 | typs = RDL.type_cast([], "Array", force: true) 614 | hash_type.elts.each { |k, v| ## bug here, originally had: hash_type.each { |k, v| 615 | if v.is_a?(RDL::Type::FiniteHashType) 616 | ## will reach this with joined tables, but we're only interested in column names 617 | RDL.type_cast(v, 'RDL::Type::FiniteHashType', force: true).elts.each { |k1, v1| 618 | typs = typs + [RDL::Type::SingletonType.new(k1)] unless v1.is_a?(RDL::Type::FiniteHashType) ## potentially two dimensions in joined table 619 | } 620 | else 621 | typs = typs + [RDL::Type::SingletonType.new(k)] 622 | end 623 | } 624 | return RDL::Type::OptionalType.new(RDL::Type::UnionType.new(*typs)) 625 | end 626 | RDL.type DBType, 'self.count_input', "(RDL::Type::Type, Array) -> RDL::Type::OptionalType", wrap: false, typecheck: :type_code, effect: [:+, :+] 627 | 628 | end 629 | --------------------------------------------------------------------------------