Skip to content

Commit

Permalink
Add a Table.hasPrimaryKey method.
Browse files Browse the repository at this point in the history
  • Loading branch information
broneill committed Jan 15, 2025
1 parent 0cf4140 commit d533eb4
Show file tree
Hide file tree
Showing 20 changed files with 240 additions and 14 deletions.
6 changes: 6 additions & 0 deletions src/main/java/org/cojen/tupl/Table.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
* @see PrimaryKey
*/
public interface Table<R> extends Closeable {
/**
* Returns true if this table has a primary key defined, as specified by the {@link rowType
* row type}.
*/
public boolean hasPrimaryKey();

/**
* Returns the interface which defines the rows of this table.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/remote/ClientDerivedTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ private ClientTable<Row> newBroken(ClientTable<Row> toReplace, Throwable cause)
return new ClientTable<Row>(toReplace.mDb, broken, toReplace.mType);
}

@Override
public boolean hasPrimaryKey() {
return derived().hasPrimaryKey();
}

@Override
public Class<Row> rowType() {
return derived().rowType();
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/remote/ClientTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ final class ClientTable<R> implements Table<R> {
mHelper = ClientTableHelper.find(type);
}

@Override
public boolean hasPrimaryKey() {
return mRemote.hasPrimaryKey();
}

@Override
public Class<R> rowType() {
return mType;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/cojen/tupl/remote/RemoteTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
*/
@AutoDispose
public interface RemoteTable extends Remote, Disposable {
@RemoteFailure(declared=false)
public boolean hasPrimaryKey();

public Pipe newScanner(RemoteTransaction txn, Pipe pipe) throws IOException;

public Pipe newScanner(RemoteTransaction txn, Pipe pipe, String query, Object... args)
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/remote/ServerTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ protected RemoteTableProxy newValue(byte[] descriptor, Object unused) {
};
}

@Override
public boolean hasPrimaryKey() {
return mTable.hasPrimaryKey();
}

@Override
public Pipe newScanner(RemoteTransaction txn, Pipe pipe) throws IOException {
try {
Expand Down
84 changes: 74 additions & 10 deletions src/main/java/org/cojen/tupl/table/AbstractMappedTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package org.cojen.tupl.table;

import java.lang.invoke.MethodHandles;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

Expand Down Expand Up @@ -52,6 +54,67 @@
* @author Brian S. O'Neill
*/
public abstract class AbstractMappedTable<S, T> extends WrappedTable<S, T> {
/**
* The primary key columns of the target row type must have untransformed inverse mappings,
* and the target column types must support the corresponding source types with no loss.
*
* @param inverseFunctions the class which contains static inverse functions
*/
protected static boolean hasPrimaryKey(Class<?> sourceType, Class<?> targetType,
Class<?> inverseFunctions)
{
RowInfo targetInfo = RowInfo.find(targetType);

if (targetInfo.keyColumns.isEmpty()) {
return false;
}

RowInfo sourceInfo = RowInfo.find(sourceType);
var finder = new InverseFinder(sourceInfo.allColumns, inverseFunctions);

for (ColumnInfo targetColumn : targetInfo.keyColumns.values()) {
ColumnFunction sourceFun = finder.tryFindSource(targetColumn, true);

if (sourceFun == null) {
// Target primary key column doesn't map to a source column.
return false;
}

if (!sourceFun.isUntransformed()) {
// Target primary key column is transformed.
return false;
}

ColumnInfo commonType = ConvertUtils.commonType
(targetColumn, sourceFun.column(), ColumnFilter.OP_EQ);

if (commonType == null || commonType.type != targetColumn.type) {
// Target primary key column has a potential lossy conversion.
return false;
}
}

return true;
}

// Condy bootstrap method.
protected static boolean hasPrimaryKey(MethodHandles.Lookup lookup, String name, Class type,
Class<?> sourceType, Class<?> targetType,
Class<?> inverseFunctions)
{
return hasPrimaryKey(sourceType, targetType, inverseFunctions);
}

protected static void addHasPrimaryKeyMethod(ClassMaker cm,
Class<?> sourceType, Class<?> targetType,
Class<?> inverseFunctions)
{
MethodMaker mm = cm.addMethod(boolean.class, "hasPrimaryKey").public_();
var bootstrap = mm.var(AbstractMappedTable.class)
.condy("hasPrimaryKey", sourceType, targetType, inverseFunctions);
mm.return_(bootstrap.invoke(boolean.class, "_"));
}

protected AbstractMappedTable(Table<S> source) {
super(source);
}
Expand Down Expand Up @@ -88,7 +151,7 @@ private record ArgMapper(int targetArgNum, Method function) { }

Splitter(QuerySpec targetQuery) {
RowInfo sourceInfo = RowInfo.find(mSource.rowType());
var finder = new InverseFinder(sourceInfo.allColumns);
var finder = new InverseFinder(sourceInfo.allColumns, inverseFunctions());

RowFilter targetFilter = targetQuery.filter();

Expand Down Expand Up @@ -241,21 +304,26 @@ protected static class SortPlan {
/**
* Finds inverse mapping functions defined in a Mapper or GrouperFactory implementation.
*/
protected class InverseFinder {
protected static class InverseFinder {
final Map<String, ColumnInfo> mSourceColumns;
final TreeMap<String, Method> mAllMethods;

InverseFinder(Map<String, ColumnInfo> sourceColumns) {
/**
* @param inverseFunctions the class which contains static inverse functions
*/
InverseFinder(Map<String, ColumnInfo> sourceColumns, Class<?> inverseFunctions) {
mSourceColumns = sourceColumns;

mAllMethods = new TreeMap<>();
for (Method m : inverseFunctions().getMethods()) {
mAllMethods.put(m.getName(), m);
for (Method m : inverseFunctions.getMethods()) {
if (Modifier.isStatic(m.getModifiers())) {
mAllMethods.put(m.getName(), m);
}
}
}

/**
* @param requie pass false to allow mappings of the form "target_to_" which don't
* @param require pass false to allow mappings of the form "target_to_" which don't
* specify a source column; the ColumnFunction.column field will be null and the
* function returns void
*/
Expand All @@ -268,10 +336,6 @@ ColumnFunction tryFindSource(ColumnInfo targetColumn, boolean require) {
break;
}

if (!Modifier.isStatic(candidate.getModifiers())) {
continue;
}

Class<?>[] paramTypes = candidate.getParameterTypes();
if (paramTypes.length != 1) {
continue;
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/cojen/tupl/table/AggregatedTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ private static MethodHandle makeTableFactory(FactoryKey key, Table<?> source) {
ctor.invokeSuperConstructor(ctor.field("cache"), ctor.param(1), ctor.param(2));
}

// Add the hasPrimaryKey method.
{
MethodMaker mm = cm.addMethod(boolean.class, "hasPrimaryKey").public_();
mm.return_(!targetInfo.keyColumns.isEmpty());
}

// Add the compareSourceRows method.
{
MethodMaker mm = cm.addMethod
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/table/ConcatTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ private ConcatTable(Table<R>[] sources) {
mSources = sources;
}

@Override
public boolean hasPrimaryKey() {
return false;
}

@Override
public Class<R> rowType() {
return mSources[0].rowType();
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/cojen/tupl/table/ConvertUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public static Variable convertArray(MethodMaker mm, Class toType, Variable lengt
* Finds a common type which two columns can be converted to without loss or abiguity. The
* name of the returned ColumnInfo is undefined (it might be null).
*
* @param op defined in ColumnFilter; pass -1 if not performing a comparison operation
* @param op defined in ColumnFilter; pass -1 if not performing a comparison operation;
* pass OP_EQ to use a lenient rule which doesn't care if a number converts to a string
* @return null if a common type cannot be inferred or is ambiguous
*/
public static ColumnInfo commonType(ColumnInfo aInfo, ColumnInfo bInfo, int op) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/table/EmptyTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ private EmptyTable(Table<R> empty) {
mEmpty = empty;
}

@Override
public boolean hasPrimaryKey() {
return mEmpty.hasPrimaryKey();
}

@Override
public Class<R> rowType() {
return mEmpty.rowType();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/cojen/tupl/table/GroupedTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ private static MethodHandle makeTableFactory(FactoryKey key, Table<?> source) {
(ctor.field("cache"), ctor.param(1), groupBySpec, orderBySpec, ctor.param(2));
}

addHasPrimaryKeyMethod(cm, sourceType, targetType, key.factoryClass());

MethodHandles.Lookup lookup = cm.finishLookup();
Class<?> tableClass = lookup.lookupClass();

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/table/JoinIdentityTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public final class JoinIdentityTable extends BaseTable<Row> implements Query<Row
private JoinIdentityTable() {
}

@Override
public boolean hasPrimaryKey() {
return false;
}

@Override
public Class<Row> rowType() {
return Row.class;
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/cojen/tupl/table/MappedTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ private static MethodHandle makeTableFactory(FactoryKey key) {
ctor.invokeSuperConstructor(ctor.field("cache"), ctor.param(1), ctor.param(2));
}

addHasPrimaryKeyMethod(tableMaker, key.sourceType(), targetType, key.mapperClass());

addMarkValuesUnset(key, info, tableMaker);

MethodHandles.Lookup lookup = tableMaker.finishLookup();
Expand Down Expand Up @@ -435,7 +437,7 @@ private InverseMapper<S, T> makeInverseMapper(int mode) {
sourceColumns = sourceInfo.allColumns;
}

var finder = new InverseFinder(sourceColumns);
var finder = new InverseFinder(sourceColumns, inverseFunctions());

// Maps source columns to the targets that map to it.
var toTargetMap = new LinkedHashMap<String, Set<ColumnFunction>>();
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/table/StoredTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ public final TableManager<R> tableManager() {
return mTableManager;
}

@Override
public boolean hasPrimaryKey() {
return true;
}

@Override
public final Scanner<R> newScanner(R row, Transaction txn) throws IOException {
return newScanner(row, txn, unfiltered());
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/table/StoredTableIndex.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ final boolean supportsSecondaries() {
return false;
}

@Override
public final boolean hasPrimaryKey() {
return false;
}

@Override
public final void store(Transaction txn, R row) throws IOException {
throw new UnmodifiableViewException();
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/cojen/tupl/table/ViewedTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,11 @@ protected Updater<R> applyChecks(Updater<R> updater) {
return updater;
}

@Override
public boolean hasPrimaryKey() {
return mSource.hasPrimaryKey();
}

@Override
public boolean tryLoad(Transaction txn, R row) throws IOException {
return mSource.tryLoad(txn, row);
Expand Down Expand Up @@ -1021,6 +1026,11 @@ private static sealed class HasFilter<R> extends ViewedTable<R> {
super(queryStr, queryRef, maxArg, source, args);
}

@Override
public boolean hasPrimaryKey() {
return mSource.hasPrimaryKey();
}

@Override
public boolean tryLoad(Transaction txn, R row) throws IOException {
Helper<R> helper = helper();
Expand Down Expand Up @@ -1194,6 +1204,11 @@ private static final class NoPrimaryKeyAndNoFilter<R> extends HasProjectionAndNo
super(queryStr, queryRef, maxArg, source, args);
}

@Override
public boolean hasPrimaryKey() {
return false;
}

@Override
public boolean tryLoad(Transaction txn, R row) throws IOException {
// Requires primary key columns.
Expand Down Expand Up @@ -1248,6 +1263,11 @@ private static final class NoPrimaryKeyAndHasFilter<R> extends HasProjectionAndH
super(queryStr, queryRef, maxArg, source, args);
}

@Override
public boolean hasPrimaryKey() {
return false;
}

@Override
public boolean tryLoad(Transaction txn, R row) throws IOException {
// Requires primary key columns.
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/cojen/tupl/table/join/JoinTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ protected final QueryLauncher<J> scannerQueryLauncher(String query) {
}
}

@Override
public final boolean hasPrimaryKey() {
return false;
}

@Override
public final Scanner<J> newScanner(J row, Transaction txn) throws IOException {
return newScanner(row, txn, "{*}", (Object[]) null);
Expand Down
15 changes: 13 additions & 2 deletions src/test/java/org/cojen/tupl/table/ConcatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ private void basic(boolean autoType) throws Exception {
concat = Table.concat(mTable1, mTable2);
}

assertFalse(concat.hasPrimaryKey());

Query<?> query;
String plan, expect;

Expand Down Expand Up @@ -171,11 +173,20 @@ private void basic(boolean autoType) throws Exception {
}
}

@Test
public void concatNone() throws Exception {
concatNone(Table.concat(ConcatRow.class));
}

@Test
public void concatNoneAuto() throws Exception {
Table<Row> empty = Table.concat();
concatNone(Table.concat());
}

private <R> void concatNone(Table<R> empty) throws Exception {
assertFalse(empty.hasPrimaryKey());
assertTrue(empty.isEmpty());
Row r = empty.newRow();
R r = empty.newRow();
try {
empty.store(null, r);
fail();
Expand Down
1 change: 1 addition & 0 deletions src/test/java/org/cojen/tupl/table/GroupedTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ public void basic() throws Exception {
Table<TestRowGroup> grouped = mTable.group
("", "", TestRowGroup.class, new Grouped1Factory(false, true));

assertFalse(grouped.hasPrimaryKey());
assertTrue(grouped.isEmpty());
assertFalse(grouped.anyRows(null));
assertFalse(grouped.anyRows(null, "count == ?", 3));
Expand Down
Loading

0 comments on commit d533eb4

Please sign in to comment.