Index: extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/LuceneSearchSession.java =================================================================== --- extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/LuceneSearchSession.java (revision 2338) +++ extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/LuceneSearchSession.java (working copy) @@ -1558,20 +1558,49 @@ public class LuceneSearchSession implements WorkspaceSession { int docId = doc + docOffset; Object[] tuple = new Object[numValues]; Document document = currentReader.document(docId, fieldSelector); - for (String columnName : columns.getColumnNames()) { - int index = columns.getColumnIndexForName(columnName); - // We just need to retrieve the first value if there is more than one ... - tuple[index] = document.get(columnName); - } + // Read the location ... + Location location = session.readLocation(document); + tuple[locationIndex] = location; // Set the score column if required ... + Float score = null; if (recordScore) { assert scorer != null; - tuple[scoreIndex] = scorer.score(); + score = new Float(scorer.score()); + tuple[scoreIndex] = score; + } + + // Read the column values ... + for (String columnName : columns.getColumnNames()) { + int index = columns.getColumnIndexForName(columnName); + // We just need to retrieve the first value if there is more than one ... + Object value = document.get(columnName); + if (value == null) { + if (columnName.equals("jcr:path")) { + value = session.processor.stringFactory.create(location.getPath()); + } else if (columnName.equals("mode:depth")) { + value = new Long(location.getPath().size()); + } else if (columnName.equals("jcr:score")) { + value = score == null ? new Double(scorer.score()) : new Double(score.doubleValue()); + } else if (columnName.equals("jcr:name")) { + Path path = location.getPath(); + if (path.isRoot()) { + value = ""; + } else { + value = session.processor.stringFactory.create(path.getLastSegment().getName()); + } + } else if (columnName.equals("mode:localName")) { + Path path = location.getPath(); + if (path.isRoot()) { + value = ""; + } else { + value = path.getLastSegment().getName().getLocalName(); + } + } + } + tuple[index] = value; } - // Read the location ... - tuple[locationIndex] = session.readLocation(document); tuples.add(tuple); } } Index: modeshape-graph/src/main/java/org/modeshape/graph/query/plan/CanonicalPlanner.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/plan/CanonicalPlanner.java (revision 2338) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/plan/CanonicalPlanner.java (working copy) @@ -481,7 +481,7 @@ public class CanonicalPlanner implements Planner { List columns, List columnTypes ) { // Compute the columns from this selector ... - for (Schemata.Column column : table.getColumns()) { + for (Schemata.Column column : table.getSelectAllColumns()) { String columnName = column.getName(); String propertyName = columnName; Column newColumn = new Column(tableName, propertyName, columnName); Index: modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanUtil.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanUtil.java (revision 2338) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanUtil.java (working copy) @@ -32,8 +32,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import org.modeshape.graph.GraphI18n; -import org.modeshape.graph.JcrLexicon; import org.modeshape.graph.query.QueryContext; import org.modeshape.graph.query.model.And; import org.modeshape.graph.query.model.ArithmeticOperand; @@ -230,12 +228,6 @@ public class PlanUtil { // The column could not be found in the source, but it may be referring to a residual property. // Therefore, we'll use the default type ... types.add(context.getTypeSystem().getDefaultType()); - if (JcrLexicon.NAME.getLocalName().equals(column.propertyName())) { - // Someone is using the "name" column, so record a warning ... - context.getProblems().addWarning(GraphI18n.columnDoesNotExistOnTable, - column.propertyName(), - column.selectorName().name()); - } } } return types; Index: modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableSchemata.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableSchemata.java (revision 2338) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableSchemata.java (working copy) @@ -24,9 +24,11 @@ package org.modeshape.graph.query.validate; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -73,7 +75,7 @@ public class ImmutableSchemata implements Schemata { public static class Builder { private final TypeSystem typeSystem; - private final Map tables = new HashMap(); + private final Map tables = new HashMap(); private final Map viewDefinitions = new HashMap(); private final Set tablesOrViewsWithExtraColumns = new HashSet(); @@ -101,8 +103,8 @@ public class ImmutableSchemata implements Schemata { CheckArg.isNotEmpty(columnName, "columnName[" + (i++) + "]"); columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType())); } - ImmutableTable table = new ImmutableTable(new SelectorName(name), columns, false); - tables.put(table.getName(), table); + MutableTable table = new MutableTable(name, columns, false); + tables.put(name, table); return this; } @@ -131,8 +133,8 @@ public class ImmutableSchemata implements Schemata { CheckArg.isNotEmpty(columnName, "columnName[" + i + "]"); columns.add(new ImmutableColumn(columnName, types[i])); } - ImmutableTable table = new ImmutableTable(new SelectorName(name), columns, false); - tables.put(table.getName(), table); + MutableTable table = new MutableTable(name, columns, false); + tables.put(name, table); return this; } @@ -212,17 +214,16 @@ public class ImmutableSchemata implements Schemata { CheckArg.isNotEmpty(tableName, "tableName"); CheckArg.isNotEmpty(columnName, "columnName"); CheckArg.isNotNull(type, "type"); - SelectorName selector = new SelectorName(tableName); - ImmutableTable existing = tables.get(selector); - ImmutableTable table = null; + MutableTable existing = tables.get(tableName); + Column column = new ImmutableColumn(columnName, type, fullTextSearchable); if (existing == null) { List columns = new ArrayList(); - columns.add(new ImmutableColumn(columnName, type, fullTextSearchable)); - table = new ImmutableTable(selector, columns, false); + columns.add(column); + existing = new MutableTable(tableName, columns, false); + tables.put(tableName, existing); } else { - table = existing.withColumn(columnName, type, fullTextSearchable); + existing.addColumn(column); } - tables.put(table.getName(), table); return this; } @@ -238,22 +239,19 @@ public class ImmutableSchemata implements Schemata { String columnName ) { CheckArg.isNotEmpty(tableName, "tableName"); CheckArg.isNotEmpty(columnName, "columnName"); - SelectorName selector = new SelectorName(tableName); - ImmutableTable existing = tables.get(selector); - ImmutableTable table = null; + MutableTable existing = tables.get(tableName); if (existing == null) { List columns = new ArrayList(); columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType(), true)); - table = new ImmutableTable(selector, columns, false); + existing = new MutableTable(tableName, columns, false); + tables.put(tableName, existing); } else { Column column = existing.getColumn(columnName); - String type = typeSystem.getDefaultType(); - if (column != null) { - type = column.getPropertyType(); + if (column != null && !column.isFullTextSearchable()) { + column = new ImmutableColumn(columnName, column.getPropertyType(), true); } - table = existing.withColumn(columnName, type, true); + existing.addColumn(column); } - tables.put(table.getName(), table); return this; } @@ -262,12 +260,34 @@ public class ImmutableSchemata implements Schemata { * * @param tableName the name of the table * @return this builder, for convenience in method chaining; never null - * @throws IllegalArgumentException if the table name does is null or empty + * @throws IllegalArgumentException if the table name is null or empty, or the table does not exist */ public Builder markExtraColumns( String tableName ) { CheckArg.isNotEmpty(tableName, "tableName"); - SelectorName selector = new SelectorName(tableName); - tablesOrViewsWithExtraColumns.add(selector); + tablesOrViewsWithExtraColumns.add(new SelectorName(tableName)); + return this; + } + + /** + * Specify that the named column in the given table should be excluded from the selected columns when "SELECT *" is used. + * + * @param tableName the name of the new table + * @param columnName the names of the column + * @return this builder, for convenience in method chaining; never null + * @throws IllegalArgumentException if the table name is null or empty or if the column name is null or empty + */ + public Builder excludeFromSelectStar( String tableName, + String columnName ) { + CheckArg.isNotEmpty(tableName, "tableName"); + CheckArg.isNotEmpty(columnName, "columnName"); + MutableTable existing = tables.get(tableName); + if (existing == null) { + List columns = new ArrayList(); + columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType(), true)); + existing = new MutableTable(tableName, columns, false); + tables.put(tableName, existing); + } + existing.excludeFromSelectStar(columnName); return this; } @@ -284,21 +304,20 @@ public class ImmutableSchemata implements Schemata { String... columnNames ) { CheckArg.isNotEmpty(tableName, "tableName"); CheckArg.isNotEmpty(columnNames, "columnNames"); - ImmutableTable existing = tables.get(new SelectorName(tableName)); + MutableTable existing = tables.get(tableName); if (existing == null) { throw new IllegalArgumentException(GraphI18n.tableDoesNotExist.text(tableName)); } Set keyColumns = new HashSet(); for (String columnName : columnNames) { - Column existingColumn = existing.getColumnsByName().get(columnName); + Column existingColumn = existing.getColumn(columnName); if (existingColumn == null) { String msg = GraphI18n.schemataKeyReferencesNonExistingColumn.text(tableName, columnName); throw new IllegalArgumentException(msg); } keyColumns.add(existingColumn); } - ImmutableTable table = existing.withKey(keyColumns); - tables.put(table.getName(), table); + existing.addKey(keyColumns); return this; } @@ -310,15 +329,16 @@ public class ImmutableSchemata implements Schemata { * @throws InvalidQueryException if any of the view definitions is invalid and cannot be resolved */ public Schemata build() { - // Go through the tables and mark those that have extra columns ... - for (SelectorName tableName : tablesOrViewsWithExtraColumns) { - ImmutableTable table = tables.get(tableName); - if (table != null) { - tables.put(table.getName(), table.withExtraColumns()); + Map tablesByName = new HashMap(); + // Add all the tables ... + for (MutableTable mutableTable : tables.values()) { + if (tablesOrViewsWithExtraColumns.contains(mutableTable.getName())) { + mutableTable.setExtraColumns(true); } + Table table = mutableTable.asImmutable(); + tablesByName.put(table.getName(), table); } - - ImmutableSchemata schemata = new ImmutableSchemata(new HashMap(tables)); + ImmutableSchemata schemata = new ImmutableSchemata(tablesByName); // Make a copy of the view definitions, and create the views ... Map definitions = new HashMap(viewDefinitions); @@ -348,6 +368,7 @@ public class ImmutableSchemata implements Schemata { // Go through all the columns and look up the types ... Map tableNameByAlias = null; List viewColumns = new ArrayList(columns.size()); + List viewColumnsInSelectStar = new ArrayList(columns.size()); for (org.modeshape.graph.query.model.Column column : columns) { // Find the table that the column came from ... Table source = schemata.getTable(column.selectorName()); @@ -370,8 +391,12 @@ public class ImmutableSchemata implements Schemata { "The view references a non-existant column '" + column.columnName() + "' in '" + source.getName() + "'"); } - viewColumns.add(new ImmutableColumn(viewColumnName, sourceColumn.getPropertyType(), - sourceColumn.isFullTextSearchable())); + Column newColumn = new ImmutableColumn(viewColumnName, sourceColumn.getPropertyType(), + sourceColumn.isFullTextSearchable()); + viewColumns.add(newColumn); + if (source.getSelectAllColumnsByName().containsKey(sourceColumnName)) { + viewColumnsInSelectStar.add(newColumn); + } } if (viewColumns.size() != columns.size()) { // We weren't able to resolve all of the columns, @@ -380,10 +405,22 @@ public class ImmutableSchemata implements Schemata { } // If we could resolve the definition ... + Map viewColumnsByName = new HashMap(); + Map viewSelectStarColumnsByName = new HashMap(); + for (Column column : viewColumns) { + viewColumnsByName.put(column.getName(), column); + } + for (Column column : viewColumnsInSelectStar) { + viewSelectStarColumnsByName.put(column.getName(), column); + } + Set keys = Collections.emptySet(); boolean hasExtraColumns = tablesOrViewsWithExtraColumns.contains(name); - ImmutableView view = new ImmutableView(name, viewColumns, hasExtraColumns, command); + ImmutableView view = new ImmutableView(name, viewColumnsByName, viewColumns, hasExtraColumns, command, keys, + viewSelectStarColumnsByName, viewColumnsInSelectStar); definitions.remove(name); - schemata = schemata.with(view); + + tablesByName.put(view.getName(), view); + schemata = new ImmutableSchemata(tablesByName); added = true; } } while (added && !definitions.isEmpty()); @@ -436,4 +473,74 @@ public class ImmutableSchemata implements Schemata { return sb.toString(); } + protected static class MutableTable { + private final SelectorName name; + private final Map columnsByName = new HashMap(); + private final List columns = new LinkedList(); + private final Set keys = new HashSet(); + private boolean extraColumns = false; + private final Set columnNamesNotInSelectStar = new HashSet(); + + protected MutableTable( String name, + List columns, + boolean extraColumns ) { + this.name = new SelectorName(name); + this.columns.addAll(columns); + for (Column column : columns) { + Column existing = this.columnsByName.put(column.getName(), column); + assert existing == null; + } + } + + public SelectorName getName() { + return name; + } + + protected void addColumn( Column column ) { + Column existing = this.columnsByName.put(column.getName(), column); + if (existing != null) { + this.columns.remove(existing); + } + this.columns.add(column); + } + + protected Column getColumn( String name ) { + return columnsByName.get(name); + } + + protected Set getColumnNamesInSelectStar() { + return columnNamesNotInSelectStar; + } + + protected boolean addKey( Collection keyColumns ) { + return keys.add(new ImmutableKey(keyColumns)); + } + + protected void setExtraColumns( boolean extraColumns ) { + this.extraColumns = extraColumns; + } + + protected void excludeFromSelectStar( String columnName ) { + columnNamesNotInSelectStar.add(columnName); + } + + protected Table asImmutable() { + Map columnsByName = Collections.unmodifiableMap(this.columnsByName); + List columns = Collections.unmodifiableList(this.columns); + Set keys = Collections.unmodifiableSet(this.keys); + List columnsInSelectStar = new ArrayList(); + Map columnsInSelectStarByName = new HashMap(); + for (Column column : columns) { + if (!columnNamesNotInSelectStar.contains(column.getName())) { + columnsInSelectStar.add(column); + columnsInSelectStarByName.put(column.getName(), column); + } + } + columnsInSelectStar = Collections.unmodifiableList(columnsInSelectStar); + columnsInSelectStarByName = Collections.unmodifiableMap(columnsInSelectStarByName); + return new ImmutableTable(name, columnsByName, columns, keys, extraColumns, columnsInSelectStarByName, + columnsInSelectStar); + } + } + } Index: modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableTable.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableTable.java (revision 2338) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableTable.java (working copy) @@ -46,6 +46,8 @@ class ImmutableTable implements Table { private final List columns; private final Set keys; private final boolean extraColumns; + private final List selectStarColumns; + private final Map selectStarColumnsByName; protected ImmutableTable( SelectorName name, Iterable columns, @@ -85,18 +87,27 @@ class ImmutableTable implements Table { this.keys = Collections.emptySet(); } this.extraColumns = extraColumns; + this.selectStarColumns = this.columns; + this.selectStarColumnsByName = this.columnsByName; } protected ImmutableTable( SelectorName name, Map columnsByName, List columns, Set keys, - boolean extraColumns ) { + boolean extraColumns, + Map selectStarColumnsByName, + List selectStarColumns ) { this.name = name; this.columns = columns; this.columnsByName = columnsByName; this.keys = keys; this.extraColumns = extraColumns; + assert selectStarColumns != null; + assert selectStarColumnsByName != null; + assert selectStarColumns.size() == selectStarColumnsByName.size(); + this.selectStarColumns = selectStarColumns; + this.selectStarColumnsByName = selectStarColumnsByName; } /** @@ -129,6 +140,24 @@ class ImmutableTable implements Table { /** * {@inheritDoc} * + * @see org.modeshape.graph.query.validate.Schemata.Table#getSelectAllColumns() + */ + public List getSelectAllColumns() { + return selectStarColumns; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.query.validate.Schemata.Table#getSelectAllColumnsByName() + */ + public Map getSelectAllColumnsByName() { + return selectStarColumnsByName; + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.query.validate.Schemata.Table#getColumnsByName() */ public Map getColumnsByName() { @@ -144,6 +173,10 @@ class ImmutableTable implements Table { return keys; } + protected Set getKeySet() { + return keys; + } + /** * {@inheritDoc} * @@ -197,29 +230,56 @@ class ImmutableTable implements Table { public ImmutableTable withColumn( String name, String type ) { - List newColumns = new LinkedList(columns); - newColumns.add(new ImmutableColumn(name, type)); - return new ImmutableTable(getName(), newColumns, extraColumns); + return withColumn(name, type, ImmutableColumn.DEFAULT_FULL_TEXT_SEARCHABLE); } public ImmutableTable withColumn( String name, String type, boolean fullTextSearchable ) { + // Create the new column ... + Column newColumn = new ImmutableColumn(name, type, fullTextSearchable); + // Add to the list and map ... List newColumns = new LinkedList(columns); - newColumns.add(new ImmutableColumn(name, type, fullTextSearchable)); - return new ImmutableTable(getName(), newColumns, extraColumns); + newColumns.add(newColumn); + List selectStarColumns = new LinkedList(this.selectStarColumns); + Map selectStarColumnMap = new HashMap(this.selectStarColumnsByName); + Map columnMap = new HashMap(columnsByName); + Column existing = columnMap.put(newColumn.getName(), newColumn); + if (existing != null) { + newColumns.remove(existing); + if (selectStarColumnMap.containsKey(existing.getName())) { + // The old column was in the SELECT * list, so the new one should be, too... + selectStarColumnMap.put(newColumn.getName(), newColumn); + selectStarColumns.add(newColumn); + } + } + return new ImmutableTable(getName(), columnMap, newColumns, keys, extraColumns, selectStarColumnMap, selectStarColumns); } public ImmutableTable withColumns( Iterable columns ) { + // Add to the list and map ... List newColumns = new LinkedList(this.getColumns()); + List selectStarColumns = new LinkedList(this.selectStarColumns); + Map selectStarColumnMap = new HashMap(this.selectStarColumnsByName); + Map columnMap = new HashMap(columnsByName); for (Column column : columns) { - newColumns.add(new ImmutableColumn(column.getName(), column.getPropertyType(), column.isFullTextSearchable())); + Column newColumn = new ImmutableColumn(column.getName(), column.getPropertyType(), column.isFullTextSearchable()); + newColumns.add(newColumn); + Column existing = columnMap.put(newColumn.getName(), newColumn); + if (existing != null) { + newColumns.remove(existing); + if (selectStarColumnMap.containsKey(existing.getName())) { + // The old column was in the SELECT * list, so the new one should be, too... + selectStarColumnMap.put(newColumn.getName(), newColumn); + selectStarColumns.add(newColumn); + } + } } - return new ImmutableTable(getName(), newColumns, extraColumns); + return new ImmutableTable(getName(), columnMap, newColumns, keys, extraColumns, selectStarColumnMap, selectStarColumns); } public ImmutableTable with( SelectorName name ) { - return new ImmutableTable(name, columnsByName, columns, keys, extraColumns); + return new ImmutableTable(name, columnsByName, columns, keys, extraColumns, selectStarColumnsByName, selectStarColumns); } public ImmutableTable withKey( Iterable keyColumns ) { @@ -228,7 +288,7 @@ class ImmutableTable implements Table { assert columns.contains(keyColumn); } if (!keys.add(new ImmutableKey(keyColumns))) return this; - return new ImmutableTable(name, columnsByName, columns, keys, extraColumns); + return new ImmutableTable(name, columnsByName, columns, keys, extraColumns, selectStarColumnsByName, selectStarColumns); } public ImmutableTable withKey( Column... keyColumns ) { @@ -236,11 +296,27 @@ class ImmutableTable implements Table { } public ImmutableTable withExtraColumns() { - return extraColumns ? this : new ImmutableTable(name, columnsByName, columns, keys, true); + return extraColumns ? this : new ImmutableTable(name, columnsByName, columns, keys, true, selectStarColumnsByName, + selectStarColumns); } public ImmutableTable withoutExtraColumns() { - return !extraColumns ? this : new ImmutableTable(name, columnsByName, columns, keys, false); + return !extraColumns ? this : new ImmutableTable(name, columnsByName, columns, keys, false, selectStarColumnsByName, + selectStarColumns); + } + + public ImmutableTable withColumnNotInSelectStar( String name ) { + Column column = columnsByName.get(name); + if (column == null) return this; + if (!getSelectAllColumnsByName().containsKey(name)) { + return this; // already not in select * + } + List selectStarColumns = new LinkedList(this.selectStarColumns); + Map selectStarColumnsByName = new HashMap(this.selectStarColumnsByName); + selectStarColumns.remove(column); + selectStarColumnsByName.remove(name); + return new ImmutableTable(this.name, columnsByName, columns, keys, extraColumns, selectStarColumnsByName, + selectStarColumns); } /** Index: modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableView.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableView.java (revision 2338) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/validate/ImmutableView.java (working copy) @@ -65,7 +65,19 @@ class ImmutableView extends ImmutableTable implements View { boolean extraColumns, QueryCommand definition, Set keys ) { - super(name, columnsByName, columns, keys, extraColumns); + super(name, columnsByName, columns, keys, extraColumns, columnsByName, columns); + this.definition = definition; + } + + protected ImmutableView( SelectorName name, + Map columnsByName, + List columns, + boolean extraColumns, + QueryCommand definition, + Set keys, + Map selectStarColumnsByName, + List selectStarColumns ) { + super(name, columnsByName, columns, keys, extraColumns, selectStarColumnsByName, selectStarColumns); this.definition = definition; } @@ -81,6 +93,19 @@ class ImmutableView extends ImmutableTable implements View { /** * {@inheritDoc} * + * @see org.modeshape.graph.query.validate.ImmutableTable#withColumnNotInSelectStar(java.lang.String) + */ + @Override + public ImmutableView withColumnNotInSelectStar( String name ) { + ImmutableTable result = super.withColumnNotInSelectStar(name); + if (result == this) return this; + return new ImmutableView(result.getName(), result.getColumnsByName(), result.getColumns(), result.hasExtraColumns(), + definition, result.getKeySet(), result.getSelectAllColumnsByName(), result.getSelectAllColumns()); + } + + /** + * {@inheritDoc} + * * @see java.lang.Object#toString() */ @Override Index: modeshape-graph/src/main/java/org/modeshape/graph/query/validate/Schemata.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/validate/Schemata.java (revision 2338) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/validate/Schemata.java (working copy) @@ -85,6 +85,20 @@ public interface Schemata { List getColumns(); /** + * Get the queryable columns in this table that should be used in case of "SELECT *". + * + * @return the immutable, ordered list of immutable column objects; never null + */ + List getSelectAllColumns(); + + /** + * Get the queryable columns in this table that should be used in case of "SELECT *". + * + * @return the immutable map of immutable column objects by their name; never null + */ + Map getSelectAllColumnsByName(); + + /** * Get the collection of keys for this table. * * @return the immutable collection of immutable keys; never null, but possibly empty Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java (working copy) @@ -162,6 +162,7 @@ public final class JcrI18n { public static I18n selectorNotUsedInQuery; public static I18n selectorUsedInEquiJoinCriteriaDoesNotExistInQuery; public static I18n multipleSelectorsAppearInQueryRequireSpecifyingSelectorName; + public static I18n equiJoinWithOneJcrPathPseudoColumnIsInvalid; // Type registration messages public static I18n invalidNodeTypeName; Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrPropertyDefinition.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrPropertyDefinition.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrPropertyDefinition.java (working copy) @@ -88,6 +88,7 @@ class JcrPropertyDefinition extends JcrItemDefinition implements PropertyDefinit this.queryOrderable = queryOrderable; this.queryOperators = queryOperators; this.isPrivate = name != null && ModeShapeIntLexicon.Namespace.URI.equals(name.getNamespaceUri()); + assert this.valueConstraints != null; } /** Index: modeshape-jcr/src/main/java/org/modeshape/jcr/ModeShapeLexicon.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/ModeShapeLexicon.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/ModeShapeLexicon.java (working copy) @@ -63,4 +63,7 @@ public class ModeShapeLexicon extends org.modeshape.repository.ModeShapeLexicon * proxy. */ public static final Name SHARED_UUID = new BasicName(Namespace.URI, "sharedUuid"); + + public static final Name DEPTH = new BasicName(Namespace.URI, "depth"); + public static final Name LOCALNAME = new BasicName(Namespace.URI, "localName"); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/NodeTypeSchemata.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/NodeTypeSchemata.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/NodeTypeSchemata.java (working copy) @@ -23,14 +23,18 @@ */ package org.modeshape.jcr; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import javax.jcr.PropertyType; +import javax.jcr.Value; import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.OnParentVersionAction; import net.jcip.annotations.Immutable; import net.jcip.annotations.NotThreadSafe; import org.apache.lucene.document.Field; @@ -67,14 +71,15 @@ class NodeTypeSchemata implements Schemata { private final Map types; private final Map prefixesByUris = new HashMap(); private final boolean includeColumnsForInheritedProperties; - private final Iterable propertyDefinitions; + private final Collection propertyDefinitions; private final Map nodeTypesByName; private final Multimap subtypesByName = LinkedHashMultimap.create(); private final IndexRules indexRules; + private final List pseudoProperties = new ArrayList(); NodeTypeSchemata( ExecutionContext context, Map nodeTypes, - Iterable propertyDefinitions, + Collection propertyDefinitions, boolean includeColumnsForInheritedProperties ) { this.includeColumnsForInheritedProperties = includeColumnsForInheritedProperties; this.propertyDefinitions = propertyDefinitions; @@ -112,13 +117,19 @@ class NodeTypeSchemata implements Schemata { types.put(PropertyType.NAME, typeSystem.getStringFactory().getTypeName()); types.put(PropertyType.URI, typeSystem.getStringFactory().getTypeName()); + pseudoProperties.add(pseudoProperty(context, JcrLexicon.PATH, PropertyType.PATH)); + pseudoProperties.add(pseudoProperty(context, JcrLexicon.NAME, PropertyType.NAME)); + pseudoProperties.add(pseudoProperty(context, JcrLexicon.SCORE, PropertyType.DOUBLE)); + pseudoProperties.add(pseudoProperty(context, ModeShapeLexicon.LOCALNAME, PropertyType.STRING)); + pseudoProperties.add(pseudoProperty(context, ModeShapeLexicon.DEPTH, PropertyType.LONG)); + // Create the "ALLNODES" table, which will contain all possible properties ... IndexRules.Builder indexRulesBuilder = IndexRules.createBuilder(LuceneSearchEngine.DEFAULT_RULES); indexRulesBuilder.defaultTo(Field.Store.YES, Field.Index.ANALYZED, DEFAULT_CAN_CONTAIN_REFERENCES, DEFAULT_FULL_TEXT_SEARCHABLE); - addAllNodesTable(builder, indexRulesBuilder, context); + addAllNodesTable(builder, indexRulesBuilder, context, pseudoProperties); // Define a view for each node type ... for (JcrNodeType nodeType : nodeTypesByName.values()) { @@ -129,6 +140,24 @@ class NodeTypeSchemata implements Schemata { indexRules = indexRulesBuilder.build(); } + protected JcrPropertyDefinition pseudoProperty( ExecutionContext context, + Name name, + int propertyType ) { + int opv = OnParentVersionAction.IGNORE; + boolean autoCreated = true; + boolean mandatory = true; + boolean isProtected = true; + boolean multiple = false; + boolean fullTextSearchable = false; + boolean queryOrderable = true; + Value[] defaultValues = null; + String[] valueConstraints = new String[] {}; + String[] queryOperators = null; + return new JcrPropertyDefinition(context, null, name, opv, autoCreated, mandatory, isProtected, defaultValues, + propertyType, valueConstraints, multiple, fullTextSearchable, queryOrderable, + queryOperators); + } + /** * Get the index rules ... * @@ -144,7 +173,8 @@ class NodeTypeSchemata implements Schemata { protected final void addAllNodesTable( ImmutableSchemata.Builder builder, IndexRules.Builder indexRuleBuilder, - ExecutionContext context ) { + ExecutionContext context, + List additionalProperties ) { NamespaceRegistry registry = context.getNamespaceRegistry(); TypeSystem typeSystem = context.getValueFactories().getTypeSystem(); @@ -198,6 +228,36 @@ class NodeTypeSchemata implements Schemata { canBeReference, isStrongReference); } + if (additionalProperties != null) { + boolean canBeReference = false; + boolean isStrongReference = false; + boolean fullTextSearchable = false; + assert !first; + for (JcrPropertyDefinition defn : additionalProperties) { + Name name = defn.getInternalName(); + String columnName = name.getString(registry); + assert defn.getRequiredType() != PropertyType.UNDEFINED; + String type = types.get(defn.getRequiredType()); + assert type != null; + String previousType = typesForNames.put(columnName, type); + if (previousType != null && !previousType.equals(type)) { + // There are two property definitions with the same name but different types, so we need to find a common type + // ... + type = typeSystem.getCompatibleType(previousType, type); + } + // Add (or overwrite) the column ... + builder.addColumn(tableName, columnName, type, fullTextSearchable); + builder.excludeFromSelectStar(tableName, columnName); + + // And build an indexing rule for this type ... + if (indexRuleBuilder != null) addIndexRule(indexRuleBuilder, + defn, + type, + typeSystem, + canBeReference, + isStrongReference); + } + } } /** @@ -270,8 +330,8 @@ class NodeTypeSchemata implements Schemata { } // Create the SQL statement ... StringBuilder viewDefinition = new StringBuilder("SELECT "); - boolean first = true; boolean hasResidualProperties = false; + boolean first = true; for (JcrPropertyDefinition defn : defns) { if (defn.isResidual()) { hasResidualProperties = true; @@ -286,6 +346,14 @@ class NodeTypeSchemata implements Schemata { else viewDefinition.append(','); viewDefinition.append('[').append(columnName).append(']'); } + // Add the pseudo-properties ... + for (JcrPropertyDefinition defn : pseudoProperties) { + Name name = defn.getInternalName(); + String columnName = name.getString(registry); + if (first) first = false; + else viewDefinition.append(','); + viewDefinition.append('[').append(columnName).append(']'); + } if (first) { // All the properties were skipped ... return; @@ -423,7 +491,7 @@ class NodeTypeSchemata implements Schemata { this.nameFactory = context.getValueFactories().getNameFactory(); this.builder = ImmutableSchemata.createBuilder(context.getValueFactories().getTypeSystem()); // Add the "AllNodes" table ... - addAllNodesTable(builder, null, context); + addAllNodesTable(builder, null, context, null); this.schemata = builder.build(); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryQueryManager.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryQueryManager.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryQueryManager.java (working copy) @@ -24,6 +24,7 @@ package org.modeshape.jcr; import java.io.File; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -54,6 +55,7 @@ import org.modeshape.graph.query.QueryResults.Columns; import org.modeshape.graph.query.model.QueryCommand; import org.modeshape.graph.query.model.TypeSystem; import org.modeshape.graph.query.optimize.Optimizer; +import org.modeshape.graph.query.optimize.OptimizerRule; import org.modeshape.graph.query.optimize.RuleBasedOptimizer; import org.modeshape.graph.query.plan.CanonicalPlanner; import org.modeshape.graph.query.plan.PlanHints; @@ -72,6 +74,7 @@ import org.modeshape.graph.request.processor.RequestProcessor; import org.modeshape.graph.search.SearchEngine; import org.modeshape.graph.search.SearchEngineIndexer; import org.modeshape.graph.search.SearchEngineProcessor; +import org.modeshape.jcr.query.RewritePseudoColumns; import org.modeshape.search.lucene.IndexRules; import org.modeshape.search.lucene.LuceneConfiguration; import org.modeshape.search.lucene.LuceneConfigurations; @@ -316,7 +319,24 @@ abstract class RepositoryQueryManager { // Set up the query engine ... Planner planner = new CanonicalPlanner(); - Optimizer optimizer = new RuleBasedOptimizer(); + + // Create a custom optimizer that has our rules first ... + Optimizer optimizer = new RuleBasedOptimizer() { + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.query.optimize.RuleBasedOptimizer#populateRuleStack(java.util.LinkedList, + * org.modeshape.graph.query.plan.PlanHints) + */ + @Override + protected void populateRuleStack( LinkedList ruleStack, + PlanHints hints ) { + super.populateRuleStack(ruleStack, hints); + ruleStack.addFirst(RewritePseudoColumns.INSTANCE); + } + }; + + // Create a custom processor that knows how to submit the access query requests ... Processor processor = new QueryProcessor() { /** Index: modeshape-jcr/src/main/java/org/modeshape/jcr/query/JcrQueryResult.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/query/JcrQueryResult.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/query/JcrQueryResult.java (working copy) @@ -33,19 +33,24 @@ import javax.jcr.AccessDeniedException; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.NodeIterator; +import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.query.QueryResult; import javax.jcr.query.Row; import javax.jcr.query.RowIterator; import net.jcip.annotations.NotThreadSafe; +import org.modeshape.common.collection.Collections; import org.modeshape.graph.Location; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Path; import org.modeshape.graph.query.QueryResults; import org.modeshape.graph.query.QueryResults.Columns; import org.modeshape.graph.query.model.Column; import org.modeshape.graph.query.validate.Schemata; import org.modeshape.graph.query.validate.Schemata.Table; import org.modeshape.jcr.JcrI18n; +import org.modeshape.jcr.query.JcrSqlQueryResult.JcrSqlQueryResultRowIterator; /** * The results of a query. This is not thread-safe because it relies upon JcrSession, which is not thread-safe. Also, although the @@ -56,6 +61,17 @@ import org.modeshape.jcr.JcrI18n; */ @NotThreadSafe public class JcrQueryResult implements QueryResult, org.modeshape.jcr.api.query.QueryResult { + public static final String JCR_SCORE_COLUMN_NAME = "jcr:score"; + public static final String JCR_PATH_COLUMN_NAME = "jcr:path"; + public static final String JCR_NAME_COLUMN_NAME = "jcr:name"; + public static final String MODE_LOCALNAME_COLUMN_NAME = "mode:localName"; + public static final String MODE_DEPTH_COLUMN_NAME = "mode:depth"; + protected static final Set PSEUDO_COLUMNS = Collections.unmodifiableSet(JCR_SCORE_COLUMN_NAME, + JCR_PATH_COLUMN_NAME, + JCR_NAME_COLUMN_NAME, + MODE_LOCALNAME_COLUMN_NAME, + MODE_DEPTH_COLUMN_NAME); + protected final JcrQueryContext context; protected final QueryResults results; protected final Schemata schemata; @@ -423,6 +439,30 @@ public class JcrQueryResult implements QueryResult, org.modeshape.jcr.api.query. public void remove() { throw new UnsupportedOperationException(); } + + protected Value jcrPath( Path path ) { + return context.createValue(PropertyType.PATH, path); + } + + protected Value jcrName( Name name ) { + return context.createValue(PropertyType.NAME, name); + } + + protected Value jcrName() { + return context.createValue(PropertyType.NAME, ""); + } + + protected Value jcrString( String name ) { + return context.createValue(PropertyType.STRING, name); + } + + protected Value jcrLong( Long value ) { + return context.createValue(PropertyType.LONG, value); + } + + protected Value jcrDouble( Float score ) { + return context.createValue(PropertyType.DOUBLE, score); + } } /** @@ -500,6 +540,34 @@ public class JcrQueryResult implements QueryResult, org.modeshape.jcr.api.query. * @see javax.jcr.query.Row#getValue(java.lang.String) */ public Value getValue( String columnName ) throws ItemNotFoundException, RepositoryException { + if (PSEUDO_COLUMNS.contains(columnName)) { + if (JCR_PATH_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[iterator.locationIndex]; + return iterator.jcrPath(location.getPath()); + } + if (JCR_NAME_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[iterator.locationIndex]; + Path path = location.getPath(); + if (path.isRoot()) return ((JcrSqlQueryResultRowIterator)iterator).jcrName(); + return iterator.jcrName(path.getLastSegment().getName()); + } + if (MODE_LOCALNAME_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[iterator.locationIndex]; + Path path = location.getPath(); + if (path.isRoot()) return ((JcrSqlQueryResultRowIterator)iterator).jcrString(""); + return iterator.jcrString(path.getLastSegment().getName().getLocalName()); + } + if (MODE_DEPTH_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[iterator.locationIndex]; + Path path = location.getPath(); + Long depth = new Long(path.size()); + return iterator.jcrLong(depth); + } + if (JCR_SCORE_COLUMN_NAME.equals(columnName)) { + Float score = (Float)tuple[iterator.scoreIndex]; + return iterator.jcrDouble(score); + } + } return node.getProperty(columnName).getValue(); } @@ -598,7 +666,37 @@ public class JcrQueryResult implements QueryResult, org.modeshape.jcr.api.query. throw new RepositoryException(JcrI18n.queryResultsDoNotIncludeColumn.text(columnName, iterator.query)); } Node node = nodes[locationIndex]; - return node != null ? node.getProperty(columnName).getValue() : null; + if (node == null) return null; + if (PSEUDO_COLUMNS.contains(columnName)) { + if (JCR_PATH_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[locationIndex]; + return iterator.jcrPath(location.getPath()); + } + if (JCR_NAME_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[locationIndex]; + Path path = location.getPath(); + if (path.isRoot()) return ((JcrSqlQueryResultRowIterator)iterator).jcrName(); + return iterator.jcrName(path.getLastSegment().getName()); + } + if (MODE_LOCALNAME_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[locationIndex]; + Path path = location.getPath(); + if (path.isRoot()) return ((JcrSqlQueryResultRowIterator)iterator).jcrString(""); + return iterator.jcrString(path.getLastSegment().getName().getLocalName()); + } + if (MODE_DEPTH_COLUMN_NAME.equals(columnName)) { + Location location = (Location)tuple[locationIndex]; + Path path = location.getPath(); + Long depth = new Long(path.size()); + return iterator.jcrLong(depth); + } + if (JCR_SCORE_COLUMN_NAME.equals(columnName)) { + int scoreIndex = iterator.columns.getFullTextSearchScoreIndexFor(columnName); + Float score = scoreIndex == -1 ? 0.0f : (Float)tuple[scoreIndex]; + return iterator.jcrDouble(score); + } + } + return node.getProperty(columnName).getValue(); } /** Index: modeshape-jcr/src/main/java/org/modeshape/jcr/query/JcrSqlQueryResult.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/query/JcrSqlQueryResult.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/query/JcrSqlQueryResult.java (working copy) @@ -35,7 +35,6 @@ import javax.jcr.Value; import javax.jcr.query.Row; import javax.jcr.query.RowIterator; import org.modeshape.graph.Location; -import org.modeshape.graph.property.Path; import org.modeshape.graph.query.QueryResults; import org.modeshape.graph.query.validate.Schemata; @@ -119,14 +118,6 @@ public class JcrSqlQueryResult extends JcrQueryResult { Object[] tuple ) { return new JcrSqlQueryResultRow(this, node, tuple); } - - protected Value jcrPath( Path path ) { - return context.createValue(PropertyType.PATH, path); - } - - protected Value jcrScore( Float score ) { - return context.createValue(PropertyType.DOUBLE, score); - } } protected static class JcrSqlQueryResultRow extends SingleSelectorQueryResultRow { @@ -145,11 +136,11 @@ public class JcrSqlQueryResult extends JcrQueryResult { public Value getValue( String columnName ) throws ItemNotFoundException, RepositoryException { if (JCR_PATH_COLUMN_NAME.equals(columnName)) { Location location = (Location)tuple[iterator.locationIndex]; - return ((JcrSqlQueryResultRowIterator)iterator).jcrPath(location.getPath()); + return iterator.jcrPath(location.getPath()); } if (JCR_SCORE_COLUMN_NAME.equals(columnName)) { Float score = (Float)tuple[iterator.scoreIndex]; - return ((JcrSqlQueryResultRowIterator)iterator).jcrScore(score); + return iterator.jcrDouble(score); } return super.getValue(columnName); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/query/RewritePseudoColumns.java new file mode 100644 =================================================================== --- /dev/null (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/query/RewritePseudoColumns.java (working copy) @@ -0,0 +1,208 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.jcr.query; + +import java.util.LinkedList; +import net.jcip.annotations.Immutable; +import org.modeshape.graph.query.QueryContext; +import org.modeshape.graph.query.model.And; +import org.modeshape.graph.query.model.Between; +import org.modeshape.graph.query.model.Comparison; +import org.modeshape.graph.query.model.Constraint; +import org.modeshape.graph.query.model.DynamicOperand; +import org.modeshape.graph.query.model.EquiJoinCondition; +import org.modeshape.graph.query.model.FullTextSearchScore; +import org.modeshape.graph.query.model.JoinCondition; +import org.modeshape.graph.query.model.NodeDepth; +import org.modeshape.graph.query.model.NodeLocalName; +import org.modeshape.graph.query.model.NodeName; +import org.modeshape.graph.query.model.NodePath; +import org.modeshape.graph.query.model.Not; +import org.modeshape.graph.query.model.Or; +import org.modeshape.graph.query.model.PropertyExistence; +import org.modeshape.graph.query.model.PropertyValue; +import org.modeshape.graph.query.model.SameNodeJoinCondition; +import org.modeshape.graph.query.model.SelectorName; +import org.modeshape.graph.query.model.SetCriteria; +import org.modeshape.graph.query.optimize.OptimizerRule; +import org.modeshape.graph.query.plan.PlanNode; +import org.modeshape.graph.query.plan.PlanNode.Property; +import org.modeshape.graph.query.plan.PlanNode.Traversal; +import org.modeshape.graph.query.plan.PlanNode.Type; +import org.modeshape.jcr.JcrI18n; + +/** + * An {@link OptimizerRule optimizer rule} that moves up higher in the plan any {@link Property#VARIABLE_NAME variable name} + * property to the node immediately under a {@link Type#DEPENDENT_QUERY dependent query} node. + */ +@Immutable +public class RewritePseudoColumns implements OptimizerRule { + + public static final RewritePseudoColumns INSTANCE = new RewritePseudoColumns(); + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.query.optimize.OptimizerRule#execute(org.modeshape.graph.query.QueryContext, + * org.modeshape.graph.query.plan.PlanNode, java.util.LinkedList) + */ + public PlanNode execute( QueryContext context, + PlanNode plan, + LinkedList ruleStack ) { + + // First go through all the JOIN conditions ... + for (PlanNode join : plan.findAllAtOrBelow(Traversal.PRE_ORDER, Type.JOIN)) { + JoinCondition condition = join.getProperty(Property.JOIN_CONDITION, JoinCondition.class); + JoinCondition newCondition = rewrite(context, condition); + if (newCondition != condition) { + join.setProperty(Property.JOIN_CONDITION, newCondition); + } + Constraint constraint = join.getProperty(Property.JOIN_CONSTRAINTS, Constraint.class); + Constraint newConstraint = rewrite(context, constraint); + if (newConstraint != constraint) { + join.setProperty(Property.JOIN_CONSTRAINTS, newConstraint); + } + } + + // Go through all the WHERE criteria ... + for (PlanNode node : plan.findAllAtOrBelow(Traversal.PRE_ORDER, Type.SELECT)) { + Constraint constraint = node.getProperty(Property.SELECT_CRITERIA, Constraint.class); + Constraint newConstraint = rewrite(context, constraint); + if (newConstraint != constraint) { + node.setProperty(Property.SELECT_CRITERIA, newConstraint); + } + } + return plan; + } + + protected JoinCondition rewrite( QueryContext context, + JoinCondition condition ) { + if (condition instanceof EquiJoinCondition) { + EquiJoinCondition equiJoin = (EquiJoinCondition)condition; + if ("jcr:path".equals(equiJoin.property1Name())) { + SelectorName selector1 = equiJoin.selector1Name(); + SelectorName selector2 = equiJoin.selector2Name(); + if ("jcr:path".equals(equiJoin.property2Name())) { + // This equijoin should be rewritten as a ISSAMENODE condition ... + return new SameNodeJoinCondition(selector1, selector2); + } + // Only one side uses "jcr:path", and we cannot handle this ... + context.getProblems().addError(JcrI18n.equiJoinWithOneJcrPathPseudoColumnIsInvalid, selector1, selector2); + } else if ("jcr:path".equals(equiJoin.property2Name())) { + SelectorName selector1 = equiJoin.selector1Name(); + SelectorName selector2 = equiJoin.selector2Name(); + // Only one side uses "jcr:path", and we cannot handle this ... + context.getProblems().addError(JcrI18n.equiJoinWithOneJcrPathPseudoColumnIsInvalid, selector1, selector2); + } + } + return condition; + } + + protected Constraint rewrite( QueryContext context, + Constraint constraint ) { + if (constraint instanceof Comparison) { + Comparison comparison = (Comparison)constraint; + DynamicOperand operand = comparison.operand1(); + DynamicOperand newOperand = rewrite(context, operand); + if (newOperand != operand) { + return new Comparison(newOperand, comparison.operator(), comparison.operand2()); + } + } else if (constraint instanceof And) { + And and = (And)constraint; + Constraint left = and.left(); + Constraint right = and.right(); + Constraint newLeft = rewrite(context, left); + Constraint newRight = rewrite(context, right); + if (newLeft != left || newRight != right) { + return new And(newLeft, newRight); + } + } else if (constraint instanceof Or) { + Or or = (Or)constraint; + Constraint left = or.left(); + Constraint right = or.right(); + Constraint newLeft = rewrite(context, left); + Constraint newRight = rewrite(context, right); + if (newLeft != left || newRight != right) { + return new Or(newLeft, newRight); + } + } else if (constraint instanceof Not) { + Not not = (Not)constraint; + Constraint oldInner = not.constraint(); + Constraint newInner = rewrite(context, oldInner); + if (oldInner != newInner) { + return new Not(newInner); + } + } else if (constraint instanceof Between) { + Between between = (Between)constraint; + DynamicOperand operand = between.operand(); + DynamicOperand newOperand = rewrite(context, operand); + if (newOperand != operand) { + return new Between(newOperand, between.lowerBound(), between.upperBound(), between.isLowerBoundIncluded(), + between.isUpperBoundIncluded()); + } + } else if (constraint instanceof SetCriteria) { + SetCriteria set = (SetCriteria)constraint; + DynamicOperand operand = set.leftOperand(); + DynamicOperand newOperand = rewrite(context, operand); + if (newOperand != operand) { + return new SetCriteria(newOperand, set.rightOperands()); + } + } else if (constraint instanceof PropertyExistence) { + PropertyExistence exist = (PropertyExistence)constraint; + String property = exist.propertyName(); + if ("jcr:path".equals(property) || "jcr:name".equals(property) || "mode:localName".equals(property) + || "jcr:localName".equals(property) || "mode:depth".equals(property) || "jcr:depth".equals(property) + || "jcr:score".equals(property)) { + // This constraint will always be true, so use this constraint that always is true ... + return new PropertyExistence(exist.selectorName(), "jcr:primaryType"); + } + } + return constraint; + } + + protected DynamicOperand rewrite( QueryContext context, + DynamicOperand operand ) { + if (operand instanceof PropertyValue) { + PropertyValue propValue = (PropertyValue)operand; + String property = propValue.propertyName(); + if ("jcr:path".equals(property)) { + return new NodePath(propValue.selectorName()); + } + if ("jcr:name".equals(property)) { + return new NodeName(propValue.selectorName()); + } + if ("mode:localName".equals(property) || "jcr:localName".equals(property)) { + return new NodeLocalName(propValue.selectorName()); + } + if ("mode:depth".equals(property) || "jcr:depth".equals(property)) { + return new NodeDepth(propValue.selectorName()); + } + if ("jcr:score".equals(property)) { + return new FullTextSearchScore(propValue.selectorName()); + } + } + return operand; + } + +} Index: modeshape-jcr/src/main/java/org/modeshape/jcr/query/XPathQueryResult.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/query/XPathQueryResult.java (revision 2338) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/query/XPathQueryResult.java (working copy) @@ -36,7 +36,6 @@ import javax.jcr.query.QueryResult; import javax.jcr.query.Row; import javax.jcr.query.RowIterator; import org.modeshape.graph.Location; -import org.modeshape.graph.property.Path; import org.modeshape.graph.query.QueryResults; import org.modeshape.graph.query.validate.Schemata; @@ -118,14 +117,6 @@ public class XPathQueryResult extends JcrQueryResult { Object[] tuple ) { return new XPathQueryResultRow(this, node, tuple); } - - protected Value jcrPath( Path path ) { - return context.createValue(PropertyType.PATH, path); - } - - protected Value jcrScore( Float score ) { - return context.createValue(PropertyType.DOUBLE, score); - } } protected static class XPathQueryResultRow extends SingleSelectorQueryResultRow { @@ -148,7 +139,7 @@ public class XPathQueryResult extends JcrQueryResult { } if (JCR_SCORE_COLUMN_NAME.equals(columnName)) { Float score = (Float)tuple[iterator.scoreIndex]; - return ((XPathQueryResultRowIterator)iterator).jcrScore(score); + return ((XPathQueryResultRowIterator)iterator).jcrDouble(score); } return super.getValue(columnName); } Index: modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties =================================================================== --- modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties (revision 2338) +++ modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties (working copy) @@ -150,6 +150,7 @@ queryResultsDoNotIncludeColumn = The column '{0}' does not appear in the results selectorNotUsedInQuery = The selector '{0}' was not used in the query: {1} selectorUsedInEquiJoinCriteriaDoesNotExistInQuery = The selector '{0}' used in the equijoin criteria (at line {1} and column {2}) does not exist in the query multipleSelectorsAppearInQueryRequireSpecifyingSelectorName = Selector name must be specified when the query contains multiple selectors: {0} +equiJoinWithOneJcrPathPseudoColumnIsInvalid = Equi-join condition using one 'jcr:path' column is not valid: expected "... [{0}].[jcr:path] = [{1}].[jcr:path] ..." invalidNodeTypeName=Node types cannot have a null or empty name badNodeTypeName={0} cannot have a null or invalid name Index: modeshape-jcr/src/test/java/org/modeshape/jcr/JcrQueryManagerTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/JcrQueryManagerTest.java (revision 2338) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/JcrQueryManagerTest.java (working copy) @@ -131,7 +131,15 @@ public class JcrQueryManagerTest { } // Start the repository ... - repository = engine.getRepository("cars"); + try { + repository = engine.getRepository("cars"); + } catch (RuntimeException t) { + t.printStackTrace(); + throw t; + } catch (Exception t) { + t.printStackTrace(); + throw t; + } // Use a session to load the contents ... Session session = repository.login(); @@ -551,6 +559,122 @@ public class JcrQueryManagerTest { } } + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedPathInSelect() throws RepositoryException { + Query query = session.getWorkspace().getQueryManager().createQuery("select [jcr:primaryType], [jcr:path] FROM [nt:base]", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + // print = true; + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 24); + assertResultsHaveColumns(result, new String[] {"jcr:primaryType", "jcr:path"}); + RowIterator iter = result.getRows(); + while (iter.hasNext()) { + Row row = iter.nextRow(); + assertThat(row, is(notNullValue())); + } + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedPathInSelectAndCriteria() throws RepositoryException { + Query query = session.getWorkspace() + .getQueryManager() + .createQuery("select [jcr:primaryType], [jcr:path] FROM [nt:base] WHERE [jcr:path] LIKE '/Cars/%'", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + // print = true; + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 17); + assertResultsHaveColumns(result, new String[] {"jcr:primaryType", "jcr:path"}); + RowIterator iter = result.getRows(); + while (iter.hasNext()) { + Row row = iter.nextRow(); + assertThat(row, is(notNullValue())); + } + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedPathInSelectAndUnqualifiedNameInCriteria() throws RepositoryException { + Query query = session.getWorkspace() + .getQueryManager() + .createQuery("select [jcr:primaryType], [jcr:path] FROM [nt:base] WHERE [jcr:name] LIKE '%3%'", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + // print = true; + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 4); + assertResultsHaveColumns(result, new String[] {"jcr:primaryType", "jcr:path"}); + RowIterator iter = result.getRows(); + while (iter.hasNext()) { + Row row = iter.nextRow(); + assertThat(row, is(notNullValue())); + } + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedPathInSelectAndUnqualifiedLocalNameInCriteria() throws RepositoryException { + Query query = session.getWorkspace() + .getQueryManager() + .createQuery("select [jcr:primaryType], [jcr:path] FROM [nt:base] WHERE [mode:localName] LIKE '%3%'", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + // print = true; + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 4); + assertResultsHaveColumns(result, new String[] {"jcr:primaryType", "jcr:path"}); + RowIterator iter = result.getRows(); + while (iter.hasNext()) { + Row row = iter.nextRow(); + assertThat(row, is(notNullValue())); + } + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithJcrPathInJoinCriteria() throws RepositoryException { + Query query = session.getWorkspace() + .getQueryManager() + .createQuery("select base.[jcr:primaryType], base.[jcr:path], car.[car:year] " + + "FROM [nt:base] AS base JOIN [car:Car] AS car ON car.[jcr:path] = base.[jcr:path]", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + // print = true; + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 13); + assertResultsHaveColumns(result, new String[] {"jcr:primaryType", "jcr:path", "car:year"}); + RowIterator iter = result.getRows(); + while (iter.hasNext()) { + Row row = iter.nextRow(); + assertThat(row, is(notNullValue())); + } + } + + @FixFor( "MODE-934" ) + @Test + public void shouldNotIncludePseudoColumnsInSelectStar() throws RepositoryException { + Query query = session.getWorkspace().getQueryManager().createQuery("select * FROM [nt:base]", Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + // print = true; + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 24); + assertResultsHaveColumns(result, new String[] {"jcr:primaryType"}); + RowIterator iter = result.getRows(); + while (iter.hasNext()) { + Row row = iter.nextRow(); + assertThat(row, is(notNullValue())); + } + } + // ---------------------------------------------------------------------------------------------------------------- // Full-text Search Queries // ---------------------------------------------------------------------------------------------------------------- @@ -561,7 +685,7 @@ public class JcrQueryManagerTest { Query query = session.getWorkspace().getQueryManager().createQuery("land", JcrRepository.QueryLanguage.SEARCH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - print = true; + // print = true; assertThat(result, is(notNullValue())); assertResults(query, result, 3); assertResultsHaveColumns(result, searchColumnNames()); Index: modeshape-jcr/src/test/java/org/modeshape/jcr/query/JcrSql2QueryParserTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/query/JcrSql2QueryParserTest.java (revision 2338) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/query/JcrSql2QueryParserTest.java (working copy) @@ -36,6 +36,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.modeshape.common.FixFor; import org.modeshape.common.text.TokenStream; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.property.Name; @@ -317,6 +318,126 @@ public class JcrSql2QueryParserTest { assertThat(joinCondition.descendantSelectorName(), is(selectorName("lang"))); } + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedPathInSelect() { + query = parse("select [jcr:primaryType], [jcr:path] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeNameInSelect() { + query = parse("select [jcr:primaryType], [jcr:name] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeLocalNameInSelect() { + query = parse("select [jcr:primaryType], [jcr:localName] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeDepthInSelect() { + query = parse("select [jcr:primaryType], [jcr:depth] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeScoreInSelect() { + query = parse("select [jcr:primaryType], [jcr:score] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedPathInSelect() { + query = parse("select [nt:base].[jcr:primaryType], [nt:base].[jcr:path] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeNameInSelect() { + query = parse("select [nt:base].[jcr:primaryType], [nt:base].[jcr:name] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeLocalNameInSelect() { + query = parse("select [nt:base].[jcr:primaryType], [nt:base].[jcr:localName] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeDepthInSelect() { + query = parse("select [nt:base].[jcr:primaryType], [nt:base].[jcr:depth] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeScoreInSelect() { + query = parse("select [nt:base].[jcr:primaryType], [nt:base].[jcr:score] FROM [nt:base]"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedPathInCriteria() { + query = parse("select [jcr:primaryType] FROM [nt:base] WHERE [jcr:path] = '/some/path'"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeNameInCriteria() { + query = parse("select [jcr:primaryType] FROM [nt:base] WHERE [jcr:path] = 'mode:nodeName'"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeLocalNameInCriteria() { + query = parse("select [jcr:primaryType] FROM [nt:base] WHERE [jcr:localName] = 'nodeName'"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeDepthInCriteria() { + query = parse("select [jcr:primaryType] FROM [nt:base] WHERE [jcr:depth] = 2"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithUnqualifiedNodeScoreInCriteria() { + query = parse("select [jcr:primaryType] FROM [nt:base] WHERE [jcr:score] <= 2.0"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedPathInCriteria() { + query = parse("select [nt:base].[jcr:primaryType] FROM [nt:base] WHERE [nt:base].[jcr:path] = '/some/path'"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeNameInCriteria() { + query = parse("select [nt:base].[jcr:primaryType] FROM [nt:base] WHERE [nt:base].[jcr:name] = 'mode:nodeName'"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeLocalNameInCriteria() { + query = parse("select [nt:base].[jcr:primaryType] FROM [nt:base] WHERE [nt:base].[jcr:localName] = 'nodeName'"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeDepthInCriteria() { + query = parse("select [nt:base].[jcr:primaryType] FROM [nt:base] WHERE [nt:base].[jcr:depth] = 3"); + } + + @FixFor( "MODE-934" ) + @Test + public void shouldParseQueryWithQualifiedNodeScoreInCriteria() { + query = parse("select [nt:base].[jcr:primaryType] FROM [nt:base] WHERE [nt:base].[jcr:score] <= 1.3"); + } + protected Join isJoin( Source source ) { assertThat(source, is(instanceOf(Join.class))); return (Join)source;