com.yahoo.yqlplus.engine.internal.source.SourceUnitGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.yahoo.yqlplus.engine.internal.source.SourceUnitGenerator.java

Source

/*
 * Copyright (c) 2016 Yahoo Inc.
 * Licensed under the terms of the Apache version 2.0 license.
 * See LICENSE file for terms.
 */

package com.yahoo.yqlplus.engine.internal.source;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.yahoo.yqlplus.api.Source;
import com.yahoo.yqlplus.api.annotations.CompoundKey;
import com.yahoo.yqlplus.api.annotations.DefaultValue;
import com.yahoo.yqlplus.api.annotations.Delete;
import com.yahoo.yqlplus.api.annotations.Insert;
import com.yahoo.yqlplus.api.annotations.Key;
import com.yahoo.yqlplus.api.annotations.Query;
import com.yahoo.yqlplus.api.annotations.Set;
import com.yahoo.yqlplus.api.annotations.TimeoutBudget;
import com.yahoo.yqlplus.api.annotations.Update;
import com.yahoo.yqlplus.api.index.IndexDescriptor;
import com.yahoo.yqlplus.api.trace.Tracer;
import com.yahoo.yqlplus.api.types.YQLNamePair;
import com.yahoo.yqlplus.api.types.YQLStructType;
import com.yahoo.yqlplus.api.types.YQLType;
import com.yahoo.yqlplus.api.types.YQLTypeException;
import com.yahoo.yqlplus.engine.TaskContext;
import com.yahoo.yqlplus.engine.api.PropertyNotFoundException;
import com.yahoo.yqlplus.engine.api.Record;
import com.yahoo.yqlplus.engine.internal.bytecode.types.gambit.GambitCreator;
import com.yahoo.yqlplus.engine.internal.bytecode.types.gambit.GambitScope;
import com.yahoo.yqlplus.engine.internal.bytecode.types.gambit.ObjectBuilder;
import com.yahoo.yqlplus.engine.internal.bytecode.types.gambit.PhysicalExprOperatorCompiler;
import com.yahoo.yqlplus.engine.internal.bytecode.types.gambit.ScopedBuilder;
import com.yahoo.yqlplus.engine.internal.bytecode.types.gambit.StructBuilder;
import com.yahoo.yqlplus.engine.internal.plan.DispatchSourceTypeAdapter;
import com.yahoo.yqlplus.engine.internal.plan.SourceType;
import com.yahoo.yqlplus.engine.internal.plan.types.AssignableValue;
import com.yahoo.yqlplus.engine.internal.plan.types.BytecodeExpression;
import com.yahoo.yqlplus.engine.internal.plan.types.TypeWidget;
import com.yahoo.yqlplus.engine.internal.plan.types.base.AnyTypeWidget;
import com.yahoo.yqlplus.engine.internal.plan.types.base.BaseTypeAdapter;
import com.yahoo.yqlplus.engine.internal.plan.types.base.FieldWriter;
import com.yahoo.yqlplus.engine.internal.plan.types.base.ListTypeWidget;
import com.yahoo.yqlplus.engine.internal.plan.types.base.NotNullableTypeWidget;
import com.yahoo.yqlplus.engine.internal.plan.types.base.PropertyAdapter;
import com.yahoo.yqlplus.language.parser.Location;
import com.yahoo.yqlplus.language.parser.ProgramCompileException;
import org.objectweb.asm.Opcodes;

import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeSet;

/**
 * Generated one or more classes adapting Source-API equipped classes.
 */
public class SourceUnitGenerator extends SourceApiGenerator {
    private static final String[] OPERATIONS = new String[] { "SELECT", "INSERT", "UPDATE", "DELETE" };

    public SourceUnitGenerator(GambitScope gambitScope) {
        super(gambitScope);
    }

    // sync / async
    // single / batch
    // "free" arguments
    // injectable arguments
    //    taskemitter
    //    tracer
    //    timeout
    //

    public ObjectBuilder createSourceAdapter(String sourceName, Class<?> sourceClass, TypeWidget sourceType,
            TypeWidget contextType, BytecodeExpression sourceProvider) {
        gambitScope.addClass(sourceClass);
        ObjectBuilder adapter = gambitScope.createObject();
        ObjectBuilder.ConstructorBuilder cb = adapter.getConstructor();
        ObjectBuilder.FieldBuilder fld = adapter.field("$source", sourceType);
        fld.addModifiers(Opcodes.ACC_FINAL);
        cb.exec(fld.get(cb.local("this")).write(cb.cast(sourceType, cb.invokeExact(Location.NONE, "get",
                Provider.class, AnyTypeWidget.getInstance(), sourceProvider))));
        adapter.addParameterField(fld);
        ObjectBuilder.FieldBuilder programName = adapter.field("$programName", BaseTypeAdapter.STRING);
        programName.annotate(Inject.class);
        programName.annotate(Named.class).put("value", "programName");
        adapter.addParameterField(programName);

        BytecodeExpression metric = gambitScope.constant(PhysicalExprOperatorCompiler.EMPTY_DIMENSION);
        metric = metricWith(cb, metric, "source", sourceName);
        for (String operation : OPERATIONS) {
            // adapter.addParameterField(adapter.finalField("$metricDimension$" + operation, metricWith(cb, metric, "operation", operation)));
            // current code breaks if we add operation dimension; don't for now  TODO: modify that code to ignore new dimensions
            adapter.addParameterField(adapter.finalField("$metricDimension$" + operation, metric));
        }
        return adapter;
    }

    interface SourceMethodBuilder extends SourceArgumentVisitor {
        void begin(ScopedBuilder body, TypeWidget rowType, boolean singleton, boolean async);

        void complete(ObjectBuilder.MethodBuilder adapterMethod, ScopedBuilder body);
    }

    class AdapterBuilder {
        List<Method> methods = Lists.newArrayList();
        String sourceName;
        Class<?> clazz;
        ObjectBuilder target;
        TypeWidget sourceClass;
        List<Class<?>> signature;
        List<String> argumentNames;

        // SELECT
        // DELETE - match an index (just like SELECT)

        // UPDATE - match an INDEX
        // INSERT - accept a record, dispatch to an appropriate method if possible

        Map<IndexDescriptor, QueryMethod> selectMap = Maps.newHashMap();
        QueryMethod scanner;

        Map<IndexDescriptor, QueryMethod> deleteMap = Maps.newHashMap();
        QueryMethod deleteAll;

        Map<IndexDescriptor, UpdateMethod> updateMap = Maps.newHashMap();
        UpdateMethod updateAll;

        InsertMethod insert;
        long minimumBudget;
        long maximumBudget;
        int sym = 0;

        public AdapterBuilder(String sourceName, Class<? extends Source> clazz, BytecodeExpression providerConstant,
                List<Class<?>> signature, long minimumBudget, long maximumBudget) {
            this.clazz = clazz;
            this.sourceName = sourceName;
            this.minimumBudget = minimumBudget;
            this.maximumBudget = maximumBudget;
            // TODO: should accept the annotations for the free argument signatures, so they can be checked for @NotNullable or equiv.
            // TODO: should likely use generic types for type adapters
            this.sourceClass = gambitScope.adapt(clazz, false);
            this.target = createSourceAdapter(sourceName, clazz, sourceClass,
                    gambitScope.adapt(TaskContext.class, false), providerConstant);
            this.argumentNames = Lists.newArrayList();
            this.signature = signature;
            for (Class<?> arg : signature) {
                String name = "$arg" + argumentNames.size();
                switch (arg.getCanonicalName()) {
                case "java.lang.Byte":
                    target.addParameter(name, BaseTypeAdapter.INT8);
                    break;
                case "java.lang.Short":
                    target.addParameter(name, BaseTypeAdapter.INT16);
                    break;
                case "java.lang.Integer":
                    target.addParameter(name, BaseTypeAdapter.INT32);
                    break;
                case "java.lang.Long":
                    target.addParameter(name, BaseTypeAdapter.INT64);
                    break;
                case "java.lang.Double":
                    target.addParameter(name, BaseTypeAdapter.FLOAT64);
                    break;
                case "java.lang.Boolean":
                    target.addParameter(name, BaseTypeAdapter.BOOLEAN);
                    break;
                default:
                    target.addParameter(name, gambitScope.adapt(arg, true));
                    break;
                }
                argumentNames.add(name);
            }
        }

        private String gensym(String prefix) {
            return prefix + (++sym);
        }

        public SourceType create() {
            return new IndexedSourceAdapter(target.type(), sourceName, selectMap, scanner, deleteMap, deleteAll,
                    updateMap, updateAll, insert);
        }

        private void reportMethodParameterException(String type, Method method, String message, Object... args) {
            message = String.format(message, args);
            throw new YQLTypeException(
                    String.format("@%s method error: %s.%s: %s", type, clazz.getName(), method.getName(), message));
        }

        private void reportMethodException(Method method, String message, Object... args) {
            message = String.format(message, args);
            throw new YQLTypeException(
                    String.format("method error: %s.%s: %s", clazz.getName(), method.getName(), message));
        }

        /**
         * Verifies that the given resultType has a property matching the given fieldName and fieldType.
         */
        private void verifyArgumentType(String methodTypeName, TypeWidget rowType, PropertyAdapter rowProperties,
                String propertyName, TypeWidget argumentType, String annotationName, Method method)
                throws ProgramCompileException, PropertyNotFoundException {
            try {
                TypeWidget targetType = rowProperties.getPropertyType(propertyName);
                if (!targetType.isAssignableFrom(argumentType)) {
                    reportMethodParameterException(methodTypeName, method,
                            "class %s property %s is %s while @%s('%s') type is %s: @%s('%s') "
                                    + "argument type %s cannot be coerced to property '%s' type %s in method %s.%s",
                            rowType.getTypeName(), propertyName, targetType.getTypeName(), annotationName,
                            propertyName, argumentType.getTypeName(), annotationName, propertyName,
                            argumentType.getTypeName(), propertyName, targetType.getTypeName(),
                            method.getDeclaringClass().getName(), method.getName());
                }
            } catch (PropertyNotFoundException e) {
                reportMethodParameterException(methodTypeName, method,
                        "Property @%s('%s') for method %s.%s does not exist on return type %s", annotationName,
                        propertyName, method.getDeclaringClass().getName(), method.getName(),
                        rowType.getTypeName());
            }
        }

        private ObjectBuilder.MethodBuilder addAdapterMethod(Method method, String operation,
                SourceMethodBuilder bodyBuilder) {
            ObjectBuilder.MethodBuilder adapterMethod = target.method(gensym(operation + "$invoke$"));
            TypeWidget contextType = gambitScope.adapt(TaskContext.class, false);
            adapterMethod.addArgument("$context", contextType);
            GambitCreator.ScopeBuilder block = adapterMethod.scope();

            TypeWidget outputType = gambitScope.adapt(method.getGenericReturnType(), true);
            AssignableValue resultValue = block.allocate(outputType);

            TypeWidget rowType = outputType;
            boolean singleton = true;
            boolean async = false;
            if (outputType.isPromise()) {
                rowType = outputType.getPromiseAdapter().getResultType();
                async = true;
            }
            if (rowType.isIterable()) {
                singleton = false;
                rowType = rowType.getIterableAdapter().getValue();
            }

            if (!rowType.hasProperties()) {
                throw new YQLTypeException(
                        "Source method " + method + " does not return a STRUCT type: " + rowType);
            }

            // construct for evaluate an expression in a context with or without timeout enforcement

            AssignableValue contextVar = block.local("$context");

            BytecodeExpression metricExpr = block.local("$metricDimension$" + operation);
            block.set(Location.NONE, contextVar, block.invokeExact(Location.NONE, "start", TaskContext.class,
                    contextVar.getType(), contextVar, metricWith(block, metricExpr, "method", method.getName())));
            GambitCreator.CatchBuilder catchBlock = block.tryCatchFinally();
            ScopedBuilder finallyBody = catchBlock.always();

            ScopedBuilder catchBody = catchBlock.body();
            // we do NOT want to fork a callable -- just call the function and return the result.
            GambitCreator.Invocable targetMethod = catchBody.findExactInvoker(method.getDeclaringClass(),
                    method.getName(), outputType, method.getParameterTypes());

            Iterator<String> freeArguments = argumentNames.iterator();
            List<BytecodeExpression> invocationArguments = Lists.newArrayList();
            invocationArguments.add(catchBody.local("$source"));

            // lots of boolean parameters are not great
            bodyBuilder.begin(catchBody, rowType, singleton, async);

            visitMethodArguments(target, method, bodyBuilder, contextVar, catchBody, freeArguments,
                    invocationArguments, block);

            bodyBuilder.complete(adapterMethod, catchBody);

            BytecodeExpression invocation = catchBody.invoke(Location.NONE, targetMethod, invocationArguments);

            catchBody.set(Location.NONE, resultValue, invocation);

            block.exec(catchBlock.build());
            block.exec(finallyBody.invokeExact(Location.NONE, "end", TaskContext.class, BaseTypeAdapter.VOID,
                    contextVar));
            for (BytecodeExpression argument : invocationArguments) {
                if (argument.getType().getJVMType().getClassName().equals(Tracer.class.getName())) {
                    block.exec(finallyBody.invokeExact(Location.NONE, "end", Tracer.class, BaseTypeAdapter.VOID,
                            argument));
                }
            }
            adapterMethod.exit(block.complete(resultValue));
            return adapterMethod;
        }

        public void addSelectMethod(final Method method) {
            TimeoutBudget budget = method.getAnnotation(TimeoutBudget.class);
            long minimumBudget = this.minimumBudget;
            long maximumBudget = this.maximumBudget;
            if (budget != null) {
                minimumBudget = budget.minimumMilliseconds();
                maximumBudget = budget.maximumMilliseconds();
            }

            SelectMethodAdapterBuilder builder = new SelectMethodAdapterBuilder(method);
            ObjectBuilder.MethodBuilder methodBuilder = addAdapterMethod(method, "SELECT", builder);
            // ok, so we've define the adapter method which handles all of the injectable arguments
            // and we're left with a method that takes (context{, key-or-keys}) where
            // key-or-keys is:
            //    absent if it's a SCAN method
            //    a single Record instance of the keys to do a lookup for if it's a SINGLE
            //    a list of Record instances of the keys to do a lookup for if it's a BATCH

            if (builder.isScan()) {
                if (scanner != null) {
                    reportMethodException(method,
                            "There can be only one @Query method for SCAN (no @Key/@CompoundKey arguments) (and one is already set)");
                }
                scanner = new QueryMethod(builder.rowType, target.type(), methodBuilder.invoker(),
                        builder.singleton, builder.async, minimumBudget, maximumBudget);
            } else if (builder.batch) {
                IndexDescriptor descriptor = builder.indexBuilder.build();
                final QueryMethod qm = new QueryMethod(descriptor, QueryMethod.QueryType.BATCH, builder.rowType,
                        target.type(), methodBuilder.invoker(), builder.singleton, builder.async, minimumBudget,
                        maximumBudget);
                selectMap.put(descriptor, qm);
            } else {
                // a single key at a time
                IndexDescriptor descriptor = builder.indexBuilder.build();
                final QueryMethod qm = new QueryMethod(descriptor, QueryMethod.QueryType.SINGLE, builder.rowType,
                        target.type(), methodBuilder.invoker(), builder.singleton, builder.async, minimumBudget,
                        maximumBudget);
                selectMap.put(descriptor, qm);
            }
        }

        public void addInsertMethod(Method method) {
            if (insert != null) {
                reportMethodException(method, "There can be only one @Insert method (and one is already set)");
            }
            TimeoutBudget budget = method.getAnnotation(TimeoutBudget.class);
            long minimumBudget = this.minimumBudget;
            long maximumBudget = this.maximumBudget;
            if (budget != null) {
                minimumBudget = budget.minimumMilliseconds();
                maximumBudget = budget.maximumMilliseconds();
            }

            InsertMethodAdapterBuilder builder = new InsertMethodAdapterBuilder(method);
            ObjectBuilder.MethodBuilder methodBuilder = addAdapterMethod(method, "INSERT", builder);
            // ok, so we've define the adapter method which handles all of the injectable arguments
            // and we're left with a method that takes (context{, key-or-keys}) where
            // key-or-keys is:
            //    absent if it's a SCAN method
            //    a single Record instance of the keys to do a lookup for if it's a SINGLE
            //    a list of Record instances of the keys to do a lookup for if it's a BATCH
            this.insert = new InsertMethod(method.getDeclaringClass() + ":" + method.getName(), builder.rowType,
                    builder.insertType, target.type(), methodBuilder.invoker(), false, builder.singleton,
                    builder.async, minimumBudget, maximumBudget);
        }

        public void addUpdateMethod(Method method) {
            TimeoutBudget budget = method.getAnnotation(TimeoutBudget.class);
            long minimumBudget = this.minimumBudget;
            long maximumBudget = this.maximumBudget;
            if (budget != null) {
                minimumBudget = budget.minimumMilliseconds();
                maximumBudget = budget.maximumMilliseconds();
            }

            UpdateMethodAdapterBuilder builder = new UpdateMethodAdapterBuilder(method);
            ObjectBuilder.MethodBuilder methodBuilder = addAdapterMethod(method, "UPDATE", builder);
            // ok, so we've define the adapter method which handles all of the injectable arguments
            // and we're left with a method that takes (context{, key-or-keys}) where
            // key-or-keys is:
            //    absent if it's a SCAN method
            //    a single Record instance of the keys to do a lookup for if it's a SINGLE
            //    a list of Record instances of the keys to do a lookup for if it's a BATCH
            if (builder.isScan()) {
                if (updateAll != null) {
                    reportMethodException(method,
                            "There can be only one @Update all method (and one is already set)");
                }
                updateAll = new UpdateMethod(method.getDeclaringClass() + ":" + method.getName(), builder.rowType,
                        builder.updateType, builder.updateRecord, target.type(), methodBuilder.invoker(),
                        builder.singleton, builder.async, minimumBudget, maximumBudget);
            } else if (builder.batch) {
                IndexDescriptor descriptor = builder.indexBuilder.build();
                final UpdateMethod qm = new UpdateMethod(method.getDeclaringClass() + ":" + method.getName(),
                        descriptor, QueryMethod.QueryType.BATCH, builder.rowType, builder.updateType,
                        builder.updateRecord, target.type(), methodBuilder.invoker(), builder.singleton,
                        builder.async, minimumBudget, maximumBudget);
                updateMap.put(descriptor, qm);
            } else {
                // a single key at a time
                IndexDescriptor descriptor = builder.indexBuilder.build();
                final UpdateMethod qm = new UpdateMethod(method.getDeclaringClass() + ":" + method.getName(),
                        descriptor, QueryMethod.QueryType.SINGLE, builder.rowType, builder.updateType,
                        builder.updateRecord, target.type(), methodBuilder.invoker(), builder.singleton,
                        builder.async, minimumBudget, maximumBudget);
                updateMap.put(descriptor, qm);
            }
        }

        public void addDeleteMethod(Method method) {
            TimeoutBudget budget = method.getAnnotation(TimeoutBudget.class);
            long minimumBudget = this.minimumBudget;
            long maximumBudget = this.maximumBudget;
            if (budget != null) {
                minimumBudget = budget.minimumMilliseconds();
                maximumBudget = budget.maximumMilliseconds();
            }

            DeleteMethodAdapterBuilder builder = new DeleteMethodAdapterBuilder(method);
            ObjectBuilder.MethodBuilder methodBuilder = addAdapterMethod(method, "DELETE", builder);
            // ok, so we've define the adapter method which handles all of the injectable arguments
            // and we're left with a method that takes (context{, key-or-keys}) where
            // key-or-keys is:
            //    absent if it's a SCAN method
            //    a single Record instance of the keys to do a lookup for if it's a SINGLE
            //    a list of Record instances of the keys to do a lookup for if it's a BATCH

            if (builder.isScan()) {
                deleteAll = new QueryMethod(builder.rowType, target.type(), methodBuilder.invoker(),
                        builder.singleton, builder.async, minimumBudget, maximumBudget);
            } else if (builder.batch) {
                IndexDescriptor descriptor = builder.indexBuilder.build();
                final QueryMethod qm = new QueryMethod(descriptor, QueryMethod.QueryType.BATCH, builder.rowType,
                        target.type(), methodBuilder.invoker(), builder.singleton, builder.async, minimumBudget,
                        maximumBudget);
                deleteMap.put(descriptor, qm);
            } else {
                // a single key at a time
                IndexDescriptor descriptor = builder.indexBuilder.build();
                final QueryMethod qm = new QueryMethod(descriptor, QueryMethod.QueryType.SINGLE, builder.rowType,
                        target.type(), methodBuilder.invoker(), builder.singleton, builder.async, minimumBudget,
                        maximumBudget);
                deleteMap.put(descriptor, qm);
            }
        }

        private abstract class IndexedMethodAdapterBuilder implements SourceMethodBuilder {
            private final String methodType;

            final IndexDescriptor.Builder indexBuilder;
            final Map<String, AssignableValue> keyArguments;
            AssignableValue compoundKeyArgument;
            final Method method;
            boolean singleton;
            boolean async;
            boolean batch;

            ScopedBuilder body;

            TypeWidget rowType;
            PropertyAdapter rowProperties;

            public IndexedMethodAdapterBuilder(String methodType, Method method) {
                this.methodType = methodType;
                this.method = method;
                indexBuilder = IndexDescriptor.builder();
                keyArguments = Maps.newLinkedHashMap();
                batch = false;
            }

            @Override
            public void begin(ScopedBuilder body, TypeWidget rowType, boolean singleton, boolean async) {
                this.body = body;
                this.rowType = rowType;
                if (!rowType.hasProperties()) {
                    reportMethodException(method, "method return type has no properties (is not a struct): %s",
                            rowType.getTypeName());
                }
                rowProperties = rowType.getPropertyAdapter();
                this.async = async;
                this.singleton = singleton;
            }

            protected void addIndexKey(String keyName, TypeWidget keyType, boolean skipEmpty, boolean skipNull) {
                try {
                    indexBuilder.addColumn(keyName, keyType.getValueCoreType(), skipEmpty, skipNull);
                } catch (IllegalArgumentException e) {
                    reportMethodParameterException(methodType, method, "Key '%s' cannot be added to index: %s",
                            keyName, e.getMessage());
                }
            }

            public boolean isScan() {
                return keyArguments.isEmpty() && compoundKeyArgument == null;
            }

            @Override
            public BytecodeExpression visitSet(Set annotate, DefaultValue defaultValue, ScopedBuilder body,
                    Class<?> parameterType, TypeWidget parameterWidget) {
                reportMethodParameterException(methodType, method,
                        "@Set parameters are not permitted on @%s methods", methodType);
                // unreachable
                throw new IllegalArgumentException();
            }

            @Override
            public BytecodeExpression visitKeyArgument(Key key, ScopedBuilder body, Class<?> parameterType,
                    TypeWidget parameterWidget) {
                String keyName = key.value().toLowerCase();
                boolean skipEmpty = key.skipEmptyOrZero();
                boolean skipNull = key.skipNull();
                if (compoundKeyArgument != null) {
                    reportMethodParameterException(methodType, method,
                            "@Key column '%s' conflicts with @CompoundKey argument (only one of @Key arguments and @CompoundKey may be used)",
                            keyName);
                } else if (keyArguments.containsKey(keyName)) {
                    reportMethodParameterException(methodType, method, "@Key column '%s' used multiple times",
                            keyName);
                } else if (List.class.isAssignableFrom(parameterType)) {
                    if (!batch && !isScan()) {
                        reportMethodParameterException(methodType, method,
                                "@Key column '%s' is a List (batch); a method must either be entirely-batch or entirely-not",
                                keyName);
                    }
                    batch = true;
                    TypeWidget keyType = parameterWidget.getIterableAdapter().getValue();
                    verifyArgumentType(methodType, rowType, rowProperties, keyName, keyType, "Key", method);
                    addIndexKey(keyName, keyType, skipEmpty, skipNull);
                    addKeyParameter(body, parameterWidget, keyName);
                } else if (batch) {
                    reportMethodParameterException(methodType, method,
                            "@Key column '%s' is a single value but other parameters are batch; a method must either be entirely-batch or entirely-not",
                            keyName);
                } else {
                    verifyArgumentType(methodType, rowType, rowProperties, keyName, parameterWidget, "Key", method);
                    addIndexKey(keyName, parameterWidget, skipEmpty, skipNull);
                    addKeyParameter(body, parameterWidget, keyName);
                }
                return keyArguments.get(keyName);
            }

            private void addKeyParameter(ScopedBuilder body, TypeWidget parameterWidget, String keyName) {
                keyArguments.put(keyName,
                        body.allocate(gensym(keyName), NotNullableTypeWidget.create(parameterWidget)));
            }

            @Override
            public BytecodeExpression visitCompoundKey(CompoundKey compoundKey, ScopedBuilder body,
                    Class<?> parameterType, TypeWidget parameterWidget) {
                if (!isScan()) {
                    reportMethodParameterException(methodType, method,
                            "Only one @CompoundKey argument is permitted and is mutually exclusive with any @Key arguments");
                }
                boolean skipEmpty = compoundKey.skipEmptyOrZero();
                boolean skipNull = compoundKey.skipNull();
                TypeWidget itemType = parameterWidget;
                if (List.class.isAssignableFrom(parameterType)) {
                    batch = true;
                    itemType = parameterWidget.getIterableAdapter().getValue();
                }
                TypeWidget recordType = gambitScope.adapt(Record.class, false);
                if (!recordType.getJVMType().getDescriptor().equals(itemType.getJVMType().getDescriptor())) {
                    reportMethodParameterException(methodType, method,
                            "@CompoundKey argument type must be %s (not %s)", Record.class.getName(),
                            parameterType.getName());
                }
                String[] keyNames = compoundKey.names();
                if (keyNames == null || keyNames.length == 0) {
                    reportMethodParameterException(methodType, method,
                            "@CompoundKey argument must specify 1 or more names");
                }
                for (String keyName : keyNames) {
                    try {
                        TypeWidget propertyType = rowProperties.getPropertyType(keyName);
                        addIndexKey(keyName, propertyType, skipEmpty, skipNull);
                    } catch (PropertyNotFoundException e) {
                        reportMethodParameterException(methodType, method,
                                "@CompoundKey property '%s' does not exist on row type %s", keyName,
                                rowType.getTypeName());
                    }
                }
                if (batch) {
                    compoundKeyArgument = body.allocate(new ListTypeWidget(recordType));
                    return compoundKeyArgument;
                } else {
                    compoundKeyArgument = body.allocate(recordType);
                    return compoundKeyArgument;
                }
            }

            @Override
            public void complete(ObjectBuilder.MethodBuilder adapterMethod, ScopedBuilder body) {
                if (isScan()) {
                    // we're done
                    return;
                }
                // if it's not a scan, we need to prepare all the key arguments
                if (compoundKeyArgument != null) {
                    // excellent, a compound key argument means we'll just take the record passed into us, so we'll just
                    // add an argument and assign it! this works for batch or not.
                    body.set(Location.NONE, compoundKeyArgument,
                            adapterMethod.addArgument("$key", compoundKeyArgument.getType()));
                    return;
                }
                // if it's not a compound key argument, we need to prepare each of the keyArguments, which will
                // either be lists or not
                // we'll receive a (list of) structs

                // first build the struct type representing each compound key
                StructBuilder structBuilder = gambitScope.createStruct();
                for (Map.Entry<String, AssignableValue> keyEntry : keyArguments.entrySet()) {
                    TypeWidget type = keyEntry.getValue().getType();
                    structBuilder.add(keyEntry.getKey(), (batch ? type.getIterableAdapter().getValue() : type));
                }
                TypeWidget recordType = gambitScope.adapt(Record.class, false);
                if (batch) {
                    BytecodeExpression keys = adapterMethod.addArgument("$key",
                            NotNullableTypeWidget.create(new ListTypeWidget(recordType)));
                    GambitCreator.ScopeBuilder convertScope = body.scope();
                    GambitCreator.IterateBuilder loop = body.iterate(keys);
                    for (Map.Entry<String, AssignableValue> keyEntry : keyArguments.entrySet()) {
                        String propertyName = keyEntry.getKey();
                        AssignableValue keyListValue = keyEntry.getValue();
                        // create the list (above the loop)
                        convertScope.set(Location.NONE, keyListValue, convertScope.invoke(Location.NONE,
                                convertScope.constructor(keyListValue.getType())));
                        // for each item, extract the property into the list
                        loop.exec(loop.invokeExact(Location.NONE, "add", Collection.class, BaseTypeAdapter.BOOLEAN,
                                keyListValue,
                                // explicit cast to Object so the invokeExact works
                                loop.cast(Location.NONE, AnyTypeWidget.getInstance(),
                                        loop.propertyValue(Location.NONE, loop.getItem(), propertyName))));
                    }
                    // run the loop, making and setting all the parallel lists
                    body.exec(convertScope.complete(loop.build()));
                } else {
                    BytecodeExpression key = adapterMethod.addArgument("$key", recordType);
                    // scatter the record in key into all the key values
                    for (Map.Entry<String, AssignableValue> keyEntry : keyArguments.entrySet()) {
                        String propertyName = keyEntry.getKey();
                        AssignableValue keyListValue = keyEntry.getValue();
                        // create the list (above the loop)
                        body.set(Location.NONE, keyListValue, body.cast(keyListValue.getType(),
                                body.propertyValue(Location.NONE, key, propertyName)));
                    }
                }
            }
        }

        private class SelectMethodAdapterBuilder extends IndexedMethodAdapterBuilder {
            private SelectMethodAdapterBuilder(Method method) {
                super("Query", method);
            }

        }

        private class DeleteMethodAdapterBuilder extends IndexedMethodAdapterBuilder {
            private DeleteMethodAdapterBuilder(Method method) {
                super("Delete", method);
            }
        }

        private BytecodeExpression parseDefaultValue(ScopedBuilder body, Method method, String keyName,
                TypeWidget setType, String defaultValue) {
            if (setType.isIterable()) {
                TypeWidget target = setType.getIterableAdapter().getValue();
                List<BytecodeExpression> expr = Lists.newArrayList();
                StringTokenizer tokenizer = new StringTokenizer(defaultValue, ",", false);
                while (tokenizer.hasMoreElements()) {
                    expr.add(parseDefaultValue(body, method, keyName, target, tokenizer.nextToken().trim()));
                }
                return body.list(Location.NONE, expr);
            } else {
                try {
                    switch (setType.getValueCoreType()) {
                    case BOOLEAN:
                        return body.constant(Boolean.valueOf(defaultValue));
                    case INT8:
                        return body.constant(Byte.decode(defaultValue));
                    case INT16:
                        return body.constant(Short.valueOf(defaultValue));
                    case INT32:
                        return body.constant(Integer.valueOf(defaultValue));
                    case INT64:
                    case TIMESTAMP:
                        return body.constant(Long.valueOf(defaultValue));
                    case FLOAT32:
                        return body.constant(Float.valueOf(defaultValue));
                    case FLOAT64:
                        return body.constant(Double.valueOf(defaultValue));
                    case STRING:
                        return body.constant(defaultValue);
                    // TODO: what should this be? as-is? base64? some kind of encoding?
                    //                            case BYTES:
                    //                                return body.constant(defaultValue);
                    default:
                        reportMethodException(method,
                                "Unable to match default value for @Set('%s') @DefaultValue('%s') to type %s",
                                keyName, defaultValue, setType.getTypeName());
                        throw new IllegalArgumentException(); // reachability
                    }
                } catch (NumberFormatException e) {
                    reportMethodException(method, "Unable to parse default argument %s for @Set('%s'): %s",
                            defaultValue, keyName, e.getMessage());
                    throw new IllegalArgumentException(); // reachability
                }
            }
        }

        private class UpdateMethodAdapterBuilder extends IndexedMethodAdapterBuilder {
            private UpdateMethodAdapterBuilder(Method method) {
                super("Update", method);
                dataValues = Maps.newLinkedHashMap();
                defaultValues = Maps.newLinkedHashMap();
                argumentMap = YQLStructType.builder();
            }

            final Map<String, AssignableValue> dataValues;
            final Map<String, BytecodeExpression> defaultValues;
            final YQLStructType.Builder argumentMap;
            TypeWidget updateRecord;
            YQLStructType updateType;

            @Override
            public BytecodeExpression visitSet(Set annotate, DefaultValue defaultValue, ScopedBuilder body,
                    Class<?> parameterType, TypeWidget setType) {
                String keyName = annotate.value();
                if (dataValues.containsKey(keyName)) {
                    reportMethodParameterException("Update", method, "@Set('%s') used multiple times", keyName);
                    throw new IllegalArgumentException(); // unreachable, but satisfies javac reachability analyzer
                }
                dataValues.put(keyName, body.allocate(setType));
                verifyArgumentType("Update", rowType, rowProperties, keyName, setType, "Set", method);
                YQLType type = gambitScope.createYQLType(setType);
                argumentMap.addField(keyName, type, defaultValue != null);
                if (defaultValue != null) {
                    defaultValues.put(keyName,
                            parseDefaultValue(body, method, keyName, setType, defaultValue.value()));
                }
                return dataValues.get(keyName);
            }

            @Override
            public void complete(ObjectBuilder.MethodBuilder adapterMethod, ScopedBuilder body) {
                super.complete(adapterMethod, body);
                // we need to surface optional/required arguments and a suitable struct type to populate the planner
                // so let's make a YQL schema for our argument map
                this.updateType = argumentMap.build();
                // derive a struct type from it
                // that is the type our adapter method takes as an argument
                // it then needs to explode that into the arguments to the method
                updateRecord = body.adapt(updateType);
                BytecodeExpression record = adapterMethod.addArgument("$record", updateRecord);
                // scatter the record in key into all the key values
                for (Map.Entry<String, AssignableValue> keyEntry : dataValues.entrySet()) {
                    String propertyName = keyEntry.getKey();
                    BytecodeExpression defaultValue = defaultValues.get(propertyName);
                    AssignableValue setValue = keyEntry.getValue();
                    if (defaultValue != null) {
                        body.set(Location.NONE, setValue, body.cast(setValue.getType(), body.coalesce(Location.NONE,
                                body.propertyValue(Location.NONE, record, propertyName), defaultValue)));
                    } else {
                        body.set(Location.NONE, setValue, body.cast(setValue.getType(),
                                body.propertyValue(Location.NONE, record, propertyName)));
                    }
                }
            }
        }

        private class InsertMethodAdapterBuilder implements SourceMethodBuilder {
            final Map<String, AssignableValue> dataValues;
            final Map<String, BytecodeExpression> defaultValues;
            final YQLStructType.Builder argumentMap;

            private final Method method;
            ScopedBuilder body;
            boolean singleton;
            boolean async;
            TypeWidget rowType;
            PropertyAdapter rowProperties;

            YQLStructType insertType;

            public InsertMethodAdapterBuilder(Method method) {
                this.method = method;
                dataValues = Maps.newLinkedHashMap();
                defaultValues = Maps.newLinkedHashMap();
                argumentMap = YQLStructType.builder();
            }

            @Override
            public void begin(ScopedBuilder body, TypeWidget rowType, boolean singleton, boolean async) {
                this.body = body;
                this.rowType = rowType;
                if (!rowType.hasProperties()) {
                    reportMethodException(method, "method return type has no properties (is not a struct): %s",
                            rowType.getTypeName());
                }
                rowProperties = rowType.getPropertyAdapter();
                this.async = async;
                this.singleton = singleton;
            }

            @Override
            public BytecodeExpression visitKeyArgument(Key key, ScopedBuilder body, Class<?> parameterType,
                    TypeWidget parameterWidget) {
                reportMethodParameterException("Insert", method,
                        "@Key parameters are not permitted on @Insert methods");
                throw new IllegalArgumentException();
            }

            @Override
            public BytecodeExpression visitCompoundKey(CompoundKey compoundKey, ScopedBuilder body,
                    Class<?> parameterType, TypeWidget parameterWidget) {
                reportMethodParameterException("Insert", method,
                        "@CompoundKey parameters are not permitted on @Insert methods");
                throw new IllegalArgumentException();
            }

            @Override
            public BytecodeExpression visitSet(Set annotate, DefaultValue defaultValue, ScopedBuilder body,
                    Class<?> parameterType, TypeWidget setType) {
                String keyName = annotate.value();
                if (dataValues.containsKey(keyName)) {
                    reportMethodParameterException("Insert", method, "@Set('%s') used multiple times", keyName);
                    throw new IllegalArgumentException(); // unreachable, but satisfies javac reachability analyzer
                }
                dataValues.put(keyName, body.allocate(setType));
                verifyArgumentType("Insert", rowType, rowProperties, keyName, setType, "Set", method);
                YQLType type = gambitScope.createYQLType(setType);
                argumentMap.addField(keyName, type, defaultValue != null);
                if (defaultValue != null) {
                    defaultValues.put(keyName,
                            parseDefaultValue(body, method, keyName, setType, defaultValue.value()));
                }
                return dataValues.get(keyName);
            }

            @Override
            public void complete(ObjectBuilder.MethodBuilder adapterMethod, ScopedBuilder body) {
                // we need to surface optional/required arguments and a suitable struct type to populate the planner
                // so let's make a YQL schema for our argument map
                this.insertType = argumentMap.build();
                // derive a struct type from it
                // that is the type our adapter method takes as an argument
                // it then needs to explode that into the arguments to the method
                TypeWidget insertRecord = AnyTypeWidget.getInstance();
                BytecodeExpression record = adapterMethod.addArgument("$record", insertRecord);
                // scatter the record in key into all the key values
                // TODO: check data types before blind coercion attempts!
                for (Map.Entry<String, AssignableValue> keyEntry : dataValues.entrySet()) {
                    String propertyName = keyEntry.getKey();
                    BytecodeExpression defaultValue = defaultValues.get(propertyName);
                    AssignableValue setValue = keyEntry.getValue();
                    if (defaultValue == null) {
                        defaultValue = new MissingRequiredFieldExpr(clazz.getName(), this.method.getName(),
                                propertyName, setValue.getType());
                    }
                    body.set(Location.NONE, setValue, body.cast(setValue.getType(), body.coalesce(Location.NONE,
                            body.propertyValue(Location.NONE, record, propertyName), defaultValue)));
                }
                // verify we didn't get any unrecognized fields
                if (insertType.isClosed()) {
                    PropertyAdapter adapter = insertRecord.getPropertyAdapter();
                    TreeSet<String> fieldNames = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
                    for (String name : Iterables.transform(insertType.getFields(),
                            new Function<YQLNamePair, String>() {
                                @Nullable
                                @Override
                                public String apply(YQLNamePair input) {
                                    return input.getName();
                                }
                            })) {
                        fieldNames.add(name);
                    }
                    body.exec(
                            adapter.mergeIntoFieldWriter(record,
                                    body.constant(body.adapt(FieldWriter.class, false),
                                            new VerifyNoExtraFieldsFieldWriter(fieldNames, clazz.getName(),
                                                    this.method.getName()))));
                }

            }
        }
    }

    /**
     * Transform a Source provider into an implementation of IndexedTable and/or IndexedTableFunction.
     *
     * @param input
     * @return
     */
    public SourceType apply(List<String> path, Provider<? extends Source> input) {
        String sourceName = Joiner.on(".").join(path);
        Source source = input.get();
        final Class<? extends Source> clazz = source.getClass();
        // first step, extract the "contract" of the Source
        //   exported row type
        //   is it a function, a table, or both (does it have @Query methods with/without free arguments)
        //     if it's a function, each signature will produce a table
        //     each table will have zero or more indexes, and zero or one SCAN operation
        long minimumBudget = -1;
        long maximumBudget = -1;
        TimeoutBudget budget = clazz.getAnnotation(TimeoutBudget.class);
        if (budget != null) {
            minimumBudget = budget.minimumMilliseconds();
            maximumBudget = budget.maximumMilliseconds();
        }
        // we're going to compile all TableFunction @Query methods into methods with the signature
        Map<Integer, AdapterBuilder> generators = Maps.newHashMap();
        final BytecodeExpression providerConstant = gambitScope.constant(input);
        for (Method method : clazz.getMethods()) {
            Query select = method.getAnnotation(Query.class);
            Insert insert = method.getAnnotation(Insert.class);
            Update update = method.getAnnotation(Update.class);
            Delete delete = method.getAnnotation(Delete.class);
            AdapterBuilder builder;
            if (select != null || insert != null || update != null || delete != null) {
                List<Class<?>> argTypes = Lists.newArrayList();
                Class<?>[] argumentTypes = method.getParameterTypes();
                Annotation[][] annotations = method.getParameterAnnotations();
                for (int i = 0; i < argumentTypes.length; ++i) {
                    if (isFreeArgument(argumentTypes[i], annotations[i])) {
                        argTypes.add(argumentTypes[i]);
                    }
                }
                builder = generators.get(argTypes.size());
                if (builder == null) {
                    builder = new AdapterBuilder(sourceName, clazz, providerConstant, argTypes, minimumBudget,
                            maximumBudget);
                    builder.methods.add(method);
                    generators.put(argTypes.size(), builder);
                } else if (!argTypes.equals(builder.signature)) {
                    throw new ProgramCompileException(
                            "@{Query|Insert|Update|Delete} methods may not be overloaded within the same source (method %s::%s argument signature differs from %s::%s by types only); they must be different in argument count if they differ in argument length",
                            method.getDeclaringClass().getName(), method.getName(),
                            builder.methods.get(0).getDeclaringClass().getName(), builder.methods.get(0).getName());
                } else {
                    builder.methods.add(method);
                }
            } else {
                // not a source method!
                continue;
            }
            if (select != null) {
                builder.addSelectMethod(method);
            } else if (insert != null) {
                builder.addInsertMethod(method);
            } else if (update != null) {
                builder.addUpdateMethod(method);
            } else if (delete != null) {
                builder.addDeleteMethod(method);
            } else {
                throw new IllegalStateException(
                        "This should be impossible (matched step 1 of source method matching but not step 2");
            }
        }

        // generate the final source type, which may need to dispatch
        if (generators.size() == 1) {
            return generators.values().iterator().next().create();
        } else {
            Map<Integer, SourceType> types = Maps.newHashMap();
            for (Map.Entry<Integer, AdapterBuilder> e : generators.entrySet()) {
                types.put(e.getKey(), e.getValue().create());
            }
            return new DispatchSourceTypeAdapter(types);
        }

    }
}