From 86062c80f3fe22683d264716c0368be20efab22c Mon Sep 17 00:00:00 2001 From: Joan Augsburger Date: Mon, 9 Nov 2020 15:20:53 +0000 Subject: [PATCH] chore(sql): column names are no longer case-sensitive --- .../questdb/cairo/GenericRecordMetadata.java | 14 +- .../io/questdb/griffin/SqlCodeGenerator.java | 2 +- .../java/io/questdb/griffin/SqlOptimiser.java | 22 +- .../java/io/questdb/griffin/SqlParser.java | 10 +- .../main/java/io/questdb/griffin/SqlUtil.java | 2 +- .../AttributeCatalogueFunctionFactory.java | 241 ++++++++++++++++++ .../catalogue/IndexCatalogueCursor.java | 75 ++++++ .../IndexCatalogueFunctionFactory.java | 50 ++++ .../catalogue/InformationSchemaCursor.java | 73 ++++++ .../InformationSchemaFunctionFactory.java | 50 ++++ .../io/questdb/griffin/model/QueryModel.java | 26 +- .../AbstractLowerCaseCharSequenceHashSet.java | 174 +++++++++++++ core/src/main/java/io/questdb/std/Chars.java | 75 +++++- .../std/LowerCaseCharSequenceHashSet.java | 103 ++++++++ .../std/LowerCaseCharSequenceIntHashMap.java | 147 +++++++++++ .../std/LowerCaseCharSequenceObjHashMap.java | 162 ++++++++++++ core/src/main/java/module-info.java | 3 + .../io.questdb.griffin.FunctionFactory | 29 ++- .../cairo/GenericRecordMetadataTest.java | 25 ++ ...AttributeCatalogueFunctionFactoryTest.java | 231 +++++++++++++++++ .../IndexCatalogueFunctionFactoryTest.java | 44 ++++ .../InformationSchemaFunctionFactoryTest.java | 44 ++++ .../std/LowerCaseCharSequenceHashSetTest.java | 97 +++++++ .../LowerCaseCharSequenceIntHashMapTest.java | 105 ++++++++ .../LowerCaseCharSequenceObjHashMapTest.java | 105 ++++++++ 25 files changed, 1871 insertions(+), 38 deletions(-) create mode 100644 core/src/main/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactory.java create mode 100644 core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueCursor.java create mode 100644 core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactory.java create mode 100644 core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaCursor.java create mode 100644 core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactory.java create mode 100644 core/src/main/java/io/questdb/std/AbstractLowerCaseCharSequenceHashSet.java create mode 100644 core/src/main/java/io/questdb/std/LowerCaseCharSequenceHashSet.java create mode 100644 core/src/main/java/io/questdb/std/LowerCaseCharSequenceIntHashMap.java create mode 100644 core/src/main/java/io/questdb/std/LowerCaseCharSequenceObjHashMap.java create mode 100644 core/src/test/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactoryTest.java create mode 100644 core/src/test/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactoryTest.java create mode 100644 core/src/test/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactoryTest.java create mode 100644 core/src/test/java/io/questdb/std/LowerCaseCharSequenceHashSetTest.java create mode 100644 core/src/test/java/io/questdb/std/LowerCaseCharSequenceIntHashMapTest.java create mode 100644 core/src/test/java/io/questdb/std/LowerCaseCharSequenceObjHashMapTest.java diff --git a/core/src/main/java/io/questdb/cairo/GenericRecordMetadata.java b/core/src/main/java/io/questdb/cairo/GenericRecordMetadata.java index f47f5d639..af92021e9 100644 --- a/core/src/main/java/io/questdb/cairo/GenericRecordMetadata.java +++ b/core/src/main/java/io/questdb/cairo/GenericRecordMetadata.java @@ -25,15 +25,16 @@ package io.questdb.cairo; import io.questdb.cairo.sql.RecordMetadata; -import io.questdb.std.CharSequenceIntHashMap; +import io.questdb.std.LowerCaseCharSequenceIntHashMap; import io.questdb.std.ObjList; public class GenericRecordMetadata extends BaseRecordMetadata { public static final GenericRecordMetadata EMPTY = new GenericRecordMetadata(); + private final LowerCaseCharSequenceIntHashMap columnNameIndexMap; public GenericRecordMetadata() { this.columnMetadata = new ObjList<>(); - this.columnNameIndexMap = new CharSequenceIntHashMap(); + this.columnNameIndexMap = new LowerCaseCharSequenceIntHashMap(); this.timestampIndex = -1; } @@ -49,6 +50,15 @@ public class GenericRecordMetadata extends BaseRecordMetadata { } } + @Override + public int getColumnIndexQuiet(CharSequence columnName, int lo, int hi) { + final int index = columnNameIndexMap.keyIndex(columnName, lo, hi); + if (index < 0) { + return columnNameIndexMap.valueAt(index); + } + return -1; + } + public static GenericRecordMetadata copyOf(RecordMetadata that) { GenericRecordMetadata metadata = copyOfSansTimestamp(that); metadata.setTimestampIndex(that.getTimestampIndex()); diff --git a/core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java b/core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java index 13dba2359..20a9b9784 100644 --- a/core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java +++ b/core/src/main/java/io/questdb/griffin/SqlCodeGenerator.java @@ -1050,7 +1050,7 @@ public class SqlCodeGenerator implements Mutable { return recordCursorFactory; } try { - final CharSequenceIntHashMap orderBy = model.getOrderHash(); + final LowerCaseCharSequenceIntHashMap orderBy = model.getOrderHash(); final ObjList columnNames = orderBy.keys(); final int size = columnNames.size(); diff --git a/core/src/main/java/io/questdb/griffin/SqlOptimiser.java b/core/src/main/java/io/questdb/griffin/SqlOptimiser.java index b27d742ad..c9842373e 100644 --- a/core/src/main/java/io/questdb/griffin/SqlOptimiser.java +++ b/core/src/main/java/io/questdb/griffin/SqlOptimiser.java @@ -668,7 +668,7 @@ class SqlOptimiser { // order hash is used to determine redundant order when parsing analytic function definition private void createOrderHash(QueryModel model) { - CharSequenceIntHashMap hash = model.getOrderHash(); + LowerCaseCharSequenceIntHashMap hash = model.getOrderHash(); hash.clear(); final ObjList orderBy = model.getOrderBy(); @@ -687,7 +687,7 @@ class SqlOptimiser { if (nestedModel != null) { createOrderHash(nestedModel); if (m > 0) { - CharSequenceIntHashMap thatHash = nestedModel.getOrderHash(); + LowerCaseCharSequenceIntHashMap thatHash = nestedModel.getOrderHash(); if (thatHash.size() > 0) { for (int i = 0; i < m; i++) { QueryColumn column = columns.getQuick(i); @@ -729,7 +729,7 @@ class SqlOptimiser { // taking into account that column is pre-aliased, e.g. // "col, col" will look like "col, col col1" - CharSequenceObjHashMap translatingAliasMap = translatingModel.getColumnNameToAliasMap(); + LowerCaseCharSequenceObjHashMap translatingAliasMap = translatingModel.getColumnNameToAliasMap(); int index = translatingAliasMap.keyIndex(columnAst.token); if (index < 0) { // column is already being referenced by translating model @@ -914,7 +914,7 @@ class SqlOptimiser { } private ExpressionNode doReplaceLiteral(@Transient ExpressionNode node, QueryModel translatingModel, QueryModel innerModel, QueryModel validatingModel) throws SqlException { - final CharSequenceObjHashMap map = translatingModel.getColumnNameToAliasMap(); + final LowerCaseCharSequenceObjHashMap map = translatingModel.getColumnNameToAliasMap(); int index = map.keyIndex(node.token); if (index > -1) { // there is a possibility that column references join table, but in a different way @@ -1210,7 +1210,7 @@ class SqlOptimiser { return orderByAdvice; } - CharSequenceObjHashMap map = model.getAliasToColumnMap(); + LowerCaseCharSequenceObjHashMap map = model.getAliasToColumnMap(); for (int i = 0; i < len; i++) { QueryColumn queryColumn = map.get(orderBy.getQuick(i).token); if (queryColumn.getAst().type == ExpressionNode.LITERAL) { @@ -2277,7 +2277,7 @@ class SqlOptimiser { if (ascendColumns && base != model) { // check if column is aliased as either // "x y" or "tab.x y" or "t.x y", where "t" is alias of table "tab" - final CharSequenceObjHashMap map = baseParent.getColumnNameToAliasMap(); + final LowerCaseCharSequenceObjHashMap map = baseParent.getColumnNameToAliasMap(); CharSequence alias = null; int index = map.keyIndex(column); if (index > -1 && dot > -1) { @@ -2496,7 +2496,7 @@ class SqlOptimiser { if (flatModel) { if (flatParent && m.getSampleBy() != null) { - throw SqlException.$(m.getSampleBy().position, "'sample by' must be used with 'select' clause, which contains aggerate expression(s)"); + throw SqlException.$(m.getSampleBy().position, "'sample by' must be used with 'select' clause, which contains aggregate expression(s)"); } } else { model.replaceJoinModel(i, rewriteSelectClause0(m)); @@ -2828,7 +2828,7 @@ class SqlOptimiser { } private static class LiteralCheckingVisitor implements PostOrderTreeTraversalAlgo.Visitor { - private CharSequenceObjHashMap nameTypeMap; + private LowerCaseCharSequenceObjHashMap nameTypeMap; @Override public void visit(ExpressionNode node) { @@ -2846,14 +2846,14 @@ class SqlOptimiser { } } - PostOrderTreeTraversalAlgo.Visitor of(CharSequenceObjHashMap nameTypeMap) { + PostOrderTreeTraversalAlgo.Visitor of(LowerCaseCharSequenceObjHashMap nameTypeMap) { this.nameTypeMap = nameTypeMap; return this; } } private static class LiteralRewritingVisitor implements PostOrderTreeTraversalAlgo.Visitor { - private CharSequenceObjHashMap aliasToColumnMap; + private LowerCaseCharSequenceObjHashMap aliasToColumnMap; @Override public void visit(ExpressionNode node) { @@ -2873,7 +2873,7 @@ class SqlOptimiser { } } - PostOrderTreeTraversalAlgo.Visitor of(CharSequenceObjHashMap aliasToColumnMap) { + PostOrderTreeTraversalAlgo.Visitor of(LowerCaseCharSequenceObjHashMap aliasToColumnMap) { this.aliasToColumnMap = aliasToColumnMap; return this; } diff --git a/core/src/main/java/io/questdb/griffin/SqlParser.java b/core/src/main/java/io/questdb/griffin/SqlParser.java index ad989ca63..e8efddd0e 100644 --- a/core/src/main/java/io/questdb/griffin/SqlParser.java +++ b/core/src/main/java/io/questdb/griffin/SqlParser.java @@ -328,7 +328,7 @@ public final class SqlParser { return parseSelect(lexer); } - QueryModel parseAsSubQuery(GenericLexer lexer, @Nullable CharSequenceObjHashMap withClauses) throws SqlException { + QueryModel parseAsSubQuery(GenericLexer lexer, @Nullable LowerCaseCharSequenceObjHashMap withClauses) throws SqlException { QueryModel model; this.subQueryMode = true; try { @@ -339,7 +339,7 @@ public final class SqlParser { return model; } - private QueryModel parseAsSubQueryAndExpectClosingBrace(GenericLexer lexer, CharSequenceObjHashMap withClauses) throws SqlException { + private QueryModel parseAsSubQueryAndExpectClosingBrace(GenericLexer lexer, LowerCaseCharSequenceObjHashMap withClauses) throws SqlException { final QueryModel model = parseAsSubQuery(lexer, withClauses); expectTok(lexer, ')'); return model; @@ -655,7 +655,7 @@ public final class SqlParser { return null; } - private QueryModel parseDml(GenericLexer lexer, @Nullable CharSequenceObjHashMap withClauses) throws SqlException { + private QueryModel parseDml(GenericLexer lexer, @Nullable LowerCaseCharSequenceObjHashMap withClauses) throws SqlException { QueryModel model = null; QueryModel prevModel = null; while (true) { @@ -823,7 +823,7 @@ public final class SqlParser { } @NotNull - private QueryModel parseDml0(GenericLexer lexer, @Nullable CharSequenceObjHashMap parentWithClauses) throws SqlException { + private QueryModel parseDml0(GenericLexer lexer, @Nullable LowerCaseCharSequenceObjHashMap parentWithClauses) throws SqlException { CharSequence tok; final int modelPosition = lexer.getPosition(); @@ -1274,7 +1274,7 @@ public final class SqlParser { return null; } - private QueryModel parseWith(GenericLexer lexer, WithClauseModel wcm, CharSequenceObjHashMap withClauses) throws SqlException { + private QueryModel parseWith(GenericLexer lexer, WithClauseModel wcm, LowerCaseCharSequenceObjHashMap withClauses) throws SqlException { QueryModel m = wcm.popModel(); if (m != null) { return m; diff --git a/core/src/main/java/io/questdb/griffin/SqlUtil.java b/core/src/main/java/io/questdb/griffin/SqlUtil.java index b1c2b17d6..457c127f6 100644 --- a/core/src/main/java/io/questdb/griffin/SqlUtil.java +++ b/core/src/main/java/io/questdb/griffin/SqlUtil.java @@ -81,7 +81,7 @@ public class SqlUtil { CharacterStore store, CharSequence base, int indexOfDot, - CharSequenceObjHashMap aliasToColumnMap + LowerCaseCharSequenceObjHashMap aliasToColumnMap ) { final boolean disallowed = disallowedAliases.contains(base); diff --git a/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactory.java b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactory.java new file mode 100644 index 000000000..cbc8e30f9 --- /dev/null +++ b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactory.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.cairo.*; +import io.questdb.cairo.sql.*; +import io.questdb.griffin.FunctionFactory; +import io.questdb.griffin.SqlExecutionContext; +import io.questdb.griffin.engine.functions.CursorFunction; +import io.questdb.std.*; +import io.questdb.std.str.NativeLPSZ; +import io.questdb.std.str.Path; + +public class AttributeCatalogueFunctionFactory implements FunctionFactory { + + private static final RecordMetadata METADATA; + + @Override + public String getSignature() { + return "pg_catalog.pg_attribute()"; + } + + @Override + public Function newInstance(ObjList args, int position, CairoConfiguration configuration) { + return new CursorFunction( + position, + new AttributeCatalogueCursorFactory( + configuration, + METADATA + ) + ); + } + + private static class AttributeCatalogueCursorFactory extends AbstractRecordCursorFactory { + + private final Path path = new Path(); + private final ReadOnlyColumn metaMem = new OnePageMemory(); + private final AttributeClassCatalogueCursor cursor; + + public AttributeCatalogueCursorFactory(CairoConfiguration configuration, RecordMetadata metadata) { + super(metadata); + this.cursor = new AttributeClassCatalogueCursor(configuration, path, metaMem); + } + + @Override + public void close() { + Misc.free(path); + Misc.free(metaMem); + } + + @Override + public RecordCursor getCursor(SqlExecutionContext executionContext) { + cursor.toTop(); + return cursor; + } + + @Override + public boolean recordCursorSupportsRandomAccess() { + return false; + } + } + + private static class AttributeClassCatalogueCursor implements NoRandomAccessRecordCursor { + private final Path path; + private final FilesFacade ff; + private final DiskReadingRecord diskReadingRecord = new DiskReadingRecord(); + private final NativeLPSZ nativeLPSZ = new NativeLPSZ(); + private final int plimit; + private final ReadOnlyColumn metaMem; + private long findFileStruct = 0; + private int columnIndex = 0; + private int tableId = 1000; + private boolean readNextFileFromDisk = true; + private int columnCount; + private boolean hasNextFile = true; + private boolean foundMetadataFile = false; + + public AttributeClassCatalogueCursor(CairoConfiguration configuration, Path path, ReadOnlyColumn metaMem) { + this.ff = configuration.getFilesFacade(); + this.path = path; + this.path.of(configuration.getRoot()).$(); + this.plimit = this.path.length(); + this.metaMem = metaMem; + } + + @Override + public void close() { + if (findFileStruct != 0) { + ff.findClose(findFileStruct); + findFileStruct = 0; + } + metaMem.close(); + } + + @Override + public Record getRecord() { + return diskReadingRecord; + } + + @Override + public boolean hasNext() { + if (findFileStruct == 0) { + findFileStruct = ff.findFirst(path.trimTo(plimit).$()); + if (findFileStruct > 0) { + return next0(); + } + + findFileStruct = 0; + return false; + } + + return next0(); + } + + @Override + public void toTop() { + if (findFileStruct != 0) { + ff.findClose(findFileStruct); + findFileStruct = 0; + } + } + + @Override + public long size() { + return -1; + } + + private boolean next0() { + do { + if (readNextFileFromDisk) { + foundMetadataFile = false; + final long pname = ff.findName(findFileStruct); + if (hasNextFile) { + nativeLPSZ.of(pname); + if ( + ff.findType(findFileStruct) == Files.DT_DIR && Chars.notDots(nativeLPSZ) + ) { + path.trimTo(plimit); + path.concat(pname); + if (ff.exists(path.concat(TableUtils.META_FILE_NAME).$())) { + foundMetadataFile = true; + metaMem.of(ff, path, ff.getPageSize(), ff.length(path)); + columnCount = metaMem.getInt(TableUtils.META_OFFSET_COUNT); + tableId = metaMem.getInt(TableUtils.META_OFFSET_TABLE_ID); + } + } + hasNextFile = ff.findNext(findFileStruct) > 0; + } + } + + if (foundMetadataFile) { + long offset = TableUtils.getColumnNameOffset(columnCount); + for (int i = 0; i < columnCount; i++) { + CharSequence name = metaMem.getStr(offset); + if (columnIndex == i) { + diskReadingRecord.name = name; + diskReadingRecord.columnNumber = (short) (i + 1); + diskReadingRecord.tableId = tableId; + columnIndex++; + if (columnIndex == columnCount) { + readNextFileFromDisk = true; + columnIndex = 0; + } else { + readNextFileFromDisk = false; + } + return true; + } + offset += ReadOnlyMemory.getStorageLength(name); + } + } + } while (hasNextFile); + + ff.findClose(findFileStruct); + findFileStruct = 0; + hasNextFile = true; + foundMetadataFile = false; + return false; + } + + private static class DiskReadingRecord implements Record { + public CharSequence name = null; + public short columnNumber = 0; + public int tableId = 0; + + @Override + public short getShort(int col) { + return columnNumber; + } + + @Override + public int getInt(int col) { + return tableId; + } + + @Override + public CharSequence getStr(int col) { + return name; + } + + @Override + public CharSequence getStrB(int col) { + return name; + } + + @Override + public int getStrLen(int col) { + return getStr(col).length(); + } + } + } + + static { + final GenericRecordMetadata metadata = new GenericRecordMetadata(); + metadata.add(new TableColumnMetadata("attrelid", ColumnType.INT)); + metadata.add(new TableColumnMetadata("attname", ColumnType.STRING)); + metadata.add(new TableColumnMetadata("attnum", ColumnType.SHORT)); + METADATA = metadata; + } +} diff --git a/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueCursor.java b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueCursor.java new file mode 100644 index 000000000..9e100c8ce --- /dev/null +++ b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueCursor.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.cairo.ColumnType; +import io.questdb.cairo.GenericRecordMetadata; +import io.questdb.cairo.TableColumnMetadata; +import io.questdb.cairo.sql.NoRandomAccessRecordCursor; +import io.questdb.cairo.sql.Record; +import io.questdb.cairo.sql.RecordMetadata; + +class IndexCatalogueCursor implements NoRandomAccessRecordCursor { + + static final RecordMetadata METADATA; + private static final IndexCatalogueRecord record = new IndexCatalogueRecord(); + + @Override + public void close() { + + } + + @Override + public Record getRecord() { + return record; + } + + @Override + public boolean hasNext() { + return false; + } + + @Override + public void toTop() { + + } + + @Override + public long size() { + return 0; + } + + private static class IndexCatalogueRecord implements Record { + } + + static { + final GenericRecordMetadata metadata = new GenericRecordMetadata(); + metadata.add(new TableColumnMetadata("indkey", ColumnType.INT)); + metadata.add(new TableColumnMetadata("indrelid", ColumnType.INT)); + metadata.add(new TableColumnMetadata("indexrelid", ColumnType.INT)); + metadata.add(new TableColumnMetadata("indisprimary", ColumnType.BOOLEAN)); + METADATA = metadata; + } +} diff --git a/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactory.java b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactory.java new file mode 100644 index 000000000..fb14a3d1d --- /dev/null +++ b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactory.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.cairo.CairoConfiguration; +import io.questdb.cairo.sql.Function; +import io.questdb.griffin.FunctionFactory; +import io.questdb.griffin.engine.functions.CursorFunction; +import io.questdb.griffin.engine.functions.GenericRecordCursorFactory; +import io.questdb.std.ObjList; + +public class IndexCatalogueFunctionFactory implements FunctionFactory { + @Override + public String getSignature() { + return "pg_catalog.pg_index()"; + } + + public Function newInstance(ObjList args, int position, CairoConfiguration configuration) { + return new CursorFunction( + position, + new GenericRecordCursorFactory( + IndexCatalogueCursor.METADATA, + new IndexCatalogueCursor(), + false + ) + ); + } +} diff --git a/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaCursor.java b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaCursor.java new file mode 100644 index 000000000..2b37d07b7 --- /dev/null +++ b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaCursor.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.cairo.ColumnType; +import io.questdb.cairo.GenericRecordMetadata; +import io.questdb.cairo.TableColumnMetadata; +import io.questdb.cairo.sql.NoRandomAccessRecordCursor; +import io.questdb.cairo.sql.Record; +import io.questdb.cairo.sql.RecordMetadata; + +class InformationSchemaCursor implements NoRandomAccessRecordCursor { + + static final RecordMetadata METADATA; + private static final InformationSchemaRecord record = new InformationSchemaRecord(); + + @Override + public void close() { + + } + + @Override + public Record getRecord() { + return record; + } + + @Override + public boolean hasNext() { + return false; + } + + @Override + public void toTop() { + + } + + @Override + public long size() { + return 0; + } + + private static class InformationSchemaRecord implements Record { + } + + static { + final GenericRecordMetadata metadata = new GenericRecordMetadata(); + metadata.add(new TableColumnMetadata("x", ColumnType.INT)); + metadata.add(new TableColumnMetadata("n", ColumnType.INT)); + METADATA = metadata; + } +} diff --git a/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactory.java b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactory.java new file mode 100644 index 000000000..ccc6fabf1 --- /dev/null +++ b/core/src/main/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactory.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.cairo.CairoConfiguration; +import io.questdb.cairo.sql.Function; +import io.questdb.griffin.FunctionFactory; +import io.questdb.griffin.engine.functions.CursorFunction; +import io.questdb.griffin.engine.functions.GenericRecordCursorFactory; +import io.questdb.std.ObjList; + +public class InformationSchemaFunctionFactory implements FunctionFactory { + @Override + public String getSignature() { + return "information_schema._pg_expandarray(I)"; + } + + public Function newInstance(ObjList args, int position, CairoConfiguration configuration) { + return new CursorFunction( + position, + new GenericRecordCursorFactory( + InformationSchemaCursor.METADATA, + new InformationSchemaCursor(), + false + ) + ); + } +} diff --git a/core/src/main/java/io/questdb/griffin/model/QueryModel.java b/core/src/main/java/io/questdb/griffin/model/QueryModel.java index 87f6201dc..5c247f890 100644 --- a/core/src/main/java/io/questdb/griffin/model/QueryModel.java +++ b/core/src/main/java/io/questdb/griffin/model/QueryModel.java @@ -56,11 +56,11 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin public static final int SET_OPERATION_INTERSECT = 3; private static final ObjList modelTypeName = new ObjList<>(); private final ObjList bottomUpColumns = new ObjList<>(); - private final CharSequenceHashSet topDownNameSet = new CharSequenceHashSet(); + private final LowerCaseCharSequenceHashSet topDownNameSet = new LowerCaseCharSequenceHashSet(); private final ObjList topDownColumns = new ObjList<>(); - private final CharSequenceObjHashMap aliasToColumnNameMap = new CharSequenceObjHashMap<>(); - private final CharSequenceObjHashMap columnNameToAliasMap = new CharSequenceObjHashMap<>(); - private final CharSequenceObjHashMap aliasToColumnMap = new CharSequenceObjHashMap<>(); + private final LowerCaseCharSequenceObjHashMap aliasToColumnNameMap = new LowerCaseCharSequenceObjHashMap<>(); + private final LowerCaseCharSequenceObjHashMap columnNameToAliasMap = new LowerCaseCharSequenceObjHashMap<>(); + private final LowerCaseCharSequenceObjHashMap aliasToColumnMap = new LowerCaseCharSequenceObjHashMap<>(); private final ObjList bottomUpColumnNames = new ObjList<>(); private final ObjList joinModels = new ObjList<>(); private final ObjList orderBy = new ObjList<>(); @@ -69,7 +69,7 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin private final IntHashSet dependencies = new IntHashSet(); private final IntList orderedJoinModels1 = new IntList(); private final IntList orderedJoinModels2 = new IntList(); - private final CharSequenceIntHashMap aliasIndexes = new CharSequenceIntHashMap(); + private final LowerCaseCharSequenceIntHashMap aliasIndexes = new LowerCaseCharSequenceIntHashMap(); private final ObjList expressionModels = new ObjList<>(); // collect frequency of column names from each join model // and check if any of columns with frequency > 0 are selected @@ -79,9 +79,9 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin private final ObjList parsedWhere = new ObjList<>(); private final IntHashSet parsedWhereConsts = new IntHashSet(); private final ArrayDeque sqlNodeStack = new ArrayDeque<>(); - private final CharSequenceIntHashMap orderHash = new CharSequenceIntHashMap(4, 0.5, -1); + private final LowerCaseCharSequenceIntHashMap orderHash = new LowerCaseCharSequenceIntHashMap(4, 0.5, -1); private final ObjList joinColumns = new ObjList<>(4); - private final CharSequenceObjHashMap withClauses = new CharSequenceObjHashMap<>(); + private final LowerCaseCharSequenceObjHashMap withClauses = new LowerCaseCharSequenceObjHashMap<>(); private final ObjList sampleByFill = new ObjList<>(); private final ObjList latestBy = new ObjList<>(); private final ObjList orderByAdvice = new ObjList<>(); @@ -182,7 +182,7 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin withClauses.put(name, model); } - public void addWithClauses(CharSequenceObjHashMap parentWithClauses) { + public void addWithClauses(LowerCaseCharSequenceObjHashMap parentWithClauses) { withClauses.putAll(parentWithClauses); } @@ -288,11 +288,11 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin return -1; } - public CharSequenceObjHashMap getAliasToColumnMap() { + public LowerCaseCharSequenceObjHashMap getAliasToColumnMap() { return aliasToColumnMap; } - public CharSequenceObjHashMap getAliasToColumnNameMap() { + public LowerCaseCharSequenceObjHashMap getAliasToColumnNameMap() { return aliasToColumnNameMap; } @@ -304,7 +304,7 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin return bottomUpColumns; } - public CharSequenceObjHashMap getColumnNameToAliasMap() { + public LowerCaseCharSequenceObjHashMap getColumnNameToAliasMap() { return columnNameToAliasMap; } @@ -441,7 +441,7 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin return orderByDirectionAdvice; } - public CharSequenceIntHashMap getOrderHash() { + public LowerCaseCharSequenceIntHashMap getOrderHash() { return orderHash; } @@ -550,7 +550,7 @@ public class QueryModel implements Mutable, ExecutionModel, AliasTranslator, Sin return withClauses.get(name); } - public CharSequenceObjHashMap getWithClauses() { + public LowerCaseCharSequenceObjHashMap getWithClauses() { return withClauses; } diff --git a/core/src/main/java/io/questdb/std/AbstractLowerCaseCharSequenceHashSet.java b/core/src/main/java/io/questdb/std/AbstractLowerCaseCharSequenceHashSet.java new file mode 100644 index 000000000..f40ae1186 --- /dev/null +++ b/core/src/main/java/io/questdb/std/AbstractLowerCaseCharSequenceHashSet.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + +import java.util.Arrays; + +public abstract class AbstractLowerCaseCharSequenceHashSet implements Mutable { + protected static final CharSequence noEntryKey = null; + protected static final int MIN_INITIAL_CAPACITY = 16; + protected final double loadFactor; + protected int mask; + protected int free; + protected int capacity; + // exposed for testing only + protected CharSequence[] keys; + + public AbstractLowerCaseCharSequenceHashSet(int initialCapacity, double loadFactor) { + if (loadFactor <= 0d || loadFactor >= 1d) { + throw new IllegalArgumentException("0 < loadFactor < 1"); + } + + free = this.capacity = Math.max(initialCapacity, MIN_INITIAL_CAPACITY); + this.loadFactor = loadFactor; + keys = new CharSequence[Numbers.ceilPow2((int) (this.capacity / loadFactor))]; + mask = keys.length - 1; + } + + @Override + public void clear() { + Arrays.fill(keys, noEntryKey); + free = this.capacity; + } + + public boolean excludes(CharSequence key) { + return keyIndex(key) > -1; + } + + public boolean excludes(CharSequence key, int lo, int hi) { + return keyIndex(key, lo, hi) > -1; + } + + public int keyIndex(CharSequence key) { + int index = Chars.lowerCaseHashCode(key) & mask; + + if (keys[index] == noEntryKey) { + return index; + } + + if (Chars.equalsLowerCase(key, keys[index])) { + return -index - 1; + } + + return probe(key, index); + } + + public int keyIndex(CharSequence key, int lo, int hi) { + int index = Chars.lowerCaseHashCode(key, lo, hi) & mask; + + if (keys[index] == noEntryKey) { + return index; + } + + CharSequence cs = keys[index]; + if (Chars.equalsLowerCase(key, lo, hi, cs, 0, cs.length())) { + return -index - 1; + } + return probe(key, lo, hi, index); + } + + public int remove(CharSequence key) { + int index = keyIndex(key); + if (index < 0) { + removeAt(index); + return -index - 1; + } + return -1; + } + + public void removeAt(int index) { + if (index < 0) { + int from = -index - 1; + erase(from); + free++; + + // after we have freed up a slot + // consider non-empty keys directly below + // they may have been a direct hit but because + // directly hit slot wasn't empty these keys would + // have moved. + // + // After slot if freed these keys require re-hash + from = (from + 1) & mask; + for ( + CharSequence key = keys[from]; + key != noEntryKey; + from = (from + 1) & mask, key = keys[from] + ) { + int idealHit = Chars.lowerCaseHashCode(key) & mask; + if (idealHit != from) { + int to; + if (keys[idealHit] != noEntryKey) { + to = probe(key, idealHit); + } else { + to = idealHit; + } + + if (to > -1) { + move(from, to); + } + } + } + } + } + + public int size() { + return capacity - free; + } + + /** + * Erases entry in array. + * + * @param index always positive, no arithmetic required. + */ + abstract protected void erase(int index); + + abstract protected void move(int from, int to); + + private int probe(CharSequence key, int index) { + do { + index = (index + 1) & mask; + if (keys[index] == noEntryKey) { + return index; + } + if (Chars.equalsLowerCase(key, keys[index])) { + return -index - 1; + } + } while (true); + } + + private int probe(CharSequence key, int lo, int hi, int index) { + do { + index = (index + 1) & mask; + if (keys[index] == noEntryKey) { + return index; + } + CharSequence cs = keys[index]; + if (Chars.equalsLowerCase(key, lo, hi, cs, 0, cs.length())) { + return -index - 1; + } + } while (true); + } +} diff --git a/core/src/main/java/io/questdb/std/Chars.java b/core/src/main/java/io/questdb/std/Chars.java index a2ae619fd..46cfc66ec 100644 --- a/core/src/main/java/io/questdb/std/Chars.java +++ b/core/src/main/java/io/questdb/std/Chars.java @@ -230,6 +230,39 @@ public final class Chars { return l != null && equalsIgnoreCase(l, r); } + public static boolean equalsLowerCase(CharSequence l, int lLo, int lHi, CharSequence r, int rLo, int rHi) { + if (l == r) { + return true; + } + + int ll = lHi - lLo; + if (ll != rHi - rLo) { + return false; + } + + for (int i = 0; i < ll; i++) { + if (Character.toLowerCase(l.charAt(i + lLo)) != Character.toLowerCase(r.charAt(i + rLo))) { + return false; + } + } + return true; + } + + public static boolean equalsLowerCase(@NotNull CharSequence l, CharSequence r) { + int ll; + if ((ll = l.length()) != r.length()) { + return false; + } + + for (int i = 0; i < ll; i++) { + if (Character.toLowerCase(l.charAt(i)) != Character.toLowerCase(r.charAt(i))) { + return false; + } + } + + return true; + } + public static boolean equalsLowerCaseAscii(CharSequence l, int lLo, int lHi, CharSequence r, int rLo, int rHi) { if (l == r) { return true; @@ -241,7 +274,7 @@ public final class Chars { } for (int i = 0; i < ll; i++) { - if (toLowerCaseAscii(l.charAt(i + lLo)) != r.charAt(i + rLo)) { + if (toLowerCaseAscii(l.charAt(i + lLo)) != toLowerCaseAscii(r.charAt(i + rLo))) { return false; } } @@ -255,7 +288,7 @@ public final class Chars { } for (int i = 0; i < ll; i++) { - if (toLowerCaseAscii(l.charAt(i)) != r.charAt(i)) { + if (toLowerCaseAscii(l.charAt(i)) != toLowerCaseAscii(r.charAt(i))) { return false; } } @@ -389,6 +422,31 @@ public final class Chars { return h; } + public static int lowerCaseHashCode(CharSequence value, int lo, int hi) { + if (hi == lo) { + return 0; + } + + int h = 0; + for (int p = lo; p < hi; p++) { + h = 31 * h + Character.toLowerCase(value.charAt(p)); + } + return h; + } + + public static int lowerCaseHashCode(CharSequence value) { + int len = value.length(); + if (len == 0) { + return 0; + } + + int h = 0; + for (int p = 0; p < len; p++) { + h = 31 * h + Character.toLowerCase(value.charAt(p)); + } + return h; + } + public static boolean noMatch(CharSequence l, int llo, int lhi, CharSequence r, int rlo, int rhi) { int lp = llo; int rp = rlo; @@ -498,8 +556,6 @@ public final class Chars { b.put(toLowerCaseAscii(value.charAt(i))); } return b.toString(); - - } public static char toLowerCaseAscii(char character) { @@ -765,4 +821,15 @@ public final class Chars { sink.put((char) (b1 << 6 ^ b2 ^ 3968)); return 2; } + + public static String toLowerCase(CharSequence str) { + final CharSink sink = Misc.getThreadLocalBuilder(); + if (str != null) { + final int len = str.length(); + for (int i = 0; i < len; i++) { + sink.put(Character.toLowerCase(str.charAt(i))); + } + } + return sink.toString(); + } } diff --git a/core/src/main/java/io/questdb/std/LowerCaseCharSequenceHashSet.java b/core/src/main/java/io/questdb/std/LowerCaseCharSequenceHashSet.java new file mode 100644 index 000000000..3dbcdc172 --- /dev/null +++ b/core/src/main/java/io/questdb/std/LowerCaseCharSequenceHashSet.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + +public class LowerCaseCharSequenceHashSet extends AbstractLowerCaseCharSequenceHashSet { + + private static final int MIN_INITIAL_CAPACITY = 16; + + public LowerCaseCharSequenceHashSet() { + this(MIN_INITIAL_CAPACITY); + } + + private LowerCaseCharSequenceHashSet(int initialCapacity) { + this(initialCapacity, 0.4); + } + + private LowerCaseCharSequenceHashSet(int initialCapacity, double loadFactor) { + super(initialCapacity, loadFactor); + clear(); + } + + /** + * Adds key to hash set preserving key uniqueness. + * + * @param key immutable sequence of characters. + * @return false if key is already in the set and true otherwise. + */ + public boolean add(CharSequence key) { + int index = keyIndex(key); + if (index < 0) { + return false; + } + + addAt(index, key); + return true; + } + + public void addAt(int index, CharSequence key) { + keys[index] = key; + if (--free < 1) { + rehash(); + } + } + + public boolean contains(CharSequence key) { + return keyIndex(key) < 0; + } + + public CharSequence keyAt(int index) { + return keys[-index - 1]; + } + + @Override + protected void erase(int index) { + keys[index] = noEntryKey; + } + + @Override + protected void move(int from, int to) { + keys[to] = keys[from]; + erase(from); + } + + private void rehash() { + int newCapacity = capacity * 2; + final int size = size(); + free = capacity = newCapacity; + int len = Numbers.ceilPow2((int) (newCapacity / loadFactor)); + CharSequence[] newKeys = new CharSequence[len]; + CharSequence[] oldKeys = keys; + mask = len - 1; + this.keys = newKeys; + free -= size; + for (int i = 0, n = oldKeys.length; i < n; i++) { + CharSequence key = oldKeys[i]; + if (key != null) { + keys[keyIndex(key)] = key; + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/questdb/std/LowerCaseCharSequenceIntHashMap.java b/core/src/main/java/io/questdb/std/LowerCaseCharSequenceIntHashMap.java new file mode 100644 index 000000000..1ea7bf8b1 --- /dev/null +++ b/core/src/main/java/io/questdb/std/LowerCaseCharSequenceIntHashMap.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + +import java.util.Arrays; + +public class LowerCaseCharSequenceIntHashMap extends AbstractLowerCaseCharSequenceHashSet { + private static final int NO_ENTRY_VALUE = -1; + private final int noEntryValue; + protected int[] values; + private final ObjList list; + + public LowerCaseCharSequenceIntHashMap() { + this(8); + } + + public LowerCaseCharSequenceIntHashMap(int initialCapacity) { + this(initialCapacity, 0.5, NO_ENTRY_VALUE); + } + + public LowerCaseCharSequenceIntHashMap(int initialCapacity, double loadFactor, int noEntryValue) { + super(initialCapacity, loadFactor); + this.noEntryValue = noEntryValue; + this.list = new ObjList<>(capacity); + values = new int[keys.length]; + clear(); + } + + public void clear() { + super.clear(); + list.clear(); + Arrays.fill(values, noEntryValue); + } + + @Override + protected void erase(int index) { + keys[index] = noEntryKey; + values[index] = noEntryValue; + } + + public void removeAt(int index) { + if (index < 0) { + int index1 = -index - 1; + CharSequence key = keys[index1]; + super.removeAt(index); + list.remove(key); + } + } + + public int valueAt(int index) { + return index < 0 ? values[-index - 1] : noEntryValue; + } + + public boolean contains(CharSequence key) { + return keyIndex(key) < 0; + } + + public int get(CharSequence key) { + return valueAt(keyIndex(key)); + } + + public boolean put(CharSequence key, int value) { + return putAt(keyIndex(key), key, value); + } + + public boolean putAt(int index, CharSequence key, int value) { + if (index < 0) { + values[-index - 1] = value; + return false; + } + putAt0(index, key, value); + list.add(key); + return true; + } + + public void putIfAbsent(CharSequence key, int value) { + int index = keyIndex(key); + if (index > -1) { + putAt0(index, key, value); + } + } + + @Override + protected void move(int from, int to) { + keys[to] = keys[from]; + values[to] = values[from]; + erase(from); + } + + protected void putAt0(int index, CharSequence key, int value) { + keys[index] = key; + values[index] = value; + if (--free == 0) { + rehash(); + } + } + + private void rehash() { + int size = size(); + int newCapacity = capacity * 2; + free = capacity = newCapacity; + int len = Numbers.ceilPow2((int) (newCapacity / loadFactor)); + + int[] oldValues = values; + CharSequence[] oldKeys = keys; + this.keys = new CharSequence[len]; + this.values = new int[len]; + Arrays.fill(keys, null); + mask = len - 1; + + free -= size; + for (int i = oldKeys.length; i-- > 0; ) { + CharSequence key = oldKeys[i]; + if (key != null) { + final int index = keyIndex(key); + keys[index] = key; + values[index] = oldValues[i]; + } + } + } + + public ObjList keys() { + return list; + } +} \ No newline at end of file diff --git a/core/src/main/java/io/questdb/std/LowerCaseCharSequenceObjHashMap.java b/core/src/main/java/io/questdb/std/LowerCaseCharSequenceObjHashMap.java new file mode 100644 index 000000000..8df55d054 --- /dev/null +++ b/core/src/main/java/io/questdb/std/LowerCaseCharSequenceObjHashMap.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + + +import java.util.Arrays; + + +public class LowerCaseCharSequenceObjHashMap extends AbstractLowerCaseCharSequenceHashSet { + private final ObjList list; + private T[] values; + + public LowerCaseCharSequenceObjHashMap() { + this(8); + } + + public LowerCaseCharSequenceObjHashMap(int initialCapacity) { + this(initialCapacity, 0.5); + } + + @SuppressWarnings("unchecked") + public LowerCaseCharSequenceObjHashMap(int initialCapacity, double loadFactor) { + super(initialCapacity, loadFactor); + values = (T[]) new Object[keys.length]; + this.list = new ObjList<>(capacity); + clear(); + } + + public final void clear() { + super.clear(); + list.clear(); + Arrays.fill(values, null); + } + + public ObjList keys() { + return list; + } + + @Override + protected void erase(int index) { + keys[index] = noEntryKey; + values[index] = null; + } + + @Override + public void removeAt(int index) { + if (index < 0) { + CharSequence key = keys[-index - 1]; + super.removeAt(index); + list.remove(key); + } + } + + public boolean contains(CharSequence key) { + return keyIndex(key) < 0; + } + + public T get(CharSequence key) { + return valueAt(keyIndex(key)); + } + + @Override + protected void move(int from, int to) { + keys[to] = keys[from]; + values[to] = values[from]; + erase(from); + } + + public boolean put(CharSequence key, T value) { + return putAt(keyIndex(key), key, value); + } + + public boolean putAt(int index, CharSequence key, T value) { + if (index < 0) { + values[-index - 1] = value; + return false; + } + + putAt0(index, key, value); + list.add(key); + return true; + } + + public void putIfAbsent(CharSequence key, T value) { + int index = keyIndex(key); + if (index > -1) { + putAt0(index, key, value); + } + } + + public T valueAt(int index) { + return index < 0 ? valueAtQuick(index) : null; + } + + public T valueAtQuick(int index) { + return values[-index - 1]; + } + + private void putAt0(int index, CharSequence key, T value) { + keys[index] = key; + values[index] = value; + if (--free == 0) { + rehash(); + } + } + + @SuppressWarnings("unchecked") + private void rehash() { + int size = size(); + int newCapacity = capacity * 2; + free = capacity = newCapacity; + int arrayCapacity = Numbers.ceilPow2((int) (newCapacity / loadFactor)); + T[] oldValues = values; + CharSequence[] oldKeys = keys; + this.keys = new CharSequence[arrayCapacity]; + this.values = (T[]) new Object[arrayCapacity]; + Arrays.fill(keys, null); + mask = arrayCapacity - 1; + + free -= size; + for (int i = oldKeys.length; i-- > 0; ) { + CharSequence key = oldKeys[i]; + if (key != null) { + final int index = keyIndex(key); + keys[index] = key; + values[index] = oldValues[i]; + } + } + } + + public void putAll(LowerCaseCharSequenceObjHashMap other) { + CharSequence[] otherKeys = other.keys; + T[] otherValues = other.values; + for (int i = 0, n = otherKeys.length; i < n; i++) { + if (otherKeys[i] != noEntryKey) { + put(otherKeys[i], otherValues[i]); + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index eeaa88279..06dfc7c97 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -455,7 +455,10 @@ open module io.questdb { io.questdb.griffin.engine.functions.conditional.CaseFunctionFactory, io.questdb.griffin.engine.functions.conditional.SwitchFunctionFactory, // PostgeSQL catalogue functions + io.questdb.griffin.engine.functions.catalogue.AttributeCatalogueFunctionFactory, io.questdb.griffin.engine.functions.catalogue.ClassCatalogueFunctionFactory, + io.questdb.griffin.engine.functions.catalogue.IndexCatalogueFunctionFactory, + io.questdb.griffin.engine.functions.catalogue.InformationSchemaFunctionFactory, io.questdb.griffin.engine.functions.catalogue.PrefixedNamespaceCatalogueFunctionFactory, io.questdb.griffin.engine.functions.catalogue.NamespaceCatalogueFunctionFactory, io.questdb.griffin.engine.functions.catalogue.IsTableVisibleCatalogueFunctionFactory, diff --git a/core/src/main/resources/META-INF/services/io.questdb.griffin.FunctionFactory b/core/src/main/resources/META-INF/services/io.questdb.griffin.FunctionFactory index 4d3699442..33b76d72a 100644 --- a/core/src/main/resources/META-INF/services/io.questdb.griffin.FunctionFactory +++ b/core/src/main/resources/META-INF/services/io.questdb.griffin.FunctionFactory @@ -22,6 +22,30 @@ # ################################################################################ +################################################################################ +# ___ _ ____ ____ +# / _ \ _ _ ___ ___| |_| _ \| __ ) +# | | | | | | |/ _ \/ __| __| | | | _ \ +# | |_| | |_| | __/\__ \ |_| |_| | |_) | +# \__\_\\__,_|\___||___/\__|____/|____/ +# +# Copyright (c) 2014-2019 Appsicle +# Copyright (c) 2019-2020 QuestDB +# +# Licensed 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. +# +################################################################################ + ################################################################################ # ___ _ ____ ____ @@ -463,8 +487,11 @@ io.questdb.griffin.engine.functions.math.RoundHalfEvenDoubleFunctionFactory io.questdb.griffin.engine.functions.conditional.CaseFunctionFactory io.questdb.griffin.engine.functions.conditional.SwitchFunctionFactory -# PostgeSQL catalogue functions +# PostgreSQL catalogue functions +io.questdb.griffin.engine.functions.catalogue.AttributeCatalogueFunctionFactory io.questdb.griffin.engine.functions.catalogue.ClassCatalogueFunctionFactory +io.questdb.griffin.engine.functions.catalogue.IndexCatalogueFunctionFactory +io.questdb.griffin.engine.functions.catalogue.InformationSchemaFunctionFactory io.questdb.griffin.engine.functions.catalogue.PrefixedNamespaceCatalogueFunctionFactory io.questdb.griffin.engine.functions.catalogue.IsTableVisibleCatalogueFunctionFactory io.questdb.griffin.engine.functions.catalogue.UserByIdCatalogueFunctionFactory diff --git a/core/src/test/java/io/questdb/cairo/GenericRecordMetadataTest.java b/core/src/test/java/io/questdb/cairo/GenericRecordMetadataTest.java index 5ae2cebc7..dbac2bc70 100644 --- a/core/src/test/java/io/questdb/cairo/GenericRecordMetadataTest.java +++ b/core/src/test/java/io/questdb/cairo/GenericRecordMetadataTest.java @@ -78,6 +78,31 @@ public class GenericRecordMetadataTest { TestUtils.assertEquals(expected, sink); } + @Test + public void testDuplicateColumn2() { + final String expected = "{\"columnCount\":2,\"columns\":[{\"index\":0,\"name\":\"abc\",\"type\":\"INT\"},{\"index\":1,\"name\":\"cde\",\"type\":\"INT\"}],\"timestampIndex\":-1}"; + + GenericRecordMetadata metadata = new GenericRecordMetadata(); + metadata.add(new TableColumnMetadata("abc", ColumnType.INT)); + metadata.add(new TableColumnMetadata("cde", ColumnType.INT)); + + sink.clear(); + metadata.toJson(sink); + TestUtils.assertEquals(expected, sink); + + try { + metadata.add(new TableColumnMetadata("ABC", ColumnType.FLOAT)); + Assert.fail(); + } catch (CairoException e) { + TestUtils.assertContains(e.getMessage(), "Duplicate column"); + } + + sink.clear(); + metadata.toJson(sink); + TestUtils.assertEquals(expected, sink); + } + + @Test public void testReuse() { String expected1 = "{\"columnCount\":3,\"columns\":[{\"index\":0,\"name\":\"abc\",\"type\":\"INT\"},{\"index\":1,\"name\":\"cde\",\"type\":\"INT\"},{\"index\":2,\"name\":\"timestamp\",\"type\":\"TIMESTAMP\"}],\"timestampIndex\":2}"; diff --git a/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactoryTest.java b/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactoryTest.java new file mode 100644 index 000000000..ea6a08d34 --- /dev/null +++ b/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/AttributeCatalogueFunctionFactoryTest.java @@ -0,0 +1,231 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.griffin.AbstractGriffinTest; +import org.junit.Test; + +public class AttributeCatalogueFunctionFactoryTest extends AbstractGriffinTest { + + @Test + public void testPgAttributeFunc() throws Exception { + assertQuery( + "attrelid\tattname\tattnum\n" + + "1\ta\t1\n", + "pg_catalog.pg_attribute;", + "create table x(a int)", + null, + false, + false + ); + } + + + @Test + public void testPgAttributeFuncNoTables() throws Exception { + assertQuery( + "attrelid\tattname\tattnum\n", + "pg_catalog.pg_attribute;", + null, + null, + false, + false + ); + } + + @Test + public void testPgAttributeFuncWith2Tables() throws Exception { + assertQuery( + "attrelid\tattname\tattnum\n" + + "1\ta\t1\n", + "pg_catalog.pg_attribute order by 1;", + "create table x(a int)", + null, + "create table y(a double, b string)", + "attrelid\tattname\tattnum\n" + + "1\ta\t1\n" + + "2\ta\t1\n" + + "2\tb\t2\n", + true, + false, + false + ); + } + + @Test + public void testPgAttributeFuncWith2TablesLimit1() throws Exception { + assertQuery( + "attrelid\tattname\tattnum\n" + + "1\ta\t1\n", + "pg_catalog.pg_attribute order by 1 limit 1;", + "create table x(a int)", + null, + "create table y(a double, b string)", + "attrelid\tattname\tattnum\n" + + "1\ta\t1\n", + true, + false, + true + ); + } + + @Test + public void testKafkaMetadataQuery() throws Exception { + + String query = "\n" + + "SELECT\n" + + " result.TABLE_CAT, \n" + + " result.TABLE_SCHEM, \n" + + " result.TABLE_NAME, \n" + + " result.COLUMN_NAME, \n" + + " result.KEY_SEQ, \n" + + " result.PK_NAME,\n" + + " result.KEYS,\n" + + " result.A_ATTNUM,\n" + + " RAW \n" + + "FROM\n" + + " (SELECT \n" + + " NULL AS TABLE_CAT, \n" + + " n.nspname AS TABLE_SCHEM, \n" + + " ct.relname AS TABLE_NAME, \n" + + " a.attname AS COLUMN_NAME, \n" + + " (information_schema._pg_expandarray(i.indkey)).n AS KEY_SEQ, \n" + + " ci.relname AS PK_NAME, \n" + + " information_schema._pg_expandarray(i.indkey) AS KEYS, \n" + + " a.attnum AS A_ATTNUM,\n" + + " i.indkey AS RAW \n" + + " FROM pg_catalog.pg_class ct\n" + + " JOIN pg_catalog.pg_attribute a ON (ct.oid = a.attrelid) \n" + + " JOIN pg_catalog.pg_namespace n ON (ct.relnamespace = n.oid) \n" + + " JOIN pg_catalog.pg_index i ON ( a.attrelid = i.indrelid) \n" + + " JOIN pg_catalog.pg_class ci ON (ci.oid = i.indexrelid) \n" + + " WHERE \n" + + " true \n" + + " AND ct.relname = E'po_items' \n" + + " AND i.indisprimary \n" + + " ) result; \n"; + + assertQuery( + "TABLE_CAT\tTABLE_SCHEM\tTABLE_NAME\tCOLUMN_NAME\tKEY_SEQ\tPK_NAME\tKEYS\tA_ATTNUM\tRAW\n", + query, + "create table x(a int)", + null, + false, + false + ); + } + + @Test + public void testKafkaMetadataQueryCaseInsensitivity1() throws Exception { + String query = "SELECT\n" + + " result.TABLE_CAT, \n" + + " result.TABLE_SCHEM, \n" + + " result.TABLE_NAME, \n" + + " result.COLUMN_NAME, \n" + + " result.KEY_SEQ, \n" + + " result.PK_NAME,\n" + + " -- result.KEYS,\n" + + " result.A_ATTNUM,\n" + + " RAW \n" + + "FROM\n" + + " (SELECT \n" + + " NULL AS TABLE_CAT, \n" + + " n.nspname AS TABLE_SCHEM, \n" + + " ct.relname AS TABLE_NAME, \n" + + " a.attname AS COLUMN_NAME, \n" + + " (information_schema._pg_expandarray(i.indkey)).n AS KEY_SEQ, \n" + + " ci.relname AS PK_NAME, \n" + + " -- information_schema._pg_expandarray(i.indkey) AS KEYS, \n" + + " a.attnum AS A_ATTNUM,\n" + + " i.indkey AS RAW \n" + + " FROM pg_catalog.pg_class ct\n" + + " JOIN pg_catalog.pg_attribute a ON (ct.oid = a.attrelid) \n" + + " JOIN pg_catalog.pg_namespace n ON (ct.relnamespace = n.oid) \n" + + " JOIN pg_catalog.pg_index i ON ( a.attrelid = i.indrelid) \n" + + " JOIN pg_catalog.pg_class ci ON (ci.oid = i.indexrelid) \n" + + " WHERE \n" + + " true \n" + + " AND ct.relname = E'po_items' \n" + + " AND i.indisprimary \n" + + " ) result \n" + + "--WHERE A_ATTNUM = (result.KEYS).x \n" + + "ORDER BY result.table_name, result.PK_NAME, result.KEY_SEQ;"; + + assertQuery( + "TABLE_CAT\tTABLE_SCHEM\tTABLE_NAME\tCOLUMN_NAME\tKEY_SEQ\tPK_NAME\tA_ATTNUM\tRAW\n", + query, + "create table x(a int)", + null, + true, + false + ); + } + + @Test + public void testKafkaMetadataQueryCaseInsensitivity2() throws Exception { + String query = "SELECT\n" + + " result.TABLE_CAT, \n" + + " result.TABLE_SCHEM, \n" + + " result.TABLE_NAME, \n" + + " result.COLUMN_NAME, \n" + + " result.KEY_SEQ, \n" + + " result.PK_NAME,\n" + + " -- result.KEYS,\n" + + " result.A_ATTNUM,\n" + + " RAW \n" + + "FROM\n" + + " (SELECT \n" + + " NULL AS TABLE_CAT, \n" + + " n.nspname AS TABLE_SCHEM, \n" + + " ct.relname AS TABLE_NAME, \n" + + " a.attname AS COLUMN_NAME, \n" + + " (information_schema._pg_expandarray(i.indkey)).n AS KEY_SEQ, \n" + + " ci.relname AS PK_NAME, \n" + + " -- information_schema._pg_expandarray(i.indkey) AS KEYS, \n" + + " a.attnum AS A_ATTNUM,\n" + + " i.indkey AS RAW \n" + + " FROM pg_catalog.pg_class ct\n" + + " JOIN pg_catalog.pg_attribute a ON (ct.oid = a.attrelid) \n" + + " JOIN pg_catalog.pg_namespace n ON (ct.relnamespace = n.oid) \n" + + " JOIN pg_catalog.pg_index i ON ( a.attrelid = i.indrelid) \n" + + " JOIN pg_catalog.pg_class ci ON (ci.oid = i.indexrelid) \n" + + " WHERE \n" + + " true \n" + + " AND ct.relname = E'po_items' \n" + + " AND i.indisprimary \n" + + " ) result \n" + + "--WHERE A_ATTNUM = (result.KEYS).x \n" + + "ORDER BY result.TABLE_NAME, result.pk_name, result.KEY_SEQ;"; + + assertQuery( + "TABLE_CAT\tTABLE_SCHEM\tTABLE_NAME\tCOLUMN_NAME\tKEY_SEQ\tPK_NAME\tA_ATTNUM\tRAW\n", + query, + "create table x(a int)", + null, + true, + false + ); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactoryTest.java b/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactoryTest.java new file mode 100644 index 000000000..c4c432510 --- /dev/null +++ b/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/IndexCatalogueFunctionFactoryTest.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.griffin.AbstractGriffinTest; +import org.junit.Test; + +public class IndexCatalogueFunctionFactoryTest extends AbstractGriffinTest { + + @Test + public void testPgIndexFunc() throws Exception { + assertQuery( + "indkey\tindrelid\tindexrelid\tindisprimary\n", + "pg_catalog.pg_index;", + "create table x(a int)", + null, + false, + false, + true + ); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactoryTest.java b/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactoryTest.java new file mode 100644 index 000000000..3b05eaafd --- /dev/null +++ b/core/src/test/java/io/questdb/griffin/engine/functions/catalogue/InformationSchemaFunctionFactoryTest.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.griffin.engine.functions.catalogue; + +import io.questdb.griffin.AbstractGriffinTest; +import org.junit.Test; + +public class InformationSchemaFunctionFactoryTest extends AbstractGriffinTest { + + @Test + public void testInformationSchemaPivotFunc() throws Exception { + assertQuery( + "x\tn\n", + "information_schema._pg_expandarray(5);", + "create table x(a int)", + null, + false, + false, + true + ); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/questdb/std/LowerCaseCharSequenceHashSetTest.java b/core/src/test/java/io/questdb/std/LowerCaseCharSequenceHashSetTest.java new file mode 100644 index 000000000..d3481b177 --- /dev/null +++ b/core/src/test/java/io/questdb/std/LowerCaseCharSequenceHashSetTest.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashSet; + +public class LowerCaseCharSequenceHashSetTest { + @Test + public void testSaturation() { + + final int N = 10_000; + final Rnd rnd = new Rnd(); + final LowerCaseCharSequenceHashSet lowerCaseSet = new LowerCaseCharSequenceHashSet(); + final HashSet referenceSet = new HashSet<>(); + for (int i = 0; i < N; i++) { + String str = rnd.nextString(4); + int keyIndex = lowerCaseSet.keyIndex(str, 0, str.length()); + if (lowerCaseSet.add(str)) { + Assert.assertTrue("at " + i, referenceSet.add(str)); + Assert.assertTrue(keyIndex > -1); + } else { + Assert.assertTrue(keyIndex < 0); + } + } + + // verify + for (String s : referenceSet) { + Assert.assertTrue(lowerCaseSet.contains(s)); + Assert.assertFalse(lowerCaseSet.excludes(s)); + Assert.assertFalse(lowerCaseSet.excludes(s, 0, s.length())); + + int keyIndex = lowerCaseSet.keyIndex(s); + Assert.assertTrue(keyIndex < 0); + Assert.assertTrue(Chars.equals(s, lowerCaseSet.keyAt(keyIndex))); + + keyIndex = lowerCaseSet.keyIndex(s, 0, s.length()); + Assert.assertTrue(Chars.equals(s, lowerCaseSet.keyAt(keyIndex))); + } + + for (int i = 0, n = lowerCaseSet.keys.length; i < n; i++) { + CharSequence v = lowerCaseSet.keys[i]; + if (v != null) { + Assert.assertTrue(referenceSet.contains(v.toString())); + } + } + + // remove every forth key + // make sure rnd generates the same keys again + rnd.reset(); + + for (int i = 0; i < N; i++) { + String s = rnd.nextString(4); + if (i % 4 == 0) { + lowerCaseSet.remove(s); + referenceSet.remove(s); + } + } + + // verify + for (String s : referenceSet) { + Assert.assertTrue(s, lowerCaseSet.contains(s)); + } + + for (int i = 0, n = lowerCaseSet.keys.length; i < n; i++) { + CharSequence v = lowerCaseSet.keys[i]; + if (v != null) { + Assert.assertTrue(referenceSet.contains(v.toString())); + } + } + + } +} \ No newline at end of file diff --git a/core/src/test/java/io/questdb/std/LowerCaseCharSequenceIntHashMapTest.java b/core/src/test/java/io/questdb/std/LowerCaseCharSequenceIntHashMapTest.java new file mode 100644 index 000000000..9080b5981 --- /dev/null +++ b/core/src/test/java/io/questdb/std/LowerCaseCharSequenceIntHashMapTest.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class LowerCaseCharSequenceIntHashMapTest { + + @Test + public void testSaturation() { + + final int N = 10_000; + final Rnd rnd = new Rnd(); + final LowerCaseCharSequenceIntHashMap lowerCaseMap = new LowerCaseCharSequenceIntHashMap(); + final HashMap referenceMap = new HashMap<>(); + for (int i = 0; i < N; i++) { + String str = rnd.nextString(4); + int value = rnd.nextInt(); + + int keyIndex = lowerCaseMap.keyIndex(str, 0, str.length()); + if (lowerCaseMap.put(str, value)) { + Assert.assertNull("at " + i, referenceMap.put(str, value)); + Assert.assertTrue(keyIndex > -1); + } else { + Assert.assertTrue(keyIndex < 0); + // this should fail to put + lowerCaseMap.putIfAbsent(str, value); + lowerCaseMap.putAt(keyIndex, str, referenceMap.get(str)); + } + } + + // verify + for (Map.Entry e : referenceMap.entrySet()) { + Assert.assertTrue(lowerCaseMap.contains(e.getKey())); + Assert.assertFalse(lowerCaseMap.excludes(e.getKey())); + Assert.assertFalse(lowerCaseMap.excludes(e.getKey(), 0, e.getKey().length())); + + int keyIndex = lowerCaseMap.keyIndex(e.getKey()); + Assert.assertTrue(keyIndex < 0); + Assert.assertEquals(e.getKey(), (int) e.getValue(), lowerCaseMap.valueAt(keyIndex)); + + keyIndex = lowerCaseMap.keyIndex(e.getKey(), 0, e.getKey().length()); + Assert.assertEquals((int) e.getValue(), lowerCaseMap.valueAt(keyIndex)); + } + + for (int i = 0, n = lowerCaseMap.keys.length; i < n; i++) { + CharSequence v = lowerCaseMap.keys[i]; + if (v != null) { + Assert.assertTrue(referenceMap.containsKey(v.toString())); + } + } + + // remove every forth key + // make sure rnd generates the same keys again + rnd.reset(); + + for (int i = 0; i < N; i++) { + String s = rnd.nextString(4); + if (i % 4 == 0) { + lowerCaseMap.remove(s); + referenceMap.remove(s); + } + } + + // verify + for (Map.Entry e : referenceMap.entrySet()) { + Assert.assertTrue(e.getKey(), lowerCaseMap.contains(e.getKey())); + } + + for (int i = 0, n = lowerCaseMap.keys.length; i < n; i++) { + CharSequence v = lowerCaseMap.keys[i]; + if (v != null) { + Assert.assertTrue(referenceMap.containsKey(v.toString())); + } + } + + } +} \ No newline at end of file diff --git a/core/src/test/java/io/questdb/std/LowerCaseCharSequenceObjHashMapTest.java b/core/src/test/java/io/questdb/std/LowerCaseCharSequenceObjHashMapTest.java new file mode 100644 index 000000000..607168445 --- /dev/null +++ b/core/src/test/java/io/questdb/std/LowerCaseCharSequenceObjHashMapTest.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * ___ _ ____ ____ + * / _ \ _ _ ___ ___| |_| _ \| __ ) + * | | | | | | |/ _ \/ __| __| | | | _ \ + * | |_| | |_| | __/\__ \ |_| |_| | |_) | + * \__\_\\__,_|\___||___/\__|____/|____/ + * + * Copyright (c) 2014-2019 Appsicle + * Copyright (c) 2019-2020 QuestDB + * + * Licensed 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 io.questdb.std; + + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class LowerCaseCharSequenceObjHashMapTest { + + @Test + public void testSaturation() { + + final int N = 10_000; + final Rnd rnd = new Rnd(); + final LowerCaseCharSequenceObjHashMap lowerCaseMap = new LowerCaseCharSequenceObjHashMap<>(); + final HashMap referenceMap = new HashMap<>(); + for (int i = 0; i < N; i++) { + String str = rnd.nextString(4); + int value = rnd.nextInt(); + + int keyIndex = lowerCaseMap.keyIndex(str, 0, str.length()); + if (lowerCaseMap.put(str, value)) { + Assert.assertNull("at " + i, referenceMap.put(str, value)); + Assert.assertTrue(keyIndex > -1); + } else { + Assert.assertTrue(keyIndex < 0); + // this should fail to put + lowerCaseMap.putIfAbsent(str, value); + lowerCaseMap.putAt(keyIndex, str, referenceMap.get(str)); + } + } + + // verify + for (Map.Entry e : referenceMap.entrySet()) { + Assert.assertTrue(lowerCaseMap.contains(e.getKey())); + Assert.assertFalse(lowerCaseMap.excludes(e.getKey())); + Assert.assertFalse(lowerCaseMap.excludes(e.getKey(), 0, e.getKey().length())); + + int keyIndex = lowerCaseMap.keyIndex(e.getKey()); + Assert.assertTrue(keyIndex < 0); + Assert.assertEquals(e.getKey(), e.getValue(), lowerCaseMap.valueAt(keyIndex)); + + keyIndex = lowerCaseMap.keyIndex(e.getKey(), 0, e.getKey().length()); + Assert.assertEquals(e.getValue(), lowerCaseMap.valueAt(keyIndex)); + } + + for (int i = 0, n = lowerCaseMap.keys.length; i < n; i++) { + CharSequence v = lowerCaseMap.keys[i]; + if (v != null) { + Assert.assertTrue(referenceMap.containsKey(v.toString())); + } + } + + // remove every forth key + // make sure rnd generates the same keys again + rnd.reset(); + + for (int i = 0; i < N; i++) { + String s = rnd.nextString(4); + if (i % 4 == 0) { + lowerCaseMap.remove(s); + referenceMap.remove(s); + } + } + + // verify + for (Map.Entry e : referenceMap.entrySet()) { + Assert.assertTrue(e.getKey(), lowerCaseMap.contains(e.getKey())); + } + + for (int i = 0, n = lowerCaseMap.keys.length; i < n; i++) { + CharSequence v = lowerCaseMap.keys[i]; + if (v != null) { + Assert.assertTrue(referenceMap.containsKey(v.toString())); + } + } + + } +} \ No newline at end of file -- GitLab