From d533eb45e53d6671ed1245fefc812e408ae3c335 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Tue, 14 Jan 2025 17:36:20 -0800 Subject: [PATCH] Add a Table.hasPrimaryKey method. --- src/main/java/org/cojen/tupl/Table.java | 6 ++ .../cojen/tupl/remote/ClientDerivedTable.java | 5 ++ .../org/cojen/tupl/remote/ClientTable.java | 5 ++ .../org/cojen/tupl/remote/RemoteTable.java | 3 + .../org/cojen/tupl/remote/ServerTable.java | 5 ++ .../cojen/tupl/table/AbstractMappedTable.java | 84 ++++++++++++++++--- .../org/cojen/tupl/table/AggregatedTable.java | 6 ++ .../org/cojen/tupl/table/ConcatTable.java | 5 ++ .../org/cojen/tupl/table/ConvertUtils.java | 3 +- .../java/org/cojen/tupl/table/EmptyTable.java | 5 ++ .../org/cojen/tupl/table/GroupedTable.java | 2 + .../cojen/tupl/table/JoinIdentityTable.java | 5 ++ .../org/cojen/tupl/table/MappedTable.java | 4 +- .../org/cojen/tupl/table/StoredTable.java | 5 ++ .../cojen/tupl/table/StoredTableIndex.java | 5 ++ .../org/cojen/tupl/table/ViewedTable.java | 20 +++++ .../org/cojen/tupl/table/join/JoinTable.java | 5 ++ .../java/org/cojen/tupl/table/ConcatTest.java | 15 +++- .../org/cojen/tupl/table/GroupedTest.java | 1 + .../java/org/cojen/tupl/table/MappedTest.java | 65 ++++++++++++++ 20 files changed, 240 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/cojen/tupl/Table.java b/src/main/java/org/cojen/tupl/Table.java index 54214e55c..aa977cfb7 100644 --- a/src/main/java/org/cojen/tupl/Table.java +++ b/src/main/java/org/cojen/tupl/Table.java @@ -111,6 +111,12 @@ * @see PrimaryKey */ public interface Table 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. */ diff --git a/src/main/java/org/cojen/tupl/remote/ClientDerivedTable.java b/src/main/java/org/cojen/tupl/remote/ClientDerivedTable.java index 4aeff27a7..90e095e15 100644 --- a/src/main/java/org/cojen/tupl/remote/ClientDerivedTable.java +++ b/src/main/java/org/cojen/tupl/remote/ClientDerivedTable.java @@ -158,6 +158,11 @@ private ClientTable newBroken(ClientTable toReplace, Throwable cause) return new ClientTable(toReplace.mDb, broken, toReplace.mType); } + @Override + public boolean hasPrimaryKey() { + return derived().hasPrimaryKey(); + } + @Override public Class rowType() { return derived().rowType(); diff --git a/src/main/java/org/cojen/tupl/remote/ClientTable.java b/src/main/java/org/cojen/tupl/remote/ClientTable.java index 932484531..a532c5f84 100644 --- a/src/main/java/org/cojen/tupl/remote/ClientTable.java +++ b/src/main/java/org/cojen/tupl/remote/ClientTable.java @@ -80,6 +80,11 @@ final class ClientTable implements Table { mHelper = ClientTableHelper.find(type); } + @Override + public boolean hasPrimaryKey() { + return mRemote.hasPrimaryKey(); + } + @Override public Class rowType() { return mType; diff --git a/src/main/java/org/cojen/tupl/remote/RemoteTable.java b/src/main/java/org/cojen/tupl/remote/RemoteTable.java index 2b86df072..da3dd9744 100644 --- a/src/main/java/org/cojen/tupl/remote/RemoteTable.java +++ b/src/main/java/org/cojen/tupl/remote/RemoteTable.java @@ -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) diff --git a/src/main/java/org/cojen/tupl/remote/ServerTable.java b/src/main/java/org/cojen/tupl/remote/ServerTable.java index 33b8f14cf..5f0566cbf 100644 --- a/src/main/java/org/cojen/tupl/remote/ServerTable.java +++ b/src/main/java/org/cojen/tupl/remote/ServerTable.java @@ -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 { diff --git a/src/main/java/org/cojen/tupl/table/AbstractMappedTable.java b/src/main/java/org/cojen/tupl/table/AbstractMappedTable.java index 930bc5304..14e93eecf 100644 --- a/src/main/java/org/cojen/tupl/table/AbstractMappedTable.java +++ b/src/main/java/org/cojen/tupl/table/AbstractMappedTable.java @@ -17,6 +17,8 @@ package org.cojen.tupl.table; +import java.lang.invoke.MethodHandles; + import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -52,6 +54,67 @@ * @author Brian S. O'Neill */ public abstract class AbstractMappedTable extends WrappedTable { + /** + * 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 source) { super(source); } @@ -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(); @@ -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 mSourceColumns; final TreeMap mAllMethods; - InverseFinder(Map sourceColumns) { + /** + * @param inverseFunctions the class which contains static inverse functions + */ + InverseFinder(Map 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 */ @@ -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; diff --git a/src/main/java/org/cojen/tupl/table/AggregatedTable.java b/src/main/java/org/cojen/tupl/table/AggregatedTable.java index 3f2faf5b4..41d59c2bd 100644 --- a/src/main/java/org/cojen/tupl/table/AggregatedTable.java +++ b/src/main/java/org/cojen/tupl/table/AggregatedTable.java @@ -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 diff --git a/src/main/java/org/cojen/tupl/table/ConcatTable.java b/src/main/java/org/cojen/tupl/table/ConcatTable.java index 22a48fd43..dca40b8cf 100644 --- a/src/main/java/org/cojen/tupl/table/ConcatTable.java +++ b/src/main/java/org/cojen/tupl/table/ConcatTable.java @@ -122,6 +122,11 @@ private ConcatTable(Table[] sources) { mSources = sources; } + @Override + public boolean hasPrimaryKey() { + return false; + } + @Override public Class rowType() { return mSources[0].rowType(); diff --git a/src/main/java/org/cojen/tupl/table/ConvertUtils.java b/src/main/java/org/cojen/tupl/table/ConvertUtils.java index b328fb13c..ac7ac3915 100644 --- a/src/main/java/org/cojen/tupl/table/ConvertUtils.java +++ b/src/main/java/org/cojen/tupl/table/ConvertUtils.java @@ -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) { diff --git a/src/main/java/org/cojen/tupl/table/EmptyTable.java b/src/main/java/org/cojen/tupl/table/EmptyTable.java index f020e5f9a..05e094175 100644 --- a/src/main/java/org/cojen/tupl/table/EmptyTable.java +++ b/src/main/java/org/cojen/tupl/table/EmptyTable.java @@ -45,6 +45,11 @@ private EmptyTable(Table empty) { mEmpty = empty; } + @Override + public boolean hasPrimaryKey() { + return mEmpty.hasPrimaryKey(); + } + @Override public Class rowType() { return mEmpty.rowType(); diff --git a/src/main/java/org/cojen/tupl/table/GroupedTable.java b/src/main/java/org/cojen/tupl/table/GroupedTable.java index 8fe0b883b..c1522f689 100644 --- a/src/main/java/org/cojen/tupl/table/GroupedTable.java +++ b/src/main/java/org/cojen/tupl/table/GroupedTable.java @@ -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(); diff --git a/src/main/java/org/cojen/tupl/table/JoinIdentityTable.java b/src/main/java/org/cojen/tupl/table/JoinIdentityTable.java index 965ebfc4e..4134ec832 100644 --- a/src/main/java/org/cojen/tupl/table/JoinIdentityTable.java +++ b/src/main/java/org/cojen/tupl/table/JoinIdentityTable.java @@ -74,6 +74,11 @@ public final class JoinIdentityTable extends BaseTable implements Query rowType() { return Row.class; diff --git a/src/main/java/org/cojen/tupl/table/MappedTable.java b/src/main/java/org/cojen/tupl/table/MappedTable.java index 7200f67b7..45170cbab 100644 --- a/src/main/java/org/cojen/tupl/table/MappedTable.java +++ b/src/main/java/org/cojen/tupl/table/MappedTable.java @@ -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(); @@ -435,7 +437,7 @@ private InverseMapper 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>(); diff --git a/src/main/java/org/cojen/tupl/table/StoredTable.java b/src/main/java/org/cojen/tupl/table/StoredTable.java index 6b2f0a894..cb7eb532e 100644 --- a/src/main/java/org/cojen/tupl/table/StoredTable.java +++ b/src/main/java/org/cojen/tupl/table/StoredTable.java @@ -129,6 +129,11 @@ public final TableManager tableManager() { return mTableManager; } + @Override + public boolean hasPrimaryKey() { + return true; + } + @Override public final Scanner newScanner(R row, Transaction txn) throws IOException { return newScanner(row, txn, unfiltered()); diff --git a/src/main/java/org/cojen/tupl/table/StoredTableIndex.java b/src/main/java/org/cojen/tupl/table/StoredTableIndex.java index 80f65eb5f..33da33a76 100644 --- a/src/main/java/org/cojen/tupl/table/StoredTableIndex.java +++ b/src/main/java/org/cojen/tupl/table/StoredTableIndex.java @@ -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(); diff --git a/src/main/java/org/cojen/tupl/table/ViewedTable.java b/src/main/java/org/cojen/tupl/table/ViewedTable.java index b5a7d663a..2a3c7e0a3 100644 --- a/src/main/java/org/cojen/tupl/table/ViewedTable.java +++ b/src/main/java/org/cojen/tupl/table/ViewedTable.java @@ -965,6 +965,11 @@ protected Updater applyChecks(Updater 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); @@ -1021,6 +1026,11 @@ private static sealed class HasFilter extends ViewedTable { super(queryStr, queryRef, maxArg, source, args); } + @Override + public boolean hasPrimaryKey() { + return mSource.hasPrimaryKey(); + } + @Override public boolean tryLoad(Transaction txn, R row) throws IOException { Helper helper = helper(); @@ -1194,6 +1204,11 @@ private static final class NoPrimaryKeyAndNoFilter 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. @@ -1248,6 +1263,11 @@ private static final class NoPrimaryKeyAndHasFilter 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. diff --git a/src/main/java/org/cojen/tupl/table/join/JoinTable.java b/src/main/java/org/cojen/tupl/table/join/JoinTable.java index d7eb4d286..cb98a37b7 100644 --- a/src/main/java/org/cojen/tupl/table/join/JoinTable.java +++ b/src/main/java/org/cojen/tupl/table/join/JoinTable.java @@ -72,6 +72,11 @@ protected final QueryLauncher scannerQueryLauncher(String query) { } } + @Override + public final boolean hasPrimaryKey() { + return false; + } + @Override public final Scanner newScanner(J row, Transaction txn) throws IOException { return newScanner(row, txn, "{*}", (Object[]) null); diff --git a/src/test/java/org/cojen/tupl/table/ConcatTest.java b/src/test/java/org/cojen/tupl/table/ConcatTest.java index 4d42bb36c..6f948b0b9 100644 --- a/src/test/java/org/cojen/tupl/table/ConcatTest.java +++ b/src/test/java/org/cojen/tupl/table/ConcatTest.java @@ -89,6 +89,8 @@ private void basic(boolean autoType) throws Exception { concat = Table.concat(mTable1, mTable2); } + assertFalse(concat.hasPrimaryKey()); + Query query; String plan, expect; @@ -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 empty = Table.concat(); + concatNone(Table.concat()); + } + + private void concatNone(Table empty) throws Exception { + assertFalse(empty.hasPrimaryKey()); assertTrue(empty.isEmpty()); - Row r = empty.newRow(); + R r = empty.newRow(); try { empty.store(null, r); fail(); diff --git a/src/test/java/org/cojen/tupl/table/GroupedTest.java b/src/test/java/org/cojen/tupl/table/GroupedTest.java index 88cbf0d4f..e25a6ef05 100644 --- a/src/test/java/org/cojen/tupl/table/GroupedTest.java +++ b/src/test/java/org/cojen/tupl/table/GroupedTest.java @@ -274,6 +274,7 @@ public void basic() throws Exception { Table 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)); diff --git a/src/test/java/org/cojen/tupl/table/MappedTest.java b/src/test/java/org/cojen/tupl/table/MappedTest.java index 602a982fd..025171869 100644 --- a/src/test/java/org/cojen/tupl/table/MappedTest.java +++ b/src/test/java/org/cojen/tupl/table/MappedTest.java @@ -85,6 +85,7 @@ public void noInverse() throws Exception { return target; }); + assertFalse(mapped.hasPrimaryKey()); assertTrue(mapped.isEmpty()); TestRow row = mTable.newRow(); @@ -271,6 +272,7 @@ public void withInverse() throws Exception { // Swap the str and num columns. Table mapped = mTable.map(TestRow.class, new Swapper()); + assertTrue(mapped.hasPrimaryKey()); assertTrue(mapped.isEmpty()); TestRow row = mTable.newRow(); @@ -625,6 +627,8 @@ public static int str_to_num(String str) { public void rename() throws Exception { Table mapped = mTable.map(Renamed.class, new Renamer()); + assertFalse(mapped.hasPrimaryKey()); + Renamed row = mapped.newRow(); row.identifier(1); row.string("hello"); @@ -840,6 +844,8 @@ public void brokenMapping() throws Exception { public void conversionMapping() throws Exception { Table mapped = mTable.map(TestRow2.class); + assertFalse(mapped.hasPrimaryKey()); + { TestRow row = mTable.newRow(); @@ -984,6 +990,8 @@ public interface TestRow2 { public void conversionMappingDroppedSourceColumn() throws Exception { Table mapped = mTable.map(TestRow3.class); + assertFalse(mapped.hasPrimaryKey()); + TestRow row = mTable.newRow(); row.id(1); row.str("hello"); @@ -1030,4 +1038,61 @@ public interface TestRow3 { String str(); void str(String str); } + + @Test + public void pkTransformed1() throws Exception { + // Target primary key column is transformed. + Table mapped = mTable.map(Mapped1.class, new Mapper1()); + assertFalse(mapped.hasPrimaryKey()); + } + + @Test + public void pkTransformed1_1() throws Exception { + // Target primary key column has a potential lossy conversion. + Table mapped = mTable.map(Mapped1.class, new Mapper1_1()); + assertFalse(mapped.hasPrimaryKey()); + } + + public static class Mapper1 implements Mapper { + @Override + public Mapped1 map(TestRow source, Mapped1 target) { + target.id((int) source.id()); + target.str(source.str()); + target.num(source.num()); + return target; + } + + public static long id_to_id(int id) { + return id; + } + + @Untransformed + public static String str_to_str(String str) { + return str; + } + + @Untransformed + public static int num_to_num(int num) { + return num; + } + } + + public static class Mapper1_1 extends Mapper1 { + @Untransformed + public static long id_to_id(int id) { + return id; + } + } + + @PrimaryKey("id") + public interface Mapped1 { + int id(); + void id(int id); + + String str(); + void str(String str); + + int num(); + void num(int num); + } }