diff --git a/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java b/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java index 19a89ded11..6a817a88d6 100644 --- a/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java +++ b/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java @@ -23,6 +23,7 @@ import org.rumbledb.exceptions.ExceptionMetadata; import org.rumbledb.exceptions.IsStaticallyUnexpectedTypeException; import org.rumbledb.exceptions.OurBadException; +import org.rumbledb.exceptions.SemanticException; import org.rumbledb.exceptions.UnexpectedStaticTypeException; import org.rumbledb.exceptions.UnknownFunctionCallException; import org.rumbledb.exceptions.UnsupportedFeatureException; @@ -2547,7 +2548,30 @@ public StaticContext visitValidateTypeExpression(ValidateTypeExpression expressi @Override public StaticContext visitValidateExpression(ValidateExpression expression, StaticContext argument) { visitDescendants(expression, expression.getStaticContext()); - expression.setStaticSequenceType(expression.getMainExpression().getStaticSequenceType()); + if (expression.getValidationMode() == ValidateExpression.ValidationMode.TYPE) { + Name typeName = expression.getTypeName(); + if (!Name.XS_NS.equals(typeName.getNamespace()) || !BuiltinTypesCatalogue.typeExists(typeName)) { + throw new SemanticException( + "The type " + typeName + " is not defined in the in-scope schema types.", + ErrorCode.ValidateTypeNotFoundErrorCode, + expression.getMetadata()); + } + ItemType targetType = BuiltinTypesCatalogue.getItemTypeByName(typeName); + if (!targetType.isAtomicItemType() + || targetType.equals(BuiltinTypesCatalogue.atomicItem) + || targetType.equals(BuiltinTypesCatalogue.NOTATIONItem)) { + throw new UnsupportedFeatureException( + "This first validate type implementation only supports concrete built-in XML Schema atomic types.", + expression.getMetadata()); + } + } + ItemType sourceItemType = + expression.getMainExpression().getStaticSequenceType().getItemType(); + ItemType resultItemType = sourceItemType.isSubtypeOf(BuiltinTypesCatalogue.elementNode) + || sourceItemType.isSubtypeOf(BuiltinTypesCatalogue.documentNode) + ? sourceItemType + : BuiltinTypesCatalogue.nodeItem; + expression.setStaticSequenceType(new SequenceType(resultItemType, SequenceType.Arity.One)); return argument; } diff --git a/src/main/java/org/rumbledb/compiler/RuntimeIteratorVisitor.java b/src/main/java/org/rumbledb/compiler/RuntimeIteratorVisitor.java index 9094a9061c..e99ab6eff0 100644 --- a/src/main/java/org/rumbledb/compiler/RuntimeIteratorVisitor.java +++ b/src/main/java/org/rumbledb/compiler/RuntimeIteratorVisitor.java @@ -229,8 +229,8 @@ import org.rumbledb.runtime.typing.CastIterator; import org.rumbledb.runtime.typing.CastableIterator; import org.rumbledb.runtime.typing.InstanceOfIterator; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.runtime.typing.TreatIterator; -import org.rumbledb.runtime.typing.ValidateTypeIterator; import org.rumbledb.runtime.update.expression.AppendExpressionIterator; import org.rumbledb.runtime.update.expression.CreateCollectionIterator; import org.rumbledb.runtime.update.expression.DeleteExpressionIterator; @@ -263,6 +263,7 @@ import org.rumbledb.runtime.xml.TextNodeConstructorRuntimeIterator; import org.rumbledb.runtime.xml.TextNodeRuntimeIterator; import org.rumbledb.runtime.xml.UnaryLookupIterator; +import org.rumbledb.runtime.xml.XQueryValidateIterator; import org.rumbledb.runtime.xml.axis.AxisIterator; import org.rumbledb.runtime.xml.axis.AxisIteratorVisitor; import org.rumbledb.types.BuiltinTypesCatalogue; @@ -1466,7 +1467,7 @@ public ItemRuntimePlan visitInstanceOfExpression(InstanceOfExpression expression @Override public ItemRuntimePlan visitValidateTypeExpression(ValidateTypeExpression expression, ItemRuntimePlan argument) { ItemRuntimePlan childExpression = this.visit(expression.getMainExpression(), argument); - ItemRuntimePlan runtimeIterator = new ValidateTypeIterator( + ItemRuntimePlan runtimeIterator = new JSONiqValidateIterator( childExpression, expression.getSequenceType().getItemType(), expression.isValidate(), @@ -1484,8 +1485,15 @@ public ItemRuntimePlan visitValidateTypeExpression(ValidateTypeExpression expres @Override public ItemRuntimePlan visitValidateExpression(ValidateExpression expression, ItemRuntimePlan argument) { - throw new UnsupportedFeatureException( - "XML Schema validate expressions are not executable yet.", expression.getMetadata()); + if (expression.getValidationMode() != ValidateExpression.ValidationMode.TYPE) { + throw new UnsupportedFeatureException( + "Strict and lax XML Schema validation are not executable yet.", expression.getMetadata()); + } + ItemRuntimePlan operand = this.visit(expression.getMainExpression(), argument); + return new XQueryValidateIterator( + operand, + BuiltinTypesCatalogue.getItemTypeByName(expression.getTypeName()), + expression.getStaticContextForRuntime(this.config, this.visitorConfig)); } @Override diff --git a/src/main/java/org/rumbledb/errorcodes/ErrorCode.java b/src/main/java/org/rumbledb/errorcodes/ErrorCode.java index 6065cada4c..8c744c793d 100644 --- a/src/main/java/org/rumbledb/errorcodes/ErrorCode.java +++ b/src/main/java/org/rumbledb/errorcodes/ErrorCode.java @@ -158,9 +158,11 @@ public String toString() { public static final ErrorCode UnexpectedTypeErrorCode = registerBuiltIn("XPTY0004"); public static final ErrorCode NodeAndNonNode = registerBuiltIn("XPTY0018"); public static final ErrorCode UnexpectedNode = registerBuiltIn("XPTY0019"); + public static final ErrorCode ValidateOperandTypeErrorCode = registerBuiltIn("XQTY0030"); public static final ErrorCode InvalidInstance = registerBuiltIn("XQDY0027"); public static final ErrorCode InvalidProcessingInstructionTargetCastErrorCode = registerBuiltIn("XQDY0041"); + public static final ErrorCode InvalidValidateDocumentStructureErrorCode = registerBuiltIn("XQDY0061"); public static final ErrorCode CycleInVariableDeclarationsErrorCode = registerBuiltIn("XQDY0054"); public static final ErrorCode InvalidProcessingInstructionContentErrorCode = registerBuiltIn("XQDY0026"); public static final ErrorCode InvalidProcessingInstructionTargetErrorCode = registerBuiltIn("XQDY0064"); @@ -203,6 +205,7 @@ public String toString() { public static final ErrorCode DecimalFormatPropertyInvalidValueErrorCode = registerBuiltIn("XQST0097"); public static final ErrorCode DecimalFormatPropertyConflictErrorCode = registerBuiltIn("XQST0098"); public static final ErrorCode DuplicateDecimalFormatPropertyErrorCode = registerBuiltIn("XQST0114"); + public static final ErrorCode ValidateTypeNotFoundErrorCode = registerBuiltIn("XQST0104"); public static final ErrorCode AtomizationError = registerBuiltIn("FOTY0012"); public static final ErrorCode UnexpectedFunctionItem = registerBuiltIn("FOTY0015"); diff --git a/src/main/java/org/rumbledb/exceptions/ValidateException.java b/src/main/java/org/rumbledb/exceptions/ValidateException.java new file mode 100644 index 0000000000..05a6512ad7 --- /dev/null +++ b/src/main/java/org/rumbledb/exceptions/ValidateException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rumbledb.exceptions; + +import java.io.Serial; + +import org.rumbledb.errorcodes.ErrorCode; + +public class ValidateException extends RumbleException { + + @Serial + private static final long serialVersionUID = 1L; + + public ValidateException(String message, ErrorCode errorCode, ExceptionMetadata metadata) { + super(message, errorCode, metadata); + } +} diff --git a/src/main/java/org/rumbledb/items/xml/AttributeItem.java b/src/main/java/org/rumbledb/items/xml/AttributeItem.java index 01b078ba89..7c2310fd5b 100644 --- a/src/main/java/org/rumbledb/items/xml/AttributeItem.java +++ b/src/main/java/org/rumbledb/items/xml/AttributeItem.java @@ -165,7 +165,8 @@ public List atomizedValue() { Item typedValue = CastIterator.castItemToType( ItemFactory.getInstance().createUntypedAtomicItem(this.stringValue), this.typeAnnotation, - ExceptionMetadata.EMPTY_METADATA); + ExceptionMetadata.EMPTY_METADATA, + NamespaceBindingUtils.namespaceResolver(this.parent)); return Collections.singletonList(typedValue); } return Collections.singletonList(ItemFactory.getInstance().createUntypedAtomicItem(this.stringValue)); diff --git a/src/main/java/org/rumbledb/items/xml/ElementItem.java b/src/main/java/org/rumbledb/items/xml/ElementItem.java index fd0d5a4366..944b0d11bc 100644 --- a/src/main/java/org/rumbledb/items/xml/ElementItem.java +++ b/src/main/java/org/rumbledb/items/xml/ElementItem.java @@ -444,7 +444,8 @@ public List atomizedValue() { Item typedValue = CastIterator.castItemToType( ItemFactory.getInstance().createUntypedAtomicItem(this.stringValue), this.typeAnnotation, - ExceptionMetadata.EMPTY_METADATA); + ExceptionMetadata.EMPTY_METADATA, + NamespaceBindingUtils.namespaceResolver(this)); return Collections.singletonList(typedValue); } // For untyped elements, atomization yields the element's typed value as xs:untypedAtomic. diff --git a/src/main/java/org/rumbledb/runtime/dataframe/ItemRuntimeDataFrameFactory.java b/src/main/java/org/rumbledb/runtime/dataframe/ItemRuntimeDataFrameFactory.java index dbeb2bc18d..cde0444486 100644 --- a/src/main/java/org/rumbledb/runtime/dataframe/ItemRuntimeDataFrameFactory.java +++ b/src/main/java/org/rumbledb/runtime/dataframe/ItemRuntimeDataFrameFactory.java @@ -20,8 +20,8 @@ import org.rumbledb.exceptions.OurBadException; import org.rumbledb.items.structured.HomogeneousItemDataFrame; import org.rumbledb.runtime.plan.ItemRuntimePlan; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.runtime.typing.TypeInferrenceUtils; -import org.rumbledb.runtime.typing.ValidateTypeIterator; import org.rumbledb.types.ItemType; /** @@ -48,7 +48,7 @@ public HomogeneousItemDataFrame fromList( log.debug("Inferred DataFrame type:\n" + itemType); } } - return ValidateTypeIterator.convertLocalItemsToDataFrame(items, itemType, context, true, staticContext); + return JSONiqValidateIterator.convertLocalItemsToDataFrame(items, itemType, context, true, staticContext); } @Override @@ -59,7 +59,7 @@ public HomogeneousItemDataFrame fromRDD( itemType = TypeInferrenceUtils.inferItemTypeOfRDDItems( rdd, staticContext.getMetadata(), TypeInferrenceUtils.TypeMergeMode.LAX); } - return ValidateTypeIterator.convertRDDToValidDataFrame(rdd, itemType, context, true, staticContext); + return JSONiqValidateIterator.convertRDDToValidDataFrame(rdd, itemType, context, true, staticContext); } public HomogeneousItemDataFrame fromPlan(ItemRuntimePlan plan, DynamicContext context) { diff --git a/src/main/java/org/rumbledb/runtime/flwor/clauses/ReturnClauseIterator.java b/src/main/java/org/rumbledb/runtime/flwor/clauses/ReturnClauseIterator.java index eab5f4602a..f4e7d90fae 100644 --- a/src/main/java/org/rumbledb/runtime/flwor/clauses/ReturnClauseIterator.java +++ b/src/main/java/org/rumbledb/runtime/flwor/clauses/ReturnClauseIterator.java @@ -68,7 +68,7 @@ import org.rumbledb.runtime.plan.RDDRuntimePlan; import org.rumbledb.runtime.plan.RuntimePlan; import org.rumbledb.runtime.plan.UpdatingRuntimePlan; -import org.rumbledb.runtime.typing.ValidateTypeIterator; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.runtime.update.PendingUpdateList; import org.rumbledb.spark.SparkSessionManager; import org.rumbledb.types.SequenceType; @@ -307,7 +307,7 @@ public HomogeneousItemDataFrame createNativeDataFrame(DynamicContext context) { } JavaRDD rdd = createNativeRDD(context); - return ValidateTypeIterator.convertRDDToValidDataFrame( + return JSONiqValidateIterator.convertRDDToValidDataFrame( rdd, this.expression.getRuntimeStaticContext().getStaticType().getItemType(), context, diff --git a/src/main/java/org/rumbledb/runtime/flwor/expression/SimpleMapExpressionIterator.java b/src/main/java/org/rumbledb/runtime/flwor/expression/SimpleMapExpressionIterator.java index 1f261b074a..e84d14d39b 100644 --- a/src/main/java/org/rumbledb/runtime/flwor/expression/SimpleMapExpressionIterator.java +++ b/src/main/java/org/rumbledb/runtime/flwor/expression/SimpleMapExpressionIterator.java @@ -56,7 +56,7 @@ import org.rumbledb.runtime.plan.LocalRuntimePlan; import org.rumbledb.runtime.plan.NativeQueryRuntimePlan; import org.rumbledb.runtime.plan.RDDRuntimePlan; -import org.rumbledb.runtime.typing.ValidateTypeIterator; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.spark.SparkSessionManager; @Log4j2 @@ -181,7 +181,7 @@ public HomogeneousItemDataFrame createNativeDataFrame(DynamicContext context) { if (nativeQuery == NativeClauseContext.NoNativeQuery) { JavaRDD rdd = createNativeRDD(context); JavaRDD rowRDD = rdd.map(i -> RowFactory.create(i.castToDecimalValue())); - StructType schema = ValidateTypeIterator.convertToDataFrameSchema( + StructType schema = JSONiqValidateIterator.convertToDataFrameSchema( getStaticType().getItemType(), this.staticContext); schema.printTreeString(); Dataset result = diff --git a/src/main/java/org/rumbledb/runtime/functions/sequences/value/DistinctValuesFunctionIterator.java b/src/main/java/org/rumbledb/runtime/functions/sequences/value/DistinctValuesFunctionIterator.java index b99029822f..6180374163 100644 --- a/src/main/java/org/rumbledb/runtime/functions/sequences/value/DistinctValuesFunctionIterator.java +++ b/src/main/java/org/rumbledb/runtime/functions/sequences/value/DistinctValuesFunctionIterator.java @@ -42,8 +42,8 @@ import org.rumbledb.runtime.plan.LocalRuntimePlan; import org.rumbledb.runtime.plan.NativeQueryRuntimePlan; import org.rumbledb.runtime.plan.RDDRuntimePlan; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.runtime.typing.TypeInferrenceUtils; -import org.rumbledb.runtime.typing.ValidateTypeIterator; import org.rumbledb.types.ItemType; public class DistinctValuesFunctionIterator extends ItemRuntimePlan @@ -95,7 +95,7 @@ public HomogeneousItemDataFrame createNativeDataFrame(DynamicContext dynamicCont itemType = TypeInferrenceUtils.inferItemTypeOfRDDItems( rdd, getMetadata(), TypeInferrenceUtils.TypeMergeMode.LAX); } - return ValidateTypeIterator.convertRDDToValidDataFrame( + return JSONiqValidateIterator.convertRDDToValidDataFrame( rdd, itemType, dynamicContext, true, getRuntimeStaticContext()); } diff --git a/src/main/java/org/rumbledb/runtime/typing/ValidateTypeIterator.java b/src/main/java/org/rumbledb/runtime/typing/JSONiqValidateIterator.java similarity index 87% rename from src/main/java/org/rumbledb/runtime/typing/ValidateTypeIterator.java rename to src/main/java/org/rumbledb/runtime/typing/JSONiqValidateIterator.java index 5689d3effb..af519a105b 100644 --- a/src/main/java/org/rumbledb/runtime/typing/ValidateTypeIterator.java +++ b/src/main/java/org/rumbledb/runtime/typing/JSONiqValidateIterator.java @@ -45,6 +45,7 @@ import org.rumbledb.runtime.plan.LocalRuntimePlan; import org.rumbledb.runtime.plan.NativeQueryRuntimePlan; import org.rumbledb.runtime.plan.RDDRuntimePlan; +import org.rumbledb.runtime.xml.BuiltinTypeValidator; import org.rumbledb.spark.SparkSessionManager; import org.rumbledb.types.BuiltinTypesCatalogue; import org.rumbledb.types.FieldDescriptor; @@ -53,7 +54,7 @@ import org.rumbledb.types.TypeMappings; @Log4j2 -public class ValidateTypeIterator extends ItemRuntimePlan +public class JSONiqValidateIterator extends ItemRuntimePlan implements LocalRuntimePlan, RDDRuntimePlan, DataFrameRuntimePlan, NativeQueryRuntimePlan { @Serial @@ -64,7 +65,7 @@ public class ValidateTypeIterator extends ItemRuntimePlan private final boolean isValidate; private final ItemValidator validator; - public ValidateTypeIterator( + public JSONiqValidateIterator( ItemRuntimePlan instance, ItemType itemType, boolean isValidate, RuntimeStaticContext staticContext) { super(Collections.singletonList(instance), staticContext); this.iterator = instance; @@ -346,7 +347,7 @@ private static Object getRowColumnFromItemUsingDataType(Item item, DataType data } if (dataType instanceof StructType structType) { - return ValidateTypeIterator.convertLocalItemToRow(item, structType, context); + return JSONiqValidateIterator.convertLocalItemToRow(item, structType, context); } if (dataType.equals(DataTypes.BooleanType)) { @@ -441,7 +442,7 @@ public Item call(Item item) { private Item validate(Item item, ItemType itemType) { if (itemType.isAtomicItemType()) { if (item.isElementNode() || item.isDocumentNode()) { - return validateXmlNodeAgainstAtomicType(item, itemType); + return BuiltinTypeValidator.validate(item, itemType, this.metadata); } if (InstanceOfIterator.doesItemTypeMatchItem(itemType, item)) { return item; @@ -626,103 +627,6 @@ private Item validate(Item item, ItemType itemType) { } return item; } - - private Item validateXmlNodeAgainstAtomicType(Item item, ItemType itemType) { - Item copiedRoot; - Item validatedElement; - if (item.isDocumentNode()) { - validateAtomicDocumentShape(item); - copiedRoot = item.copy(false); - reattachXmlParents(copiedRoot, null); - Item copiedDocumentElement = getSingleElementChild(copiedRoot); - if (copiedDocumentElement == null) { - throw new InvalidInstanceException( - "Document validation requires exactly one element child for atomic type " - + itemType.getIdentifierString(), - this.metadata); - } - validatedElement = copiedDocumentElement; - } else if (item.isElementNode()) { - validateAtomicElementShape(item); - copiedRoot = item.copy(false); - reattachXmlParents(copiedRoot, null); - validatedElement = copiedRoot; - } else { - throw new InvalidInstanceException( - "Atomic XML validation is only supported for document and element nodes.", this.metadata); - } - - Item castType; - try { - castType = CastIterator.castItemToType( - ItemFactory.getInstance().createUntypedAtomicItem(validatedElement.getStringValue()), - itemType, - this.metadata, - this.staticContext); - } catch (Exception e) { - throw new InvalidInstanceException( - "Cannot cast " + item.serialize() + " to type " + itemType.getIdentifierString()); - } - if (castType == null) { - throw new InvalidInstanceException( - "Cannot cast " + item.serialize() + " to type " + itemType.getIdentifierString()); - } - validatedElement.setSchemaType(itemType); - return copiedRoot; - } - - private static void validateAtomicDocumentShape(Item document) { - Item documentElement = getSingleElementChild(document); - if (documentElement == null) { - throw new InvalidInstanceException( - "Document validation requires exactly one element child for atomic type validation."); - } - for (Item child : document.children()) { - if (!child.isElementNode() && !child.isCommentNode() && !child.isProcessingInstructionNode()) { - throw new InvalidInstanceException( - "Document validation for atomic types only supports element, comment, and processing-instruction children."); - } - } - validateAtomicElementShape(documentElement); - } - - private static void validateAtomicElementShape(Item element) { - if (!element.attributes().isEmpty()) { - throw new InvalidInstanceException("Element validation for atomic types does not support attributes."); - } - for (Item child : element.children()) { - if (child.isElementNode()) { - throw new InvalidInstanceException( - "Element validation for atomic types does not support nested element children."); - } - } - } - - private static Item getSingleElementChild(Item document) { - Item elementChild = null; - for (Item child : document.children()) { - if (!child.isElementNode()) { - continue; - } - if (elementChild != null) { - return null; - } - elementChild = child; - } - return elementChild; - } - - private static void reattachXmlParents(Item node, Item parent) { - if (parent != null) { - node.setParent(parent); - } - for (Item attribute : node.attributes()) { - attribute.setParent(node); - } - for (Item child : node.children()) { - reattachXmlParents(child, node); - } - } } @Override diff --git a/src/main/java/org/rumbledb/runtime/xml/BuiltinTypeValidator.java b/src/main/java/org/rumbledb/runtime/xml/BuiltinTypeValidator.java new file mode 100644 index 0000000000..718f6ffff9 --- /dev/null +++ b/src/main/java/org/rumbledb/runtime/xml/BuiltinTypeValidator.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rumbledb.runtime.xml; + +import org.rumbledb.api.Item; +import org.rumbledb.exceptions.CastException; +import org.rumbledb.exceptions.DatetimeOverflowOrUnderflow; +import org.rumbledb.exceptions.DurationOverflowOrUnderflow; +import org.rumbledb.exceptions.ExceptionMetadata; +import org.rumbledb.exceptions.InvalidInstanceException; +import org.rumbledb.exceptions.InvalidLexicalValueException; +import org.rumbledb.exceptions.OurBadException; +import org.rumbledb.exceptions.UnexpectedTypeException; +import org.rumbledb.items.ItemFactory; +import org.rumbledb.runtime.typing.CastIterator; +import org.rumbledb.types.ItemType; + +/** Validates an XML element or document against a built-in atomic XML Schema type. */ +public final class BuiltinTypeValidator { + + private BuiltinTypeValidator() {} + + public static Item validate(Item item, ItemType itemType, ExceptionMetadata metadata) { + if (!itemType.isAtomicItemType()) { + throw new OurBadException("Built-in XML validation requires an atomic target type.", metadata); + } + + Item copiedRoot; + Item validatedElement; + if (item.isDocumentNode()) { + validateAtomicDocumentShape(item, metadata); + copiedRoot = item.copy(false); + reattachXmlParents(copiedRoot, null); + validatedElement = getSingleElementChild(copiedRoot); + } else if (item.isElementNode()) { + validateAtomicElementShape(item, metadata); + copiedRoot = item.copy(false); + reattachXmlParents(copiedRoot, null); + validatedElement = copiedRoot; + } else { + throw new InvalidInstanceException( + "Atomic XML validation is only supported for document and element nodes.", metadata); + } + + Item typedValue; + try { + typedValue = CastIterator.castItemToType( + ItemFactory.getInstance().createUntypedAtomicItem(validatedElement.getStringValue()), + itemType, + metadata, + NamespaceBindingUtils.namespaceResolver(validatedElement)); + } catch (CastException + | DatetimeOverflowOrUnderflow + | DurationOverflowOrUnderflow + | InvalidLexicalValueException + | UnexpectedTypeException exception) { + throw invalidValue(item, itemType, metadata, exception); + } + if (typedValue == null) { + throw invalidValue(item, itemType, metadata, null); + } + validatedElement.setSchemaType(itemType); + return copiedRoot; + } + + private static InvalidInstanceException invalidValue( + Item item, ItemType itemType, ExceptionMetadata metadata, Exception cause) { + InvalidInstanceException exception = new InvalidInstanceException( + "The value of " + item.serialize() + " is not valid for " + itemType.getIdentifierString() + ".", + metadata); + if (cause != null) { + exception.initCause(cause); + } + return exception; + } + + private static void validateAtomicDocumentShape(Item document, ExceptionMetadata metadata) { + if (!hasValidDocumentStructure(document)) { + throw new InvalidInstanceException( + "A document node must have exactly one element child and only comment or " + + "processing-instruction siblings.", + metadata); + } + validateAtomicElementShape(getSingleElementChild(document), metadata); + } + + public static boolean hasValidDocumentStructure(Item document) { + if (!document.isDocumentNode() || getSingleElementChild(document) == null) { + return false; + } + for (Item child : document.children()) { + if (!child.isElementNode() && !child.isCommentNode() && !child.isProcessingInstructionNode()) { + return false; + } + } + return true; + } + + private static void validateAtomicElementShape(Item element, ExceptionMetadata metadata) { + if (!element.attributes().isEmpty()) { + throw new InvalidInstanceException( + "An element validated against an atomic type cannot have attributes.", metadata); + } + for (Item child : element.children()) { + if (child.isElementNode()) { + throw new InvalidInstanceException( + "An element validated against an atomic type cannot have element children.", metadata); + } + } + } + + public static Item getSingleElementChild(Item document) { + Item elementChild = null; + for (Item child : document.children()) { + if (!child.isElementNode()) { + continue; + } + if (elementChild != null) { + return null; + } + elementChild = child; + } + return elementChild; + } + + private static void reattachXmlParents(Item node, Item parent) { + if (parent != null) { + node.setParent(parent); + } + for (Item attribute : node.attributes()) { + attribute.setParent(node); + } + for (Item child : node.children()) { + reattachXmlParents(child, node); + } + } +} diff --git a/src/main/java/org/rumbledb/runtime/xml/NamespaceBindingUtils.java b/src/main/java/org/rumbledb/runtime/xml/NamespaceBindingUtils.java index a85c34c2f7..940de6cc6e 100644 --- a/src/main/java/org/rumbledb/runtime/xml/NamespaceBindingUtils.java +++ b/src/main/java/org/rumbledb/runtime/xml/NamespaceBindingUtils.java @@ -20,6 +20,7 @@ package org.rumbledb.runtime.xml; +import java.util.HashMap; import java.util.Map; import org.w3c.dom.Node; @@ -178,6 +179,20 @@ public static NamespaceResolver namespaceResolver(StaticContext staticContext) { }; } + /** Resolves prefixes against an element node's in-scope namespaces. */ + public static NamespaceResolver namespaceResolver(Item element) { + if (element == null || !element.isElementNode()) { + return builtinNamespaceResolver(); + } + Map inScopeNamespaces = new HashMap<>(); + for (Item namespace : element.namespaceNodes()) { + Name namespaceName = namespace.nodeName(); + String prefix = namespaceName == null ? "" : namespaceName.getLocalName(); + inScopeNamespaces.put(prefix, namespace.getStringValue()); + } + return inScopeNamespaces::get; + } + /** * Applies XSD whiteSpace facet COLLAPSE (as for xs:QName lexical forms). */ diff --git a/src/main/java/org/rumbledb/runtime/xml/XQueryValidateIterator.java b/src/main/java/org/rumbledb/runtime/xml/XQueryValidateIterator.java new file mode 100644 index 0000000000..626988d465 --- /dev/null +++ b/src/main/java/org/rumbledb/runtime/xml/XQueryValidateIterator.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rumbledb.runtime.xml; + +import java.io.Serial; +import java.util.List; + +import org.rumbledb.api.Item; +import org.rumbledb.context.DynamicContext; +import org.rumbledb.context.RuntimeStaticContext; +import org.rumbledb.errorcodes.ErrorCode; +import org.rumbledb.exceptions.MoreThanOneItemException; +import org.rumbledb.exceptions.ValidateException; +import org.rumbledb.runtime.AbstractAtMostOneItemRuntimePlan; +import org.rumbledb.runtime.plan.ItemRuntimePlan; +import org.rumbledb.types.ItemType; + +/** Local evaluation of XQuery {@code validate type} for built-in atomic types. */ +public final class XQueryValidateIterator extends AbstractAtMostOneItemRuntimePlan { + + @Serial + private static final long serialVersionUID = 1L; + + private final ItemRuntimePlan operand; + private final ItemType targetType; + + public XQueryValidateIterator(ItemRuntimePlan operand, ItemType targetType, RuntimeStaticContext staticContext) { + super(List.of(operand), staticContext); + this.operand = operand; + this.targetType = targetType; + } + + @Override + public Item evaluateAtMostOne(DynamicContext context) { + Item item; + try { + item = this.operand.materializeAtMostOne(context); + } catch (MoreThanOneItemException exception) { + throw operandTypeError("The operand contains more than one item."); + } + if (item == null) { + throw operandTypeError("The operand is an empty sequence."); + } + if (!item.isDocumentNode() && !item.isElementNode()) { + throw operandTypeError("The operand is neither a document nor an element node."); + } + if (item.isDocumentNode() && !BuiltinTypeValidator.hasValidDocumentStructure(item)) { + throw new ValidateException( + "A document node being validated must have exactly one element child and only comment or " + + "processing-instruction siblings.", + ErrorCode.InvalidValidateDocumentStructureErrorCode, + getMetadata()); + } + return BuiltinTypeValidator.validate(item, this.targetType, getMetadata()); + } + + private ValidateException operandTypeError(String detail) { + return new ValidateException( + "A validate expression requires exactly one document or element node. " + detail, + ErrorCode.ValidateOperandTypeErrorCode, + getMetadata()); + } +} diff --git a/src/main/java/org/rumbledb/spark/ml/AnnotateFunctionIterator.java b/src/main/java/org/rumbledb/spark/ml/AnnotateFunctionIterator.java index f59e331f56..b413509279 100644 --- a/src/main/java/org/rumbledb/spark/ml/AnnotateFunctionIterator.java +++ b/src/main/java/org/rumbledb/spark/ml/AnnotateFunctionIterator.java @@ -13,7 +13,7 @@ import org.rumbledb.runtime.dataframe.ItemRuntimeDataFrameFactory; import org.rumbledb.runtime.plan.DataFrameRuntimePlan; import org.rumbledb.runtime.plan.ItemRuntimePlan; -import org.rumbledb.runtime.typing.ValidateTypeIterator; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.types.ItemType; import org.rumbledb.types.ItemTypeFactory; @@ -44,18 +44,18 @@ public HomogeneousItemDataFrame createNativeDataFrame(DynamicContext context) { return inputDataAsDataFrame; } JavaRDD inputDataAsRDDOfItems = inputDataAsDataFrame.toRDD(getMetadata()); - return ValidateTypeIterator.convertRDDToValidDataFrame( + return JSONiqValidateIterator.convertRDDToValidDataFrame( inputDataAsRDDOfItems, schemaType, context, true, this.staticContext); } if (inputDataIterator.getRuntimeStaticContext().getExecutionMode().isRDDOrDataFrame()) { JavaRDD rdd = inputDataIterator.getRDD(context); - return ValidateTypeIterator.convertRDDToValidDataFrame( + return JSONiqValidateIterator.convertRDDToValidDataFrame( rdd, schemaType, context, true, this.staticContext); } List items = inputDataIterator.materialize(context); - return ValidateTypeIterator.convertLocalItemsToDataFrame( + return JSONiqValidateIterator.convertLocalItemsToDataFrame( items, schemaType, context, true, this.staticContext); } catch (InvalidInstanceException ex) { InvalidInstanceException e = new InvalidInstanceException( diff --git a/src/main/java/org/rumbledb/spark/ml/RumbleMLUtils.java b/src/main/java/org/rumbledb/spark/ml/RumbleMLUtils.java index fa28200a7e..72f83889f9 100644 --- a/src/main/java/org/rumbledb/spark/ml/RumbleMLUtils.java +++ b/src/main/java/org/rumbledb/spark/ml/RumbleMLUtils.java @@ -24,8 +24,8 @@ import org.rumbledb.items.structured.HomogeneousItemDataFrame; import org.rumbledb.runtime.dataframe.RuntimeDataFrame; import org.rumbledb.runtime.typing.CastIterator; +import org.rumbledb.runtime.typing.JSONiqValidateIterator; import org.rumbledb.runtime.typing.TypeInferrenceUtils; -import org.rumbledb.runtime.typing.ValidateTypeIterator; import org.rumbledb.types.BuiltinTypesCatalogue; import org.rumbledb.types.ItemType; @@ -47,13 +47,13 @@ public static RuntimeDataFrame getDataFrameOrInferFromVariable( JavaRDD rdd = context.getVariableValues().getRDDVariableValue(inputVariableName, metadata); ItemType type = TypeInferrenceUtils.inferItemTypeOfRDDItems(rdd, metadata, TypeInferrenceUtils.TypeMergeMode.LAX); - return ValidateTypeIterator.convertRDDToValidDataFrame(rdd, type, context, true, staticContext); + return JSONiqValidateIterator.convertRDDToValidDataFrame(rdd, type, context, true, staticContext); } List items = context.getVariableValues().getLocalVariableValue(inputVariableName, metadata); ItemType type = TypeInferrenceUtils.inferItemTypeOfLocalItems(items, metadata, TypeInferrenceUtils.TypeMergeMode.LAX); - return ValidateTypeIterator.convertLocalItemsToDataFrame(items, type, context, true, staticContext); + return JSONiqValidateIterator.convertLocalItemsToDataFrame(items, type, context, true, staticContext); } public static ParamMap convertRumbleObjectItemToSparkMLParamMap( diff --git a/src/test/java/iq/XQueryValidateBuiltinTypeTest.java b/src/test/java/iq/XQueryValidateBuiltinTypeTest.java new file mode 100644 index 0000000000..ef706464cc --- /dev/null +++ b/src/test/java/iq/XQueryValidateBuiltinTypeTest.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package iq; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import org.rumbledb.api.Rumble; +import org.rumbledb.bindings.ExternalBindings; +import org.rumbledb.compiler.VisitorHelpers; +import org.rumbledb.config.RumbleConfiguration; +import org.rumbledb.exceptions.RumbleException; +import org.rumbledb.expressions.module.MainModule; +import org.rumbledb.expressions.typing.ValidateExpression; +import org.rumbledb.types.BuiltinTypesCatalogue; +import org.rumbledb.types.SequenceType; + +@Timeout(1000) +public class XQueryValidateBuiltinTypeTest { + + private static Rumble rumble; + private static RumbleConfiguration xqueryConfiguration; + + @BeforeAll + public static void setUp() { + xqueryConfiguration = RumbleConfiguration.builder() + .configureSemantics(semantics -> semantics.queryLanguage("xquery31")) + .build(); + rumble = new Rumble(xqueryConfiguration); + } + + @Test + public void validatesStringAndIntegerValues() { + assertTrue("data(validate type xs:string { hello }) instance of xs:string"); + assertTrue("data(validate type xs:integer { 42 }) instance of xs:integer"); + assertTrue("data(validate type xs:integer { 42 }) eq 42"); + } + + @Test + public void resolvesQNameAgainstTheValidatedElement() { + assertTrue("namespace-uri-from-QName(data(validate type xs:QName { " + + "p:name })) eq \"urn:test\""); + } + + @Test + public void returnsANewNodeWithTheSameKind() { + assertTrue("let $input := 42 " + + "let $validated := validate type xs:integer { $input } " + + "return $validated instance of element() and not($input is $validated)"); + } + + @Test + public void infersExactlyOneResultWhilePreservingTheNodeKind() { + MainModule module = VisitorHelpers.parseMainModuleFromQuery( + "validate type xs:string { (, ) }", xqueryConfiguration, ExternalBindings.empty()); + ValidateExpression expression = Assertions.assertInstanceOf(ValidateExpression.class, module.getExpression()); + Assertions.assertEquals( + SequenceType.Arity.One, expression.getStaticSequenceType().getArity()); + Assertions.assertTrue( + expression.getStaticSequenceType().getItemType().isSubtypeOf(BuiltinTypesCatalogue.elementNode)); + } + + @Test + public void rejectsInvalidLexicalValues() { + assertErrorCode("validate type xs:integer { not-an-integer }", "XQDY0027"); + } + + @Test + public void rejectsOperandsThatAreNotExactlyOneDocumentOrElement() { + assertErrorCode("validate type xs:string { () }", "XQTY0030"); + assertErrorCode("validate type xs:string { 1 }", "XQTY0030"); + assertErrorCode("validate type xs:string { (, ) }", "XQTY0030"); + } + + @Test + public void rejectsMalformedDocumentNodesWithTheDocumentStructureError() { + assertErrorCode("validate type xs:string { document { , } }", "XQDY0061"); + assertErrorCode("validate type xs:string { document { text { 'text' }, } }", "XQDY0061"); + } + + @Test + public void rejectsUnknownTypesStatically() { + assertErrorCode("validate type xs:notAType { }", "XQST0104"); + } + + private static void assertErrorCode(String query, String expectedCode) { + RumbleException exception = + Assertions.assertThrows(RumbleException.class, () -> rumble.runQueryToString(query)); + Assertions.assertEquals(expectedCode, exception.getErrorCode().toString()); + } + + private static void assertTrue(String query) { + Assertions.assertTrue(rumble.runQuery(query).getAsList().get(0).getBooleanValue()); + } +}