/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.server.store.format.protobuf;
import com.foundationdb.ais.model.Column;
import com.foundationdb.ais.model.Group;
import com.foundationdb.ais.model.Join;
import com.foundationdb.ais.model.Table;
import com.foundationdb.ais.protobuf.CommonProtobuf.ProtobufRowFormat;
import com.google.protobuf.DescriptorProtos.DescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Label;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldOptions;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
import com.google.protobuf.DescriptorProtos.FileOptions;
import com.google.protobuf.DescriptorProtos.MessageOptions;
import com.foundationdb.server.store.format.protobuf.CustomOptions.GroupOptions;
import com.foundationdb.server.store.format.protobuf.CustomOptions.TableOptions;
import com.foundationdb.server.store.format.protobuf.CustomOptions.ColumnOptions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class AISToProtobuf
{
private ProtobufRowFormat.Type formatType;
private List<Table> tables = new ArrayList<>();
private Map<Table,String> tableMessageNames = new HashMap<>();
private FileDescriptorSet.Builder setBuilder;
private FileDescriptorSet priorSet;
private FileDescriptorProto.Builder fileBuilder;
private FileDescriptorProto priorFile;
private DescriptorProto.Builder messageBuilder;
private DescriptorProto priorMessage;
private FieldDescriptorProto.Builder fieldBuilder;
private FieldDescriptorProto priorField;
private Set<String> messageNames = new HashSet<>();
private Set<String> fieldNames = new HashSet<>();
private int nextField;
public AISToProtobuf(ProtobufRowFormat.Type formatType) {
this(formatType, null);
}
public AISToProtobuf(ProtobufRowFormat.Type formatType, FileDescriptorSet priorSet) {
this.formatType = formatType;
this.priorSet = priorSet;
setBuilder = FileDescriptorSet.newBuilder();
}
public FileDescriptorSet build() {
return setBuilder.build();
}
public void addGroup(Group group) {
tables.clear();
findTables(group.getRoot());
Collections.sort(tables, new Comparator<Table>() {
@Override
public int compare(Table t1, Table t2) {
return Integer.compare(t1.getOrdinal(),
t2.getOrdinal());
}
});
fileBuilder = setBuilder.addFileBuilder();
fileBuilder.setName(ident(group.getName().getTableName(), false) + ".proto");
fileBuilder.setPackage(ident(group.getName().getSchemaName(), false));
fileBuilder.addDependency("sql_custom_options.proto");
FileOptions.Builder fileOptions = FileOptions.newBuilder();
GroupOptions.Builder groupOptions = GroupOptions.newBuilder();
groupOptions.setName(group.getName().getTableName());
groupOptions.setSchema(group.getName().getSchemaName());
// TODO: Would it be better to use the root's version and
// arrange for that to always change on DDL?
int sumVersions = 0;
for (Table table : tables) {
// Add one so that every table contributes something.
sumVersions += table.getVersion() + 1;
}
groupOptions.setVersion(sumVersions);
priorFile = null;
if (priorSet != null) {
String rootUuid = group.getRoot().getUuid().toString();
for (FileDescriptorProto file : priorSet.getFileList()) {
DescriptorProto firstMessage = file.getMessageType(0);
MessageOptions options = firstMessage.getOptions();
if ((options != null) &&
(options.hasExtension(TableOptions.fdbsql))) {
TableOptions tableOptions = options.getExtension(TableOptions.fdbsql);
if (tableOptions.getUuid().equals(rootUuid)) {
priorFile = file;
break;
}
}
}
}
messageNames.clear();
for (Table table : tables) {
tableMessageNames.put(table, uniqueIdent(ident(table.getName().getTableName(), true), messageNames));
}
for (Table table : tables) {
addTable(table);
}
if (formatType == ProtobufRowFormat.Type.GROUP_MESSAGE) {
addGroupMessage();
}
fileOptions.setExtension(GroupOptions.fdbsql, groupOptions.build());
fileBuilder.setOptions(fileOptions);
}
protected void findTables(Table table) {
tables.add(table);
for (Join join : table.getChildJoins()) {
findTables(join.getChild());
}
}
protected void addTable(Table table) {
messageBuilder = fileBuilder.addMessageTypeBuilder();
messageBuilder.setName(tableMessageNames.get(table));
MessageOptions.Builder messageOptions = MessageOptions.newBuilder();
TableOptions.Builder tableOptions = TableOptions.newBuilder();
tableOptions.setName(table.getName().getTableName());
tableOptions.setSchema(table.getName().getSchemaName());
tableOptions.setUuid(table.getUuid().toString());
priorMessage = null;
if (priorFile != null) {
for (DescriptorProto message : priorFile.getMessageTypeList()) {
MessageOptions options = message.getOptions();
if ((options != null) &&
(options.hasExtension(TableOptions.fdbsql))) {
TableOptions toptions = options.getExtension(TableOptions.fdbsql);
if (toptions.getUuid().equals(tableOptions.getUuid())) {
priorMessage = message;
break;
}
}
}
}
nextField = 1;
if (priorMessage != null) {
TableOptions options = priorMessage.getOptions().getExtension(TableOptions.fdbsql);
if (options.hasNextField()) {
nextField = options.getNextField();
}
else {
nextField = priorMessage.getField(priorMessage.getFieldCount() - 1)
.getNumber() + 1;
}
}
fieldNames.clear();
for (Column column : table.getColumnsIncludingInternal()) {
addColumn(column);
}
for (Table child : tables) { // Continue to follow ordinal order.
if (child.getParentTable() == table) {
addChildTable(child);
}
}
if (nextField != messageBuilder.getFieldOrBuilder(messageBuilder.getFieldCount() - 1).getNumber() + 1) {
tableOptions.setNextField(nextField);
}
messageOptions.setExtension(TableOptions.fdbsql, tableOptions.build());
messageBuilder.setOptions(messageOptions);
}
protected void addColumn(Column column) {
String fieldName = uniqueIdent(ident(column.getName(), false), fieldNames);
fieldBuilder = messageBuilder.addFieldBuilder();
fieldBuilder.setName(fieldName);
fieldBuilder.setLabel(Label.LABEL_OPTIONAL);
FieldOptions.Builder fieldBuilderOptions = FieldOptions.newBuilder();
ColumnOptions.Builder columnOptions = ColumnOptions.newBuilder();
if (!fieldName.equals(column.getName())) {
columnOptions.setName(column.getName());
}
columnOptions.setSqlType(column.getTypeDescription().toUpperCase());
columnOptions.setUuid(column.getUuid().toString());
priorField = null;
if (priorMessage != null) {
for (FieldDescriptorProto field : priorMessage.getFieldList()) {
FieldOptions options = field.getOptions();
if ((options != null) &&
(options.hasExtension(ColumnOptions.fdbsql))) {
ColumnOptions coptions = options.getExtension(ColumnOptions.fdbsql);
if (coptions.getUuid().equals(columnOptions.getUuid())) {
priorField = field;
break;
}
}
}
}
setColumnType(column, columnOptions);
setFieldNumber();
fieldBuilderOptions.setExtension(ColumnOptions.fdbsql, columnOptions.build());
fieldBuilder.setOptions(fieldBuilderOptions);
if (column.getNullable() &&
((column.getDefaultValue() != null) ||
(column.getDefaultFunction() != null))) {
addNullForField(column.getName(), fieldBuilder.getNumber());
}
}
protected void setColumnType(Column column, ColumnOptions.Builder columnOptions) {
ProtobufRowConversion conversion = ProtobufRowConversion.forTInstance(column.getType());
assert (conversion != null) : column;
Type type = conversion.getType();
int decimalScale = conversion.getDecimalScale();
fieldBuilder.setType(type);
if (decimalScale >= 0) {
columnOptions.setDecimalScale(decimalScale);
}
if ((priorField != null) &&
((priorField.getType() != type) ||
(priorDecimalScale() != decimalScale))) {
priorField = null;
}
}
protected int priorDecimalScale() {
if (priorField != null) {
ColumnOptions columnOptions = priorField.getOptions().getExtension(ColumnOptions.fdbsql);
if (columnOptions.hasDecimalScale()) {
return columnOptions.getDecimalScale();
}
}
return -1;
}
protected void setFieldNumber() {
if (priorField != null) {
fieldBuilder.setNumber(priorField.getNumber());
if (fieldBuilder.getNumber() >= nextField) {
nextField = fieldBuilder.getNumber() + 1;
}
}
else {
fieldBuilder.setNumber(nextField++);
}
}
protected void addNullForField(String columnName, int forField) {
String fieldName = uniqueIdent("_" + ident(columnName, false) + "_is_null", fieldNames);
fieldBuilder = messageBuilder.addFieldBuilder();
fieldBuilder.setName(fieldName);
fieldBuilder.setType(Type.TYPE_BOOL);
fieldBuilder.setLabel(Label.LABEL_OPTIONAL);
FieldOptions.Builder fieldBuilderOptions = FieldOptions.newBuilder();
ColumnOptions.Builder columnOptions = ColumnOptions.newBuilder();
columnOptions.setNullForField(forField);
priorField = null;
if (priorMessage != null) {
for (FieldDescriptorProto field : priorMessage.getFieldList()) {
FieldOptions options = field.getOptions();
if ((options != null) &&
(options.hasExtension(ColumnOptions.fdbsql))) {
ColumnOptions coptions = options.getExtension(ColumnOptions.fdbsql);
if (coptions.hasNullForField() &&
(coptions.getNullForField() == forField)) {
priorField = field;
break;
}
}
}
}
setFieldNumber();
fieldBuilderOptions.setExtension(ColumnOptions.fdbsql, columnOptions.build());
fieldBuilder.setOptions(fieldBuilderOptions);
}
protected void addChildTable(Table table) {
String fieldName = uniqueIdent(ident(table.getName().getTableName(), false), fieldNames);
fieldBuilder = messageBuilder.addFieldBuilder();
fieldBuilder.setName(fieldName);
fieldBuilder.setLabel(Label.LABEL_REPEATED);
fieldBuilder.setType(Type.TYPE_MESSAGE);
fieldBuilder.setTypeName(tableMessageNames.get(table));
FieldOptions.Builder fieldBuilderOptions = FieldOptions.newBuilder();
ColumnOptions.Builder columnOptions = ColumnOptions.newBuilder();
columnOptions.setUuid(table.getUuid().toString());
priorField = null;
if (priorMessage != null) {
for (FieldDescriptorProto field : priorMessage.getFieldList()) {
FieldOptions options = field.getOptions();
if ((options != null) &&
(options.hasExtension(ColumnOptions.fdbsql))) {
ColumnOptions coptions = options.getExtension(ColumnOptions.fdbsql);
if (coptions.getUuid().equals(columnOptions.getUuid())) {
priorField = field;
break;
}
}
}
}
setFieldNumber();
fieldBuilderOptions.setExtension(ColumnOptions.fdbsql, columnOptions.build());
fieldBuilder.setOptions(fieldBuilderOptions);
}
protected void addGroupMessage() {
messageBuilder = fileBuilder.addMessageTypeBuilder();
messageBuilder.setName(uniqueIdent("_Group", messageNames));
MessageOptions.Builder messageOptions = MessageOptions.newBuilder();
TableOptions.Builder tableOptions = TableOptions.newBuilder();
tableOptions.setIsGroup(true);
priorMessage = null;
if (priorFile != null) {
for (DescriptorProto message : priorFile.getMessageTypeList()) {
MessageOptions options = message.getOptions();
if ((options != null) &&
(options.hasExtension(TableOptions.fdbsql))) {
TableOptions toptions = options.getExtension(TableOptions.fdbsql);
if (toptions.getIsGroup()) {
priorMessage = message;
break;
}
}
}
}
nextField = 1;
if (priorMessage != null) {
TableOptions options = priorMessage.getOptions().getExtension(TableOptions.fdbsql);
if (options.hasNextField()) {
nextField = options.getNextField();
}
else {
nextField = priorMessage.getField(priorMessage.getFieldCount() - 1)
.getNumber() + 1;
}
}
fieldNames.clear();
for (Table table : tables) {
String fieldName = uniqueIdent(ident(table.getName().getTableName(), false), fieldNames);
fieldBuilder = messageBuilder.addFieldBuilder();
fieldBuilder.setName(fieldName);
fieldBuilder.setLabel(Label.LABEL_OPTIONAL);
fieldBuilder.setType(Type.TYPE_MESSAGE);
fieldBuilder.setTypeName(tableMessageNames.get(table));
FieldOptions.Builder fieldBuilderOptions = FieldOptions.newBuilder();
ColumnOptions.Builder columnOptions = ColumnOptions.newBuilder();
columnOptions.setUuid(table.getUuid().toString());
priorField = null;
if (priorMessage != null) {
for (FieldDescriptorProto field : priorMessage.getFieldList()) {
FieldOptions options = field.getOptions();
if ((options != null) &&
(options.hasExtension(ColumnOptions.fdbsql))) {
ColumnOptions coptions = options.getExtension(ColumnOptions.fdbsql);
if (coptions.getUuid().equals(columnOptions.getUuid())) {
priorField = field;
break;
}
}
}
}
setFieldNumber();
fieldBuilderOptions.setExtension(ColumnOptions.fdbsql, columnOptions.build());
fieldBuilder.setOptions(fieldBuilderOptions);
}
if (nextField != messageBuilder.getFieldOrBuilder(messageBuilder.getFieldCount() - 1).getNumber() + 1) {
tableOptions.setNextField(nextField);
}
messageOptions.setExtension(TableOptions.fdbsql, tableOptions.build());
messageBuilder.setOptions(messageOptions);
}
protected String ident(String base, boolean camelize) {
String ident = base.toLowerCase();
if (camelize) {
StringBuilder str = new StringBuilder();
boolean upper = true;
for (int i = 0; i < ident.length(); i++) {
char ch = ident.charAt(i);
if (ch == '_') {
upper = true;
}
else if (upper) {
str.append(Character.toUpperCase(ch));
upper = false;
}
else {
str.append(ch);
}
}
ident = str.toString();
}
return ident;
}
protected String uniqueIdent(String base, Set<String> existing) {
for (int i = 0; ; i++) {
String ident = (i == 0) ? base : base + "_" + i;
if (existing.add(ident)) {
return ident;
}
}
}
}