Best JVM/CLR Source-compatable Language

I asked a liquibase-related question on stackoverflow to get answers from a larger user base:
http://stackoverflow.com/questions/812474/best-jvm-clr-source-compatable-language.

If you have any expertise in the area, please help with the answers (here for liquibase-specific comments, there for general answers).

Nathan

Nathan,

How serious are you about converting Liquibase to a new language?
As a Liquibase user, this raises some concern with me:

  • Does the Liquibase developer community have the bandwidth to do this? The number of open bugs and enhancement requests would suggest that this is questionable.
  • It will be much harder to find new developers that are conversant with a new language such as Fan
  • Any language that bridges Java and CLR will have to deal with the limitations of both. I would fear that a significant amount of development time would go into resolving incompatibilities instead of into expanding the functionality.
  • A choice for a language such as Fan would limit future functionality. Take the recent request for built-in scripting as an example. Doable in Java, but it would be pretty hard in Fan.

I am not aware of any successful software that has been implemented in a JVM/CLR source compatible language.
All in all, I would strongly recommend to do one thing and do it well.

My 2 cts.

I understand your concerns.  Here are my current thoughts:

I agree that there is no way that I can maintain a .net version in addition to the java version (I found that out when creating the IDE plugins).  At the same time, I have had interest from several people in a .net port and feel like there would be a lot of interest from them.

There are a bunch of options, and I am currently trying to figure out what will be best.  My approach so far has been “wait and see if anyone takes the .net port and starts it” approach.  While there has been at least two people that said they were starting the project, neither one produced any code that I saw.  I am suspicious that it is because starting the project and getting to an initial release is such a daunting project. Perhaps if I can get a beta or even 1.0 version out, I would be able to find a maintainer that could take it over from me.  There are a few developers I know who have expressed interest in such a setup.

If I went that route, my initial thought was to use a java->C# converter to do a rough start of the project, then hand-fix it up to be a “native” .net app.  I haven’t tried the converter approach before, so how well that would work would be very dependent on how well the converter works.  There is also the problem of the 1.9 release code needing a good refactoring internally.  I would not want to port the existing code structure to C# and wish it was architected differently.  Getting the code to the point that I or someone else could do a straight conversion to c# and be happy with the resulting codebase is one of the reasons behind the refactoring we are currently doing, so nothing will go forward on the .net side until that project is done.

Where Fan (or similar) would come in, is that I was noticing that a lot of the “core” logic is very simple classes (all simple data types and collections), but captures the cross-database wierdness that is the root of many of the issues people find.  If we were to be able to start a .net community, it would be nice to be able to share the work that we all do in creating refactoring logic and database support in order to maximize what we can do with limited development resources.  That is where I got to thinking that re-writing the *Statement and *Change classes into a language that compiles to both platforms would allow us to share codebases to some degree and therefore share work.  All the of the other classes (xml parsing, command line/servlet/etc., scripting language, etc) would be written in the mainstream languages to take full advantage of the platform.

I was hoping for something more mainstream like Python or Ruby, but further investigation has shown that neither one would work as well as I hoped.  I had not remembered Fan, but from what I can tell initially it appears to be the best option as it is specifically designed to work on both platforms.  Whether it would really work or not, I am not sure.  It partly depends on how well it integrates with “regular” java and C# code.  It has an external dependency on a fan.jar file which I really did not want.

So currently the interest in Fan and .net is purely theoretical.  I am keeping it in the back of my mind while refactoring the existing code, although the primary purpose for the refactoring is to lower the bar to entry into our codebase and create a plugin-type system so others can extend liquibase without touching our code directly.  This will give developers an easy road from creating 3rd party one-off extensions to contributing bug fixes in the core to becoming full committers in hopes of increasing our bandwidth so I can tackle things like a .net port and IDE support. 

Nathan

By “Python” and “Ruby” I assume you’re referring to JPython, JRuby and their ferrous metal counterparts.  Just curious as to why you found they wouldn’t work as well as you hoped, and does Fan look like it will?

What would be the gain in having a .NET port?  Apart from getting the right JDBC drivers and plonking them in the lib folder, and figuring out the JDBC URL (all of which can be well documented), you don’t need to know java to use LiquiBase.  What will a .NET port give you?

Originally posted by: catflap
By "Python" and "Ruby" I assume you're referring to JPython, JRuby and their ferrous metal counterparts.  Just curious as to why you found they wouldn't work as well as you hoped, and does Fan look like it will?

Yes, that is what I meant. My concerns with python is that the java compiler seems to no longer be supported and the general runtime does not appear to be actively maintained. With ruby, the java version is good, but ironruby seems very immature still.  Both appear to need an execution environment set up as well, which I was hoping to avoid.

Fan appears to require an execution environment as well, but does seem. To be equally as supported in both platforms. I probably experiment at some point with fan, ruby, and python, but if I had to guess, I don’t think it will meet our needs either.

For now, I am keeping it at the “is it even possible?” stage. 

Nathan 

Originally posted by: catflap
What would be the gain in having a .NET port?  Apart from getting the right JDBC drivers and plonking them in the lib folder, and figuring out the JDBC URL (all of which can be well documented), you don't need to know java to use LiquiBase.  What will a .NET port give you?

I think the main thing they would get is better integration with their environment.  Liquibase could run automatically on startup (like our servlet listener, spring bean, etc), could integrate with nant, and would require one less dependency in their system (java). 

There is also the point that there are many .net developers that would not consider a java based solution and/or would not be able to contribute back to t.

You are right that they would not need it (and apparently liqubase runs fine under ikvm) but I am looking for ways to expand liquibase’s reach, influence, and value and it is an important direction to consider. 

It does come down to the amour of time it will take to create and maintain, which I am looking to minimize. There is also the IDE that would be a great, huge project to tackle so I am weighing my options and priorities.

Nathan 

There is also the point that there are many .net developers that would not consider a java based solution

I would accuse them of being parochial except that I’d be the kettle and they’d be the pot if LiquiBase was already a .net based application. :slight_smile:
(Mind you, that’s mainly because they tend not to work very well if at all under Linux/Mono.)

I could also see it a challenge to interface with whatever is the .net equivalent of JDBC.  Potential for a lot of duplication of code to get around the quirks of each interface.

I would seriously take another hard look at (J|Iron)(Python|Ruby) before opting for something more esoteric and less well known such as Fan.  You could end up narrowing the opportunities to contribute back instead of opening them.

That is a concern I have with Fan (being too esoteric) as well.  Also, the smaller community around it concerns me as well because it would be more likely to die at some point. 

For reference, below is the type of class (pre-cleanup) that would be a candidate for conversion.  As you can see, there is a lot of logic around the sql to run for a particular database and based on the parameters passed in, but in the end it just outputs a string.  Once you get beyond code like this, each platform is different (jdbc vs ado.net etc.) and I think it makes sense to do a fork/copy/conversion of the java codebase to C# for all that.  My hope is that it would be isolated and straightforward enough that it would be fine to expect people to code it in a non-standard language (python, ruby, fan) and it would feel more like a dsl than something tacked on.  If the goal is to increase the chances of getting patches, the last thing I want to do is come up with code that scares people away.


public class CreateTableGenerator implements SqlGenerator {
    public int getSpecializationLevel() {
        return SPECIALIZATION_LEVEL_DEFAULT;
    }

    public boolean isValidGenerator(CreateTableStatement statement, Database database) {
        return true;
    }

    public GeneratorValidationErrors validate(CreateTableStatement createTableStatement, Database database) {
        return new GeneratorValidationErrors();
    }

    public Sql[] generateSql(CreateTableStatement statement, Database database) {
            StringBuffer buffer = new StringBuffer();
        buffer.append(“CREATE TABLE “).append(database.escapeTableName(statement.getSchemaName(), statement.getTableName())).append(” “);
        buffer.append(”(”);
        Iterator columnIterator = statement.getColumns().iterator();
        while (columnIterator.hasNext()) {
            String column = columnIterator.next();
            boolean isAutoIncrement = statement.getAutoIncrementColumns().contains(column);

            buffer.append(database.escapeColumnName(statement.getSchemaName(), statement.getTableName(), column));
            buffer.append(" ").append(database.getColumnType(statement.getColumnTypes().get(column), isAutoIncrement));

            if ((database instanceof SQLiteDatabase) &&
(statement.getPrimaryKeyConstraint()!=null) &&
(statement.getPrimaryKeyConstraint().getColumns().size()==1) &&
(statement.getPrimaryKeyConstraint().getColumns().contains(column)) &&
isAutoIncrement) {
            String pkName = StringUtils.trimToNull(statement.getPrimaryKeyConstraint().getConstraintName());
            if (pkName == null) {
                pkName = database.generatePrimaryKeyName(statement.getTableName());
            }
            buffer.append(" CONSTRAINT “);
            buffer.append(database.escapeConstraintName(pkName));
buffer.append(” PRIMARY KEY AUTOINCREMENT");
}

            if (statement.getDefaultValue(column) != null) {
                if (database instanceof MSSQLDatabase) {
                    buffer.append(" CONSTRAINT “).append(((MSSQLDatabase) database).generateDefaultConstraintName(statement.getTableName(), column));
                }
                buffer.append(” DEFAULT ");
                buffer.append(statement.getDefaultValue(column));
            }

            if (isAutoIncrement &&
(database.getAutoIncrementClause()!=null) &&
(!database.getAutoIncrementClause().equals(""))) {
                if (database.supportsAutoIncrement()) {
                    buffer.append(" “).append(database.getAutoIncrementClause()).append(” “);
                } else {
                    LogFactory.getLogger().log(Level.WARNING, database.getProductName()+” does not support autoincrement columns as request for "+(database.escapeTableName(statement.getSchemaName(), statement.getTableName())));
                }
            }

            if (statement.getNotNullColumns().contains(column)) {
                buffer.append(" NOT NULL");
            } else {
                if (database instanceof SybaseDatabase || database instanceof SybaseASADatabase) {
                    buffer.append(" NULL");
                }
            }

            if ((database instanceof InformixDatabase) &&
(statement.getPrimaryKeyConstraint()!=null) &&
(statement.getPrimaryKeyConstraint().getColumns().size()==1) &&
(statement.getPrimaryKeyConstraint().getColumns().contains(column))) {
            buffer.append(" PRIMARY KEY");
            }

            if (columnIterator.hasNext()) {
                buffer.append(", ");
            }
        }

        buffer.append(",");

        // TODO informixdb
        if (!( (database instanceof SQLiteDatabase) &&
(statement.getPrimaryKeyConstraint()!=null) &&
(statement.getPrimaryKeyConstraint().getColumns().size()==1) &&
statement.getAutoIncrementColumns().contains(statement.getPrimaryKeyConstraint().getColumns().get(0)) ) &&

			!((database instanceof InformixDatabase) &&
			(statement.getPrimaryKeyConstraint()!=null) &&
			(statement.getPrimaryKeyConstraint().getColumns().size()==1)
			)) {

        // …skip this code block for sqlite if a single column primary key
        // with an autoincrement constraint exists.
        // This constraint is added after the column type.

        if (statement.getPrimaryKeyConstraint() != null && statement.getPrimaryKeyConstraint().getColumns().size() > 0) {
        	if (!(database instanceof InformixDatabase)) {
	            String pkName = StringUtils.trimToNull(statement.getPrimaryKeyConstraint().getConstraintName());
	            if (pkName == null) {
	                pkName = database.generatePrimaryKeyName(statement.getTableName());
	            }
	            buffer.append(" CONSTRAINT ");
	            buffer.append(database.escapeConstraintName(pkName));
        	}
            buffer.append(" PRIMARY KEY (");
            buffer.append(database.escapeColumnNameList(StringUtils.join(statement.getPrimaryKeyConstraint().getColumns(), ", ")));
            buffer.append(")");
            buffer.append(",");
        }

        }

        for (ForeignKeyConstraint fkConstraint : statement.getForeignKeyConstraints()) {
        if (!(database instanceof InformixDatabase)) {
        buffer.append(" CONSTRAINT “);
                buffer.append(database.escapeConstraintName(fkConstraint.getForeignKeyName()));
        }
            buffer.append(” FOREIGN KEY (")
                    .append(database.escapeColumnName(statement.getSchemaName(), statement.getTableName(), fkConstraint.getColumn()))
                    .append(") REFERENCES ")
                    .append(fkConstraint.getReferences());

            if (fkConstraint.isDeleteCascade()) {
                buffer.append(" ON DELETE CASCADE");
            }

            if ((database instanceof InformixDatabase)) {
            buffer.append(" CONSTRAINT ");
            buffer.append(database.escapeConstraintName(fkConstraint.getForeignKeyName()));
            }

            if (fkConstraint.isInitiallyDeferred()) {
                buffer.append(" INITIALLY DEFERRED");
            }
            if (fkConstraint.isDeferrable()) {
                buffer.append(" DEFERRABLE");
            }
            buffer.append(",");
        }

        for (UniqueConstraint uniqueConstraint : statement.getUniqueConstraints()) {
            if (uniqueConstraint.getConstraintName() != null && !constraintNameAfterUnique(database)) {
                buffer.append(" CONSTRAINT “);
                buffer.append(database.escapeConstraintName(uniqueConstraint.getConstraintName()));
            }
            buffer.append(” UNIQUE (");
            buffer.append(database.escapeColumnNameList(StringUtils.join(uniqueConstraint.getColumns(), “, “)));
            buffer.append(”)”);
            if (uniqueConstraint.getConstraintName() != null && constraintNameAfterUnique(database)) {
                buffer.append(" CONSTRAINT “);
                buffer.append(database.escapeConstraintName(uniqueConstraint.getConstraintName()));
            }
            buffer.append(”,");
        }

//        if (constraints != null && constraints.getCheck() != null) {
//            buffer.append(constraints.getCheck()).append(" ");
//        }
//    }

        String sql = buffer.toString().replaceFirst(",\s*$", “”) + “)”;

//        if (StringUtils.trimToNull(tablespace) != null && database.supportsTablespaces()) {
//            if (database instanceof MSSQLDatabase) {
//                buffer.append(" ON “).append(tablespace);
//            } else if (database instanceof DB2Database) {
//                buffer.append(” IN “).append(tablespace);
//            } else {
//                buffer.append(” TABLESPACE ").append(tablespace);
//            }
//        }

        if (statement.getTablespace() != null && database.supportsTablespaces()) {
            if (database instanceof MSSQLDatabase || database instanceof SybaseASADatabase) {
                sql += " ON " + statement.getTablespace();
            } else if (database instanceof DB2Database || database instanceof InformixDatabase) {
                sql += " IN " + statement.getTablespace();
            } else {
                sql += " TABLESPACE " + statement.getTablespace();
            }
        }

        return new Sql[] {
                new UnparsedSql(sql)
        };
    }

    private boolean constraintNameAfterUnique(Database database) {
return database instanceof InformixDatabase;
}

}

Nathan,

I’m in the process of integrating liquibase into a NET solution as we speak.

Truth be told there really isn’t any great hardship to requiring a JRE or embedding one in a solution other then about 30Mb of disk space. I’d rather do without it, but it’s not that big of a deal.

The availability of nicely packaged type-4, JDBC drivers has really turned out to be an important factor, much to my shock. Fully-managed NET drivers are comparably scarce and do not benefit from the jar packaging. To talk to DB2 on an AS/400 via java, I drop on jt400.jar file in the lib directory an go happily about my business. The NET counterparts often require formal installation to ensure that the driver works correctly (often because of a reliance on non-managed code).

So, even as a net developer, I would vote that you just stick with java.