/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* 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 VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.voltdb.CatalogContext.ProcedurePartitionInfo;
import org.voltdb.catalog.Catalog;
import org.voltdb.catalog.CatalogMap;
import org.voltdb.catalog.Column;
import org.voltdb.catalog.ColumnRef;
import org.voltdb.catalog.Constraint;
import org.voltdb.catalog.Database;
import org.voltdb.catalog.ProcParameter;
import org.voltdb.catalog.Procedure;
import org.voltdb.catalog.Table;
import org.voltdb.types.ConstraintType;
import org.voltdb.utils.CatalogUtil;
/**
* The DefaultProcedureManager is the hub for all default procedure code and it lives in the
* CatalogContext. It doesn't exist outside of a running VoltDB.
*
* It has three jobs. First, know what the set of callable default procs is. This is used by
* the system catalog stats (jdbc metadata, etc..) and by ClientInterface to accept calls for
* default procs. Second, it has to generate SQL for default procs. Third, it actually supplies
* compiled org.voltdb.catalog.Procedure instances to execution sites upon request.
*
*/
public class DefaultProcedureManager {
Map<String, Procedure> m_defaultProcMap = new HashMap<>();
private final Database m_db;
// fake db makes it easy to create procedures that aren't
// part of the main catalog
private final Database m_fakeDb;
public DefaultProcedureManager(Database db) {
m_db = db;
m_fakeDb = new Catalog().getClusters().add("cluster").getDatabases().add("database");
build();
}
public Procedure checkForDefaultProcedure(String name) {
return m_defaultProcMap.get(name.toLowerCase());
}
private void build() {
for (Table table : m_db.getTables()) {
String prefix = table.getTypeName() + '.';
if (CatalogUtil.isTableExportOnly(m_db, table)) {
Column partitioncolumn = table.getPartitioncolumn();
if (partitioncolumn != null) {
int partitionIndex = partitioncolumn.getIndex();
addShimProcedure(prefix + "insert", table, null, true, partitionIndex, partitioncolumn, false);
} else {
addShimProcedure(prefix + "insert", table, null, true, -1, null, false);
}
continue;
}
// skip views XXX why no get by pkey?
if (table.getMaterializer() != null) {
continue;
}
// select/delete/update crud requires pkey. Pkeys are stored as constraints.
final CatalogMap<Constraint> constraints = table.getConstraints();
final Iterator<Constraint> it = constraints.iterator();
Constraint pkey = null;
while (it.hasNext()) {
Constraint constraint = it.next();
if (constraint.getType() == ConstraintType.PRIMARY_KEY.getValue()) {
pkey = constraint;
break;
}
}
if (table.getIsreplicated()) {
// Creating multi-partition insert procedures for replicated table
addShimProcedure(prefix + "insert", table, null, true, -1, null, false);
// Creating multi-partition delete/update/upsert procedures for replicated table with pkey
if (pkey != null) {
addShimProcedure(prefix + "delete", table, pkey, false, -1, null, false);
addShimProcedure(prefix + "update", table, pkey, true, -1, null, false);
addShimProcedure(prefix + "upsert", table, null, true, -1, null, false);
}
continue;
}
// get the partition column
final Column partitioncolumn = table.getPartitioncolumn();
// this check is an accommodation for some tests that don't flesh out a catalog
if (partitioncolumn == null) {
continue;
}
final int partitionIndex = partitioncolumn.getIndex();
// all partitioned tables get insert crud procs
addShimProcedure(prefix + "insert", table, null, true, partitionIndex, partitioncolumn, false);
// Skip creation of CRUD select/delete/update for partitioned table if no primary key is declared.
if (pkey == null) {
continue;
}
// Primary key must include the partition column for the table
// for select/delete/update
int pkeyPartitionIndex = -1;
CatalogMap<ColumnRef> pkeycols = pkey.getIndex().getColumns();
Iterator<ColumnRef> pkeycolsit = pkeycols.iterator();
while (pkeycolsit.hasNext()) {
ColumnRef colref = pkeycolsit.next();
if (colref.getColumn().equals(partitioncolumn)) {
pkeyPartitionIndex = colref.getIndex();
break;
}
}
// Skip creation of CRUD select/delete/update for partitioned table
// if primary key does not include the partitioning column.
if (pkeyPartitionIndex < 0) {
continue;
}
int columnCount = table.getColumns().size();
// select, delete, update and upsert here (insert generated above)
// these next 3 prefix params with the pkey so the partition on the index of the partition column
// within the pkey
addShimProcedure(prefix + "select", table, pkey, false, pkeyPartitionIndex, partitioncolumn, true);
addShimProcedure(prefix + "delete", table, pkey, false, pkeyPartitionIndex, partitioncolumn, false);
// update partitions on the pkey column after the regular column
addShimProcedure(prefix + "update", table, pkey, true, columnCount + pkeyPartitionIndex, partitioncolumn, false);
// upsert partitions like a regular insert
addShimProcedure(prefix + "upsert", table, null, true, partitionIndex, partitioncolumn, false);
}
}
public static String sqlForDefaultProc(Procedure defaultProc) {
String name = defaultProc.getClassname();
String[] parts = name.split("\\.");
String action = parts[1];
Table table = defaultProc.getPartitiontable();
Column partitionColumn = defaultProc.getPartitioncolumn();
final CatalogMap<Constraint> constraints = table.getConstraints();
final Iterator<Constraint> it = constraints.iterator();
Constraint pkey = null;
while (it.hasNext()) {
Constraint constraint = it.next();
if (constraint.getType() == ConstraintType.PRIMARY_KEY.getValue()) {
pkey = constraint;
break;
}
}
switch(action) {
case "select":
assert (defaultProc.getSinglepartition());
return generateCrudSelect(table, partitionColumn, pkey);
case "insert":
if (defaultProc.getSinglepartition()) {
return generateCrudInsert(table, partitionColumn);
}
else {
return generateCrudReplicatedInsert(table);
}
case "update":
if (defaultProc.getSinglepartition()) {
return generateCrudUpdate(table, partitionColumn, pkey);
}
else {
return generateCrudReplicatedUpdate(table, pkey);
}
case "delete":
if (defaultProc.getSinglepartition()) {
return generateCrudDelete(table, partitionColumn, pkey);
}
else {
return generateCrudReplicatedDelete(table, pkey);
}
case "upsert":
if (defaultProc.getSinglepartition()) {
return generateCrudUpsert(table, partitionColumn);
}
else {
return generateCrudReplicatedUpsert(table, pkey);
}
default:
throw new RuntimeException("Invalid input to default proc SQL generator.");
}
}
/** Helper to sort table columns by table column order */
private static class TableColumnComparator implements Comparator<Column> {
public TableColumnComparator() {
}
@Override
public int compare(Column o1, Column o2) {
return o1.getIndex() - o2.getIndex();
}
}
/** Helper to sort index columnrefs by index column order */
private static class ColumnRefComparator implements Comparator<ColumnRef> {
public ColumnRefComparator() {
}
@Override
public int compare(ColumnRef o1, ColumnRef o2) {
return o1.getIndex() - o2.getIndex();
}
}
/**
* Helper to generate a WHERE pkey_col1 = ?, pkey_col2 = ? ...; clause.
* @param partitioncolumn partitioning column for the table
* @param pkey constraint from the catalog
* @param paramoffset 0-based counter of parameters in the full sql statement so far
* @param sb string buffer accumulating the sql statement
* @return offset in the index of the partition column
*/
private static int generateCrudPKeyWhereClause(Column partitioncolumn,
Constraint pkey, StringBuilder sb)
{
// Sort the catalog index columns by index column order.
ArrayList<ColumnRef> indexColumns = new ArrayList<ColumnRef>(pkey.getIndex().getColumns().size());
for (ColumnRef c : pkey.getIndex().getColumns()) {
indexColumns.add(c);
}
Collections.sort(indexColumns, new ColumnRefComparator());
boolean first = true;
int partitionOffset = -1;
sb.append(" WHERE ");
for (ColumnRef pkc : indexColumns) {
if (!first) sb.append(" AND ");
first = false;
sb.append("(" + pkc.getColumn().getName() + " = ?" + ")");
if (pkc.getColumn() == partitioncolumn) {
partitionOffset = pkc.getIndex();
}
}
return partitionOffset;
}
/**
* Helper to generate a full col1 = ?, col2 = ?... clause.
* @param table
* @param sb
*/
private static void generateCrudExpressionColumns(Table table, StringBuilder sb) {
boolean first = true;
// Sort the catalog table columns by column order.
ArrayList<Column> tableColumns = new ArrayList<Column>(table.getColumns().size());
for (Column c : table.getColumns()) {
tableColumns.add(c);
}
Collections.sort(tableColumns, new TableColumnComparator());
for (Column c : tableColumns) {
if (!first) sb.append(", ");
first = false;
sb.append(c.getName() + " = ?");
}
}
/**
* Helper to generate a full col1, col2, col3 list.
*/
private static void generateCrudColumnList(Table table, StringBuilder sb) {
boolean first = true;
sb.append("(");
// Sort the catalog table columns by column order.
ArrayList<Column> tableColumns = new ArrayList<Column>(table.getColumns().size());
for (Column c : table.getColumns()) {
tableColumns.add(c);
}
Collections.sort(tableColumns, new TableColumnComparator());
// Output the SQL column list.
for (Column c : tableColumns) {
assert (c.getIndex() >= 0); // mostly mask unused 'c'.
if (!first) sb.append(", ");
first = false;
sb.append("?");
}
sb.append(")");
}
/**
* Create a statement like:
* "delete from <table> where {<pkey-column =?>...}"
*/
private static String generateCrudDelete(Table table, Column partitioncolumn, Constraint pkey) {
StringBuilder sb = new StringBuilder();
sb.append("DELETE FROM " + table.getTypeName());
generateCrudPKeyWhereClause(partitioncolumn, pkey, sb);
sb.append(';');
return sb.toString();
}
/**
* Create a statement like:
* "update <table> set {<each-column = ?>...} where {<pkey-column = ?>...}
*/
private static String generateCrudUpdate(Table table, Column partitioncolumn, Constraint pkey) {
StringBuilder sb = new StringBuilder();
sb.append("UPDATE " + table.getTypeName() + " SET ");
generateCrudExpressionColumns(table, sb);
generateCrudPKeyWhereClause(partitioncolumn, pkey, sb);
sb.append(';');
return sb.toString();
}
/**
* Create a statement like:
* "insert into <table> values (?, ?, ...);"
*/
private static String generateCrudInsert(Table table, Column partitioncolumn) {
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO " + table.getTypeName() + " VALUES ");
generateCrudColumnList(table, sb);
sb.append(";");
return sb.toString();
}
/**
* Create a statement like:
* Hack simple case of implementation SQL MERGE
* "upsert into <table> values (?, ?, ...);"
*/
private static String generateCrudUpsert(Table table, Column partitioncolumn) {
StringBuilder sb = new StringBuilder();
sb.append("UPSERT INTO " + table.getTypeName() + " VALUES ");
generateCrudColumnList(table, sb);
sb.append(";");
return sb.toString();
}
/**
* Create a statement like:
* "insert into <table> values (?, ?, ...);"
* for a replicated table.
*/
private static String generateCrudReplicatedInsert(Table table) {
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO " + table.getTypeName() + " VALUES ");
generateCrudColumnList(table, sb);
sb.append(";");
return sb.toString();
}
/**
* Create a statement like:
* "update <table> set {<each-column = ?>...} where {<pkey-column = ?>...}
* for a replicated table.
*/
private static String generateCrudReplicatedUpdate(Table table, Constraint pkey)
{
StringBuilder sb = new StringBuilder();
sb.append("UPDATE " + table.getTypeName() + " SET ");
generateCrudExpressionColumns(table, sb);
generateCrudPKeyWhereClause(null, pkey, sb);
sb.append(';');
return sb.toString();
}
/**
* Create a statement like:
* "delete from <table> where {<pkey-column =?>...}"
* for a replicated table.
*/
private static String generateCrudReplicatedDelete(Table table, Constraint pkey)
{
StringBuilder sb = new StringBuilder();
sb.append("DELETE FROM " + table.getTypeName());
generateCrudPKeyWhereClause(null, pkey, sb);
sb.append(';');
return sb.toString();
}
private static String generateCrudReplicatedUpsert(Table table, Constraint pkey)
{
StringBuilder sb = new StringBuilder();
sb.append("UPSERT INTO " + table.getTypeName() + " VALUES ");
generateCrudColumnList(table, sb);
sb.append(";");
return sb.toString();
}
/**
* Create a statement like:
* "select * from <table> where pkey_col1 = ?, pkey_col2 = ? ... ;"
*/
private static String generateCrudSelect(Table table, Column partitioncolumn, Constraint pkey)
{
StringBuilder sb = new StringBuilder();
sb.append("SELECT * FROM " + table.getTypeName());
generateCrudPKeyWhereClause(partitioncolumn, pkey, sb);
sb.append(';');
return sb.toString();
}
private void addShimProcedure(String name,
Table table,
Constraint pkey,
boolean tableCols,
int partitionParamIndex,
Column partitionColumn,
boolean readOnly)
{
Procedure proc = m_fakeDb.getProcedures().add(name);
proc.setClassname(name);
proc.setDefaultproc(true);
proc.setHasjava(false);
proc.setHasseqscans(false);
proc.setSinglepartition(partitionParamIndex >= 0);
proc.setPartitioncolumn(partitionColumn);
proc.setPartitionparameter(partitionParamIndex);
proc.setReadonly(readOnly);
proc.setEverysite(false);
proc.setSystemproc(false);
proc.setPartitiontable(table);
if (partitionParamIndex >= 0) {
proc.setAttachment(new ProcedurePartitionInfo(VoltType.get((byte) partitionColumn.getType()), partitionParamIndex));
}
int paramCount = 0;
if (tableCols) {
for (Column col : table.getColumns()) {
// name each parameter "param1", "param2", etc...
ProcParameter procParam = proc.getParameters().add("param" + String.valueOf(paramCount));
procParam.setIndex(col.getIndex());
procParam.setIsarray(false);
procParam.setType(col.getType());
paramCount++;
}
}
if (pkey != null) {
CatalogMap<ColumnRef> pkeycols = pkey.getIndex().getColumns();
int paramCount2 = paramCount;
for (ColumnRef cref : pkeycols) {
// name each parameter "param1", "param2", etc...
ProcParameter procParam = proc.getParameters().add("param" + String.valueOf(paramCount2));
procParam.setIndex(cref.getIndex() + paramCount);
procParam.setIsarray(false);
procParam.setType(cref.getColumn().getType());
paramCount2++;
}
}
m_defaultProcMap.put(name.toLowerCase(), proc);
}
}