
io.permazen.core.Database Maven / Gradle / Ivy
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.core;
import com.google.common.base.Preconditions;
import io.permazen.kv.KVDatabase;
import io.permazen.kv.KVPair;
import io.permazen.kv.KVStore;
import io.permazen.kv.KVTransaction;
import io.permazen.kv.KVTransactionException;
import io.permazen.schema.SchemaModel;
import io.permazen.util.ByteReader;
import io.permazen.util.ByteUtil;
import io.permazen.util.CloseableIterator;
import io.permazen.util.Diffs;
import io.permazen.util.UnsignedIntEncoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides an object database abstraction on top of a key/value database.
*
*
* Includes support for:
*
* - Objects and fields defined by a {@link SchemaModel}, with positive schema verification
* - Simple values fields containing any atomic type, reference or custom {@link FieldType}
* - Complex fields of type {@link java.util.List}, {@link java.util.NavigableSet}, and {@link java.util.NavigableMap}
* - Invertable reference fields with strong referential integrity and configurable delete cascading
* - Configurable indexing of any simple field or complex sub-field
* - Composite indexes on multiple simple fields
* - Notification of object creation and deletion
* - Notification of object field changes, as seen through an arbitrary path of references
* - Automatic schema tracking and object versioning with schema change notification support
*
*
*
* See {@link Transaction} for further details on the above functionality.
*
*
* This class defines an abstraction layer that usually sits below a {@link io.permazen.Permazen} but is completely
* independent of {@link io.permazen.Permazen} and can be used on its own.
* Compared to {@link io.permazen.Permazen}, a {@link Database} has these differences:
*
* - A {@link SchemaModel} must be explicitly provided to define the schema in use, whereas when using a
* {@link io.permazen.Permazen} the schema is derived automatically from annotated Java model classes.
* - Object references are represented by {@link ObjId}s instead of Java objects, and there is no notion of object sub-type.
* However, reference fields may be configured with a restricted set of referrable types.
* - All object types and fields are referenced by explicit storage ID.
* - Enum values are represented by {@link EnumValue} objects.
* - There is no automatic validation support.
*
*
* @see Transaction
* @see io.permazen
*/
public class Database {
/**
* The maximum number of fields that may be indexed in a composite index ({@value #MAX_INDEXED_FIELDS}).
*/
// COMPOSITE-INDEX
public static final int MAX_INDEXED_FIELDS = 4;
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final FieldTypeRegistry fieldTypeRegistry = new FieldTypeRegistry();
private final KVDatabase kvdb;
private volatile Schemas lastSchemas;
/**
* Constructor.
*
* @param kvdb the underlying key/value store in which to store information
* @throws IllegalArgumentException if {@code kvdb} is null
*/
public Database(KVDatabase kvdb) {
Preconditions.checkArgument(kvdb != null, "null kvdb");
this.kvdb = kvdb;
}
/**
* Get the {@link FieldTypeRegistry} associated with this instance.
*
* @return field type registry associated with this instance
*/
public FieldTypeRegistry getFieldTypeRegistry() {
return this.fieldTypeRegistry;
}
/**
* Get the {@link KVDatabase} underlying this instance.
*
* @return underlying key/value database
*/
public KVDatabase getKVDatabase() {
return this.kvdb;
}
/**
* Create a new transaction.
*
*
* Convenience method; equivalent to:
*
* createTransaction(schemaModel, version, allowNewSchema, null);
*
*
* @param schemaModel schema to use with the new transaction, or null to use the schema already recorded in the database
* @param version the schema version number corresponding to {@code schemaModel},
* zero to use the highest version already recorded in the database,
* or -1 to use an {@linkplain SchemaModel#autogenerateVersion auto-generated} schema version
* @param allowNewSchema whether creating a new schema version is allowed
* @return newly created transaction
* @throws IllegalArgumentException if {@code version} is less than -1, or equal to -1 when {@code schemaModel} is null
* @throws InvalidSchemaException if {@code schemaModel} is invalid (i.e., does not pass validation checks)
* @throws SchemaMismatchException if {@code schemaModel} does not match schema version {@code version}
* as recorded in the database
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database
* and {@code allowNewSchema} is false
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database,
* {@code allowNewSchema} is true, but {@code schemaModel} is incompatible with one or more other schemas
* already recorded in the database (i.e., the same storage ID is used inconsistently between schema versions)
* @throws SchemaMismatchException
* if the database is uninitialized and {@code version == 0} or {@code schemaModel} is null
* @throws InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database
* @throws InconsistentDatabaseException if an uninitialized database is encountered but the database is not empty
* @throws IllegalStateException if no underlying {@link KVDatabase} has been configured for this instance
*/
public Transaction createTransaction(SchemaModel schemaModel, int version, boolean allowNewSchema) {
return this.createTransaction(schemaModel, version, allowNewSchema, null);
}
/**
* Create a new {@link Transaction} on this database and use the specified schema version to access objects and fields.
*
*
* Schema Versions
*
*
* Within each {@link Database} is stored a record of all schema versions previously used with the database.
* When creating a new transaction, the caller provides an expected schema version and corresponding {@link SchemaModel}.
* Both of these are optional: a schema version of zero means "use the highest version recorded in the
* database", and a null {@link SchemaModel} means "use the {@link SchemaModel} already recorded in the database under
* {@code version}".
*
*
* When this method is invoked, the following checks are applied:
*
* - If a schema with version number {@code version != 0} is recorded in the database, and {@code schemaModel} is null or
* matches it, then this method succeeds, and the {@link Transaction} will use that schema.
* - If a schema with version number {@code version} (or the highest numbered schema if {@code version == 0})
* is recorded in the database, and {@code schemaModel} is not null and does not match it, then this method fails
* and throws a {@link SchemaMismatchException}.
* - If {@code allowNewSchema} is false, and no schema with version number {@code version != 0} has yet been
* recorded in the database, then this method fails and throws a {@link SchemaMismatchException}.
* - If {@code allowNewSchema} is true, and no schema with version number {@code version != 0} has yet been
* recorded in the database, then if {@code schemaModel} is null a {@link SchemaMismatchException} is thrown;
* otherwise {@code schemaModel} is checked for compabitility with the schemas previously recorded in the database;
* if compatible, this method succeeds, {@code schema} is recorded in the database under the new version number
* {@code version}, and the {@link Transaction} will use schema version {@code version};
* otherwise a {@link SchemaMismatchException} is thrown.
* - If the database is uninitialized and {@code version == 0} or {@code schemaModel} is null,
* a {@link SchemaMismatchException} is thrown.
*
*
*
* For two schemas to "match", they must be identical in all respects, except that object, field, and index names may differ.
* In the core API, objects and fields are identified by storage ID, not name.
*
*
* Schemas must also be compatible with all other schemas previously recorded in the database.
* Basically this means storage IDs must be used consistently from a structural point of view:
*
* - Once a storage ID is assigned, it cannot be re-assigned to a different type of item (object or field).
* - Fields must have a consistent type and structural parent (object type or complex field).
*
*
*
* However, object types and fields may be added or removed across schema versions, field indexing may change,
* and reference field {@link DeleteAction}s may change.
*
*
* Object Versions
*
*
* Each object in a {@link Database} contains an internal version number that indicates its current schema version;
* this in turn dictates what fields that object contains.
*
*
* When an object is accessed during a {@link Transaction}, the object's version is compared to the {@code version} associated
* with that {@link Transaction}. If the versions are the same, no version change occurs and fields are accessed normally.
*
*
* If the object has a version {@code oldVersion} different from {@code version}, then depending on which {@link Transaction}
* method is invoked, the object version may be automatically updated to {@code version}. This will cause fields to be added
* or removed, as follows:
*
* - Fields that are common to both schema versions remain unchanged (necessarily such fields have the same storage ID,
* type, and structural parent).
* - Fields that exist in {@code oldVersion} but not in {@code version} are removed.
* - Fields that exist in {@code version} but not in {@code oldVersion} are initialized to their default values.
* - All {@link VersionChangeListener}s registered with the {@link Transaction} are notified.
*
*
*
* Note that compatibility between schema versions does not depend on the field name, nor does it depend on whether the field
* is indexed, or its {@link DeleteAction} (for reference fields). A field's index may be added or removed between schema
* versions without losing information, however, querying a field's index will only return those objects whose schema
* version corresponds to a schema in which the field is indexed. Similarly, the {@link DeleteAction} taken when a
* referenced object is deleted depends on the {@link DeleteAction} configured in the schema version of the object
* containing the reference.
*
*
* Note that an object's current schema version can go up as well as down, may change non-consecutively, and in fact
* nothing requires schema version numbers to be consecutive.
*
* @param schemaModel schema to use with the new transaction, or null to use the schema already recorded in the database
* @param version the schema version number corresponding to {@code schemaModel},
* zero to use the highest version already recorded in the database,
* or -1 to use an {@linkplain SchemaModel#autogenerateVersion auto-generated} schema version
* @param allowNewSchema whether creating a new schema version is allowed
* @param kvoptions optional {@link KVDatabase}-specific transaction options; may be null
* @return newly created transaction
* @throws IllegalArgumentException if {@code version} is less than -1, or equal to -1 when {@code schemaModel} is null
* @throws InvalidSchemaException if {@code schemaModel} is invalid (i.e., does not pass validation checks)
* @throws SchemaMismatchException if {@code schemaModel} does not match schema version {@code version}
* as recorded in the database
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database
* and {@code allowNewSchema} is false
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database,
* {@code allowNewSchema} is true, but {@code schemaModel} is incompatible with one or more other schemas
* already recorded in the database (i.e., the same storage ID is used inconsistently between schema versions)
* @throws SchemaMismatchException
* if the database is uninitialized and {@code version == 0} or {@code schemaModel} is null
* @throws InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database
* @throws InconsistentDatabaseException if an uninitialized database is encountered but the database is not empty
* @throws IllegalStateException if no underlying {@link KVDatabase} has been configured for this instance
*/
public Transaction createTransaction(SchemaModel schemaModel, int version, boolean allowNewSchema, Map kvoptions) {
final KVTransaction kvt = this.kvdb.createTransaction(kvoptions);
boolean success = false;
try {
final Transaction tx = this.createTransaction(kvt, schemaModel, version, allowNewSchema);
success = true;
return tx;
} finally {
if (!success) {
try {
kvt.rollback();
} catch (KVTransactionException e) {
// ignore
}
}
}
}
/**
* Create a new {@link Transaction} on this database using an already-opened {@link KVTransaction} and specified schema.
* The given {@link KVTransaction} will be committed or rolled-back along with the returned {@link Transaction}.
*
*
* See {@link #createTransaction(SchemaModel, int, boolean)} for details on schema and object versions.
*
* @param kvt already opened key/value store transaction
* @param schemaModel schema to use with the new transaction, or null to use the schema already recorded in the database
* @param version the schema version number corresponding to {@code schemaModel},
* zero to use the highest version already recorded in the database,
* or -1 to use an {@linkplain SchemaModel#autogenerateVersion auto-generated} schema version
* @param allowNewSchema whether creating a new schema version is allowed
* @return newly created transaction
* @throws IllegalArgumentException if {@code kvt} is null
* @throws IllegalArgumentException if {@code version} is less than -1, or equal to -1 when {@code schemaModel} is null
* @throws InvalidSchemaException if {@code schemaModel} is invalid (i.e., does not pass validation checks)
* @throws SchemaMismatchException if {@code schemaModel} does not match schema version {@code version}
* as recorded in the database
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database
* and {@code allowNewSchema} is false
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database,
* {@code allowNewSchema} is true, but {@code schemaModel} is incompatible with one or more other schemas
* already recorded in the database (i.e., the same storage ID is used inconsistently between schema versions)
* @throws SchemaMismatchException
* if the database is uninitialized and {@code version == 0} or {@code schemaModel} is null
* @throws InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database
* @throws InconsistentDatabaseException if an uninitialized database is encountered but the database is not empty
* @throws IllegalStateException if no underlying {@link KVDatabase} has been configured for this instance
*/
public Transaction createTransaction(KVTransaction kvt, SchemaModel schemaModel, int version, boolean allowNewSchema) {
// Sanity check
Preconditions.checkArgument(kvt != null, "null kvt");
// Validate meta-data
final Schemas schemas = this.verifySchemas(kvt, schemaModel, version, allowNewSchema);
assert schemas != null;
// Create transaction
return version > 0 ? new Transaction(this, kvt, schemas, version) : new Transaction(this, kvt, schemas);
}
/**
* Create a "snapshot" transaction based on the provided key/value store.
*
*
* The key/value store will be initialized if necessary (i.e., {@code kvstore} may be empty), otherwise it will be
* validated against the schema information associated with this instance.
*
*
* The returned {@link SnapshotTransaction} does not support {@link SnapshotTransaction#commit commit()},
* {@link SnapshotTransaction#rollback rollback()}, or {@link SnapshotTransaction#addCallback addCallback()},
* and can be used indefinitely.
*
* @param kvstore key/value store, empty or having content compatible with this transaction's {@link Database}
* @param schemaModel schema to use with the new transaction, or null to use the schema already recorded in the database
* @param version the schema version number corresponding to {@code schemaModel},
* zero to use the highest version already recorded in the database,
* or -1 to use an {@linkplain SchemaModel#autogenerateVersion auto-generated} schema version
* @param allowNewSchema whether creating a new schema version in {@code kvstore} is allowed
* @return snapshot transaction based on {@code kvstore}
* @throws IllegalArgumentException if {@code version} is less than -1, or equal to -1 when {@code schemaModel} is null
* @throws InvalidSchemaException if {@code schemaModel} is invalid (i.e., does not pass validation checks)
* @throws SchemaMismatchException if {@code schemaModel} does not match schema version {@code version}
* as recorded in the database
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database
* and {@code allowNewSchema} is false
* @throws SchemaMismatchException if schema version {@code version} is not recorded in the database,
* {@code allowNewSchema} is true, but {@code schemaModel} is incompatible with one or more other schemas
* already recorded in the database (i.e., the same storage ID is used inconsistently between schema versions)
* @throws SchemaMismatchException
* if the database is uninitialized and {@code version == 0} or {@code schemaModel} is null
* @throws SchemaMismatchException if {@code kvstore} contains incompatible or missing schema information
* @throws InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database
* @throws InconsistentDatabaseException if an uninitialized database is encountered but the database is not empty
* @throws IllegalArgumentException if {@code kvstore} is null
* @see Transaction#createSnapshotTransaction Transaction.createSnapshotTransaction()
*/
public SnapshotTransaction createSnapshotTransaction(KVStore kvstore,
SchemaModel schemaModel, int version, boolean allowNewSchema) {
// Validate meta-data
final Schemas schemas = this.verifySchemas(kvstore, schemaModel, version, allowNewSchema);
assert schemas != null;
// Create snapshot transaction
return version > 0 ?
new SnapshotTransaction(this, kvstore, schemas, version) : new SnapshotTransaction(this, kvstore, schemas);
}
/**
* Initialize (if necessary) and validate the given {@link KVStore} for use with this database.
*
* @param kvstore key/value store
* @param schemaModel schema to use with the new transaction, or null
* @param version schema version number
* @param allowNewSchema whether creating a new schema version is allowed
*/
private Schemas verifySchemas(KVStore kvstore, SchemaModel schemaModel, int version, boolean allowNewSchema) {
// Sanity check
Preconditions.checkArgument(kvstore != null, "null kvstore");
Preconditions.checkArgument(version >= -1, "invalid schema version: " + version);
Preconditions.checkArgument(schemaModel != null || version >= 0, "can't auto-generate version without schema model");
if (schemaModel != null) {
schemaModel.validate();
if (version == -1)
version = schemaModel.autogenerateVersion();
}
// Debug
if (this.log.isTraceEnabled()) {
this.log.trace("creating transaction using "
+ (version != 0 ? "schema version " + version : "highest recorded schema version"));
}
// Get iterator over meta-data key/value pairs
final int formatVersion;
final boolean uninitialized;
try (final CloseableIterator metaDataIterator = kvstore.getRange(Layout.getMetaDataKeyRange())) {
// Pretend user meta-data is not there
final Predicate userMetaData = key -> ByteUtil.isPrefixOf(Layout.getUserMetaDataKeyPrefix(), key);
// Get format version; it should be first; if not found, database is uninitialized (and should be empty)
byte[] formatVersionBytes = null;
if (metaDataIterator.hasNext()) {
final KVPair pair = metaDataIterator.next();
assert Layout.getMetaDataKeyRange().contains(pair.getKey());
if (Arrays.equals(pair.getKey(), Layout.getFormatVersionKey()))
formatVersionBytes = pair.getValue();
else if (!userMetaData.test(pair.getKey())) {
throw new InconsistentDatabaseException("database is uninitialized but contains unrecognized garbage (key "
+ ByteUtil.toString(pair.getKey()) + ")");
}
}
// Get database format object; check for an uninitialized database
uninitialized = formatVersionBytes == null;
if (uninitialized) {
// Sanity checks
final KVPair first = kvstore.getAtLeast(new byte[0], null);
final KVPair last = kvstore.getAtMost(new byte[] { (byte)0xff }, null);
if (first != null && !userMetaData.test(first.getKey())) {
throw new InconsistentDatabaseException("database is uninitialized but contains unrecognized garbage (key "
+ ByteUtil.toString(first.getKey()) + ")");
}
if (last != null && !userMetaData.test(last.getKey())) {
throw new InconsistentDatabaseException("database is uninitialized but contains unrecognized garbage (key "
+ ByteUtil.toString(last.getKey()) + ")");
}
if ((first != null) != (last != null) || (first != null && ByteUtil.compare(first.getKey(), last.getKey()) > 0))
throw new InconsistentDatabaseException("inconsistent results from getAtLeast() and getAtMost()");
try (CloseableIterator testIterator = kvstore.getRange(new byte[0], new byte[] { (byte)0xff })) {
if (testIterator.hasNext() ?
first == null || !Arrays.equals(testIterator.next().getKey(), first.getKey()) : first != null)
throw new InconsistentDatabaseException("inconsistent results from getAtLeast() and getRange()");
}
try (CloseableIterator testIterator = kvstore.getRange(new byte[0], new byte[] { (byte)0xff }, true)) {
if (testIterator.hasNext() ?
last == null || !Arrays.equals(testIterator.next().getKey(), last.getKey()) : last != null)
throw new InconsistentDatabaseException("inconsistent results from getAtMost() and getRange()");
}
this.checkAddNewSchema(schemaModel, version, allowNewSchema);
// Initialize database
formatVersion = Layout.CURRENT_FORMAT_VERSION;
this.log.debug("detected an uninitialized database; initializing now (format version " + formatVersion + ")");
final byte[] encodedFormatVersion = UnsignedIntEncoder.encode(formatVersion);
kvstore.put(Layout.getFormatVersionKey(), encodedFormatVersion);
// Sanity check again
formatVersionBytes = kvstore.get(Layout.getFormatVersionKey());
if (formatVersionBytes == null || ByteUtil.compare(formatVersionBytes, encodedFormatVersion) != 0)
throw new InconsistentDatabaseException("database failed basic read/write test");
final KVPair lower = kvstore.getAtLeast(new byte[0], null);
if (lower == null || !lower.equals(new KVPair(Layout.getFormatVersionKey(), encodedFormatVersion)))
throw new InconsistentDatabaseException("database failed basic read/write test");
final KVPair upper = kvstore.getAtMost(Layout.getUserMetaDataKeyPrefix(), null);
if (upper == null || !upper.equals(new KVPair(Layout.getFormatVersionKey(), encodedFormatVersion)))
throw new InconsistentDatabaseException("database failed basic read/write test");
} else {
// Read format version
try {
formatVersion = UnsignedIntEncoder.decode(formatVersionBytes);
} catch (IllegalArgumentException e) {
throw new InconsistentDatabaseException("database contains invalid encoded format version "
+ ByteUtil.toString(formatVersionBytes) + " under key " + ByteUtil.toString(Layout.getFormatVersionKey()));
}
// Validate format version
switch (formatVersion) {
case Layout.FORMAT_VERSION_1:
case Layout.FORMAT_VERSION_2:
break;
default:
throw new InconsistentDatabaseException("database contains unrecognized format version "
+ formatVersion + " under key " + ByteUtil.toString(Layout.getFormatVersionKey()));
}
}
// There should not be any other meta data prior to recorded schemas
if (metaDataIterator.hasNext()) {
final KVPair pair = metaDataIterator.next();
if (!Layout.getSchemaKeyRange().contains(pair.getKey())) {
throw new InconsistentDatabaseException("database contains unrecognized garbage at key "
+ ByteUtil.toString(pair.getKey()));
}
}
}
// Check schema
Schemas schemas = null;
for (boolean firstAttempt = true; true; firstAttempt = false) {
// Read recorded database schema versions
final TreeMap bytesMap = new TreeMap<>();
try (CloseableIterator i = kvstore.getRange(Layout.getSchemaKeyRange())) {
while (i.hasNext()) {
final KVPair pair = i.next();
assert Layout.getSchemaKeyRange().contains(pair.getKey());
// Decode schema version and get XML
final int vers = UnsignedIntEncoder.read(new ByteReader(pair.getKey(), Layout.getSchemaKeyPrefix().length));
if (vers == 0)
throw new InconsistentDatabaseException("database contains an invalid schema version zero");
bytesMap.put(vers, pair.getValue());
}
}
// Read and decode database schemas, avoiding rebuild if possible
schemas = this.lastSchemas;
if (schemas != null && !schemas.isSameVersions(bytesMap))
schemas = null;
if (schemas == null) {
try {
schemas = this.decodeSchemas(bytesMap, formatVersion);
} catch (IllegalArgumentException e) {
if (firstAttempt)
throw new InconsistentDatabaseException("database contains invalid schema information", e);
else
throw new InvalidSchemaException("schema is not valid: " + e.getMessage(), e);
}
}
// If no version specified, assume the highest recorded version
if (version == 0 && !bytesMap.isEmpty())
version = bytesMap.lastKey();
// If transaction schema was not found in the database, add it and retry
if (!bytesMap.containsKey(version)) {
// Log it
if (bytesMap.isEmpty()) {
if (!uninitialized)
throw new InconsistentDatabaseException("database is initialized but contains no recorded schema versions");
} else
this.log.debug("schema version " + version + " not found in database; known versions are " + bytesMap.keySet());
// Check whether we can add a new schema version
this.checkAddNewSchema(schemaModel, version, allowNewSchema);
// Record new schema in database
this.log.debug("recording new schema version " + version + " into database");
kvstore.put(Layout.getSchemaKey(version), Layout.encodeSchema(schemaModel, formatVersion));
// Try again
schemas = null;
continue;
}
// Compare transaction schema with the schema of the same version found in the database
if (this.log.isTraceEnabled())
this.log.trace("found schema version " + version + " in database; known versions are " + bytesMap.keySet());
final SchemaModel dbSchemaModel = schemas.getVersion(version).getSchemaModel();
if (schemaModel != null) {
if (!schemaModel.isCompatibleWith(dbSchemaModel)) {
final Diffs diffs = schemaModel.differencesFrom(dbSchemaModel);
this.log.error("schema mismatch:\n=== Database schema ===\n{}\n=== Provided schema ===\n{}"
+ "\n=== Differences ===\n{}", dbSchemaModel, schemaModel, diffs);
throw new IllegalArgumentException("the provided schema is not compatible with the schema already recorded"
+ " in the database under version " + version + ":\n" + diffs);
} else if (this.log.isTraceEnabled() && !schemaModel.equals(dbSchemaModel)) {
final Diffs diffs = schemaModel.differencesFrom(dbSchemaModel);
this.log.trace("the provided schema differs from, but is compatible with, the database schema:\n{}", diffs);
}
}
break;
}
// Save schema for next time
this.lastSchemas = schemas;
// Done
return schemas;
}
/**
* Validate a {@link SchemaModel} against this instance.
*
*
* This is a convenience method, equivalent to: {@link #validateSchema(FieldTypeRegistry, SchemaModel)
* Database.validateSchema}{@code (this.getFieldTypeRegistry(), schemaModel)}.
*
* @param schemaModel schema to validate
* @throws InvalidSchemaException if {@code schemaModel} is invalid
* @throws IllegalArgumentException if {@code schemaModel} is null
*/
public void validateSchema(SchemaModel schemaModel) {
Database.validateSchema(this.getFieldTypeRegistry(), schemaModel);
}
/**
* Validate a {@link SchemaModel}.
*
*
* This method only statically checks the schema; it does not validate the schema against any other
* existing schema versions that may be previously recorded in a database.
*
*
* To validate a schema against a particular database, simply attempt to create a transaction
* via {@link #createTransaction createTransaction()}. To validate that a collection of schemas
* are mutually consistent independently from any database, use {@link #validateSchemas validateSchemas()}.
*
* @param fieldTypeRegistry registry of simple field types
* @param schemaModel schema to validate
* @throws InvalidSchemaException if {@code schemaModel} is invalid
* @throws IllegalArgumentException if either parameter is null
*/
public static void validateSchema(FieldTypeRegistry fieldTypeRegistry, SchemaModel schemaModel) {
// Sanity check
Preconditions.checkArgument(fieldTypeRegistry != null, "null fieldTypeRegistry");
Preconditions.checkArgument(schemaModel != null, "null schemaModel");
// Validate
schemaModel.validate();
try {
new Schema(1, new byte[0], schemaModel, fieldTypeRegistry);
} catch (IllegalArgumentException e) {
throw new InvalidSchemaException("invalid schema: " + e.getMessage(), e);
}
}
/**
* Check whether a collection of {@link SchemaModel}s are individually valid and mutually consistent.
*
*
* This method verifies each schema via {@link #validateSchema(FieldTypeRegistry, SchemaModel) validateSchema()},
* and also verifies that the schemas are mututally consistent, i.e., that they do not use storage ID's incompatibly.
*
* @param fieldTypeRegistry registry of simple field types
* @param schemaModels schemas to validate (null elements are ignored)
* @throws InvalidSchemaException if an element in {@code schemaModels} is invalid
* @throws InvalidSchemaException if the {@code schemaModels} are not mutally consistent
* @throws IllegalArgumentException if either parameter is null
*/
public static void validateSchemas(FieldTypeRegistry fieldTypeRegistry, Collection schemaModels) {
// Sanity check
Preconditions.checkArgument(fieldTypeRegistry != null, "null fieldTypeRegistry");
Preconditions.checkArgument(schemaModels != null, "null schemaModels");
// Validate schemas individually and build map
final TreeMap schemaMap = new TreeMap<>();
int index = 0;
for (SchemaModel schemaModel : schemaModels) {
if (schemaModel == null)
continue;
schemaModel.validate();
final int version = index + 1;
try {
schemaMap.put(version, new Schema(version, new byte[0], schemaModel, fieldTypeRegistry));
} catch (IllegalArgumentException e) {
throw new InvalidSchemaException("invalid schema at index " + index + ": " + e.getMessage(), e);
}
index++;
}
// Validate schemas together
try {
new Schemas(schemaMap);
} catch (InvalidSchemaException e) {
throw e;
} catch (Exception e) {
throw new InvalidSchemaException("invalid schema combination: " + e.getMessage(), e);
}
}
private void checkAddNewSchema(SchemaModel schemaModel, int version, boolean allowNewSchema) {
if (version == 0)
throw new SchemaMismatchException("database is uninitialized and no schema version was provided");
if (schemaModel == null) {
throw new SchemaMismatchException("schema version " + version
+ " was not found in database, and no schema model was provided");
}
if (!allowNewSchema) {
throw new SchemaMismatchException("schema version " + version
+ " was not found in database, and recording a new schema version is disabled in this transaction");
}
}
/**
* Build {@link Schemas} object from a schema version XMLs.
*
* @throws InconsistentDatabaseException if any recorded schema version is invalid
*/
private Schemas decodeSchemas(SortedMap bytesMap, int formatVersion) {
final TreeMap versionMap = new TreeMap<>();
for (Map.Entry entry : bytesMap.entrySet()) {
final int version = entry.getKey();
final byte[] bytes = entry.getValue();
final SchemaModel schemaModel;
try {
schemaModel = Layout.decodeSchema(bytes, formatVersion);
} catch (InvalidSchemaException e) {
throw new InconsistentDatabaseException("found invalid schema version " + version + " recorded in database", e);
}
if (this.log.isTraceEnabled())
this.log.trace("read schema version {} from database:\n{}", version, schemaModel);
versionMap.put(version, new Schema(version, bytes, schemaModel, this.fieldTypeRegistry));
}
return new Schemas(versionMap);
}
}