require 'rake' require 'rake/tasklib' require 'jerbil/java_helper' require 'jerbil/inflector' require 'yaml' require 'set' module Jerbil module Hibernate # Generates a SQL schema from EJB3/Hibernate-annotated classes like Hibernate's # SchemaExport tool. Typically Jerbil::AptTask is used to compile source files and # gather a list of entities which then gets serialized to a YAML file. # ExportSchemaTask then reads this file and uses Hibernate's schema exporting # features to generate SQL. Optionally the schema can be validated, to check # whether any column or table names are reserved keywords (validate :sql) or # conform to ActiveRecord convention (validate :rails). # # == Example # Jerbil::Hibernate::ExportSchemaTask.new(:export_schema) do |t| # t.schemafile = "schema.sql" # t.entities_yml = ENTITIES_YML # t.validate = :all # end class ExportSchemaTask < Rake::TaskLib include JavaHelper attr_accessor :name # SQL schema destination file (default: schema.sql) attr_accessor :schemafile # A file containing a list of entities (javax.persistence.Entity), # serialized as a list of strings (YAML format). attr_accessor :entities_yml # a file containing a map of properties, property names are keys in the map, and # property values are values in the map attr_accessor :properties_yml # Classname implementing the db dialect, defaults to # org.hibernate.dialect.MySQL5Dialect attr_accessor :dialect # SQL statements to be executed before generated sql. attr_accessor :preamble attr_accessor :epilogue # FQN of a package containing package-info.java to be used by # hibernate. attr_accessor :package # Pretty printing of generated SQL (default: true) attr_accessor :prettyprint # name of a java system property to set when exporting schema attr_accessor :system_property_name # value of java system property to set when exporting schema attr_accessor :system_property_value def initialize(name=:export_schema) @name = name @dependencies = [] @classfilter = nil @prettyprint = true @validate = [] @schemafile = "schema.sql" @entities_yml = "entities.yml" @properties_yml = nil @dialect = "org.hibernate.dialect.MySQL5Dialect" @sql_reserved = nil @system_property_name = nil @system_property_value = nil @ddlfilter = nil yield self if block_given? define end def define # :nodoc: task name => dependencies do |t| with_system_property( @system_property_name , @system_property_value ) do entities = File.open(entities_yml) {|f| YAML.load(f)} raise 'no annotated entities found!' if entities.empty? #puts "found #{entities.size} entities" entities = entities.dup.select { |e| @classfilter.call(e) } if @classfilter entity_classes = entities.map {|klass| Rjb::import(klass)} properties = (YAML.load_file(@properties_yml) if @properties_yml) || {} cfg = get_config(entity_classes, properties, package) validate_config(cfg) unless validate.empty? sql = cfg.generateSchemaCreationScript(Rjb::import(dialect).new) schema = "# -- do not edit ---\n" schema << "# generated by Jerbil::Hibernate::ExportSchemaTask at #{Time.new}\n\n" schema << preamble << "\n" if preamble sql.each do |s| s = format(s) if prettyprint schema << "#{s};" end schema << "\n\n#{epilogue}" if epilogue schema = @ddlfilter.call( schema ) if @ddlfilter File.open(schemafile, "w") {|file| file << schema } end end file schemafile => name task name => entities_yml end # Filters all entities. Useful to only export schema for a subset of classes. # ====Example # Jerbil::Hibernate::ExportSchemaTask.new(:export_schema) do |t| # t.filter { |classname| classname =~ /^foo/ } # end def filter(*args, &block) @classfilter = block end # filters output sql using the provided block, which takes a single parameter def ddlfilter(*args, &block) @ddlfilter = block end # Validate configuration (options: :all, :sql, :rails) def validate(*what) @validate.concat(what) end # Exposes the inflections instance so exceptions can be registered. def inflections yield Inflector::Inflections.instance if block_given? end protected def get_config(classes, properties, package=nil) anncfg = Rjb::import('org.hibernate.cfg.AnnotationConfiguration') acfg = anncfg.new packages = Set.new classes.each do |clazz| #puts "adding " + clazz.class.to_s acfg.addAnnotatedClass(clazz) pkg = clazz.getPackage packages << pkg.getName if pkg && pkg.getAnnotations.length > 0 end packages << package if package packages.each { |pkg| acfg.addPackage(pkg) } properties.each do |key,value| acfg.setProperty(key, value) end acfg end def format(sql) Rjb::import('org.hibernate.pretty.DDLFormatter').new(sql).format end def reserved_words if @sql_reserved.nil? @sql_reserved = {} Dir[File.join(File.dirname(__FILE__), "..", "..", "sql_reserved_words", "**")].each do |file| @sql_reserved[File.basename(file)] = File.readlines(file).map{|s|s.chomp} end end @sql_reserved end def check_reserved_word(word) offending_dialects = [] reserved_words.each do |dialect, wordlist| offending_dialects << dialect.to_s if wordlist.include?(word.upcase) end offending_dialects end def validate?(what) @validate.include?(:all) || @validate.include?(what) end def validate_config(cfg) cfg.buildMappings class_mappings = cfg.getClassMappings invalid_tables = [] invalid_columns = [] invalid_words = [] while class_mappings.hasNext cmap = class_mappings.next simple_name = cmap.getMappedClass.getSimpleName table_name = cmap.getTable.getName #only check table semantics for toplevel classes if cmap.getRootClass.equals(cmap) if validate?(:sql) dialect_probs = check_reserved_word(table_name) unless dialect_probs.empty? $stderr << "[#{simple_name}] table name '#{table_name}' is a reserved keyword in dialects: #{dialect_probs.join(', ')}\n" if verbose invalid_words << table_name end end if validate?(:rails) expected_table_name = Inflector::tableize(simple_name).sub(/^hibernate_/, '') if expected_table_name != table_name && check_reserved_word(expected_table_name).empty? $stderr << "[#{simple_name}] invalid table: '#{table_name}', should be '#{expected_table_name}'\n" if verbose invalid_tables << table_name end end end # column checks property_it = cmap.getPropertyIterator while property_it.hasNext prop = property_it.next prop_name = prop.getName column_it = prop.getColumnIterator next unless column_it.hasNext col_name = column_it.next.getName if validate?(:sql) dialect_probs = check_reserved_word(col_name) unless dialect_probs.empty? $stderr << "[#{simple_name}] column name '#{col_name}' in '#{table_name}' is a reserved keyword in dialects: #{dialect_probs.join(', ')}\n" if verbose invalid_words << col_name end end #ignore association types for now (TODO) next if prop.getType.isAssociationType if validate?(:rails) expected_col_name = Inflector::underscore(prop_name) if expected_col_name != col_name && check_reserved_word(expected_col_name).empty? $stderr << "[#{simple_name}] invalid column: '#{table_name}.#{col_name}', should be '#{expected_col_name}'\n" if verbose invalid_columns << col_name end end end end raise "ExportSchemaTask: validation errors, not exporting" if !invalid_tables.empty? || !invalid_columns.empty? || !invalid_words.empty? end #validate config end #export schema task end # Hibernate end