lbug 0.12.3-dev.13 → 0.12.3-dev.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lbug-source/.github/workflows/ci-workflow.yml +9 -2
- package/lbug-source/CMakeLists.txt +15 -6
- package/lbug-source/Makefile +1 -2
- package/lbug-source/benchmark/serializer.py +13 -2
- package/lbug-source/extension/httpfs/test/test_files/http.test +1 -0
- package/lbug-source/scripts/generate_binary_demo.sh +1 -1
- package/lbug-source/src/include/optimizer/count_rel_table_optimizer.h +49 -0
- package/lbug-source/src/include/optimizer/logical_operator_visitor.h +6 -0
- package/lbug-source/src/include/planner/operator/logical_operator.h +1 -0
- package/lbug-source/src/include/planner/operator/scan/logical_count_rel_table.h +84 -0
- package/lbug-source/src/include/processor/operator/physical_operator.h +1 -0
- package/lbug-source/src/include/processor/operator/scan/count_rel_table.h +62 -0
- package/lbug-source/src/include/processor/plan_mapper.h +2 -0
- package/lbug-source/src/optimizer/CMakeLists.txt +1 -0
- package/lbug-source/src/optimizer/count_rel_table_optimizer.cpp +217 -0
- package/lbug-source/src/optimizer/logical_operator_visitor.cpp +6 -0
- package/lbug-source/src/optimizer/optimizer.cpp +6 -0
- package/lbug-source/src/planner/operator/logical_operator.cpp +2 -0
- package/lbug-source/src/planner/operator/scan/CMakeLists.txt +1 -0
- package/lbug-source/src/planner/operator/scan/logical_count_rel_table.cpp +24 -0
- package/lbug-source/src/processor/map/CMakeLists.txt +1 -0
- package/lbug-source/src/processor/map/map_count_rel_table.cpp +55 -0
- package/lbug-source/src/processor/map/plan_mapper.cpp +3 -0
- package/lbug-source/src/processor/operator/physical_operator.cpp +2 -0
- package/lbug-source/src/processor/operator/scan/CMakeLists.txt +1 -0
- package/lbug-source/src/processor/operator/scan/count_rel_table.cpp +137 -0
- package/lbug-source/test/common/string_format.cpp +9 -1
- package/lbug-source/test/copy/copy_test.cpp +4 -4
- package/lbug-source/test/graph_test/CMakeLists.txt +1 -1
- package/lbug-source/test/optimizer/optimizer_test.cpp +46 -0
- package/lbug-source/test/test_helper/CMakeLists.txt +1 -1
- package/lbug-source/test/test_runner/CMakeLists.txt +1 -1
- package/lbug-source/test/test_runner/insert_by_row.cpp +6 -8
- package/lbug-source/test/test_runner/multi_copy_split.cpp +2 -4
- package/lbug-source/test/transaction/checkpoint_test.cpp +1 -1
- package/lbug-source/test/transaction/transaction_test.cpp +19 -15
- package/lbug-source/tools/benchmark/count_rel_table.benchmark +5 -0
- package/lbug-source/tools/shell/embedded_shell.cpp +11 -0
- package/lbug-source/tools/shell/linenoise.cpp +3 -3
- package/lbug-source/tools/shell/test/test_helper.py +1 -1
- package/lbug-source/tools/shell/test/test_shell_basics.py +12 -0
- package/package.json +1 -1
- package/prebuilt/lbugjs-darwin-arm64.node +0 -0
- package/prebuilt/lbugjs-linux-arm64.node +0 -0
- package/prebuilt/lbugjs-linux-x64.node +0 -0
- package/prebuilt/lbugjs-win32-x64.node +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#include "planner/operator/scan/logical_count_rel_table.h"
|
|
2
|
+
|
|
3
|
+
namespace lbug {
|
|
4
|
+
namespace planner {
|
|
5
|
+
|
|
6
|
+
void LogicalCountRelTable::computeFactorizedSchema() {
|
|
7
|
+
createEmptySchema();
|
|
8
|
+
// Only output the count expression in a single-state group.
|
|
9
|
+
// This operator is a source - it has no child in the logical plan.
|
|
10
|
+
// The bound node is used internally for scanning but not exposed.
|
|
11
|
+
auto groupPos = schema->createGroup();
|
|
12
|
+
schema->insertToGroupAndScope(countExpr, groupPos);
|
|
13
|
+
schema->setGroupAsSingleState(groupPos);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
void LogicalCountRelTable::computeFlatSchema() {
|
|
17
|
+
createEmptySchema();
|
|
18
|
+
// For flat schema, create a single group with the count expression.
|
|
19
|
+
auto groupPos = schema->createGroup();
|
|
20
|
+
schema->insertToGroupAndScope(countExpr, groupPos);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
} // namespace planner
|
|
24
|
+
} // namespace lbug
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#include "planner/operator/scan/logical_count_rel_table.h"
|
|
2
|
+
#include "processor/operator/scan/count_rel_table.h"
|
|
3
|
+
#include "processor/plan_mapper.h"
|
|
4
|
+
#include "storage/storage_manager.h"
|
|
5
|
+
|
|
6
|
+
using namespace lbug::common;
|
|
7
|
+
using namespace lbug::planner;
|
|
8
|
+
using namespace lbug::storage;
|
|
9
|
+
|
|
10
|
+
namespace lbug {
|
|
11
|
+
namespace processor {
|
|
12
|
+
|
|
13
|
+
std::unique_ptr<PhysicalOperator> PlanMapper::mapCountRelTable(
|
|
14
|
+
const LogicalOperator* logicalOperator) {
|
|
15
|
+
auto& logicalCountRelTable = logicalOperator->constCast<LogicalCountRelTable>();
|
|
16
|
+
auto outSchema = logicalCountRelTable.getSchema();
|
|
17
|
+
|
|
18
|
+
auto storageManager = StorageManager::Get(*clientContext);
|
|
19
|
+
|
|
20
|
+
// Get the node tables for scanning bound nodes
|
|
21
|
+
std::vector<NodeTable*> nodeTables;
|
|
22
|
+
for (auto tableID : logicalCountRelTable.getBoundNodeTableIDs()) {
|
|
23
|
+
nodeTables.push_back(storageManager->getTable(tableID)->ptrCast<NodeTable>());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get the rel tables
|
|
27
|
+
std::vector<RelTable*> relTables;
|
|
28
|
+
for (auto tableID : logicalCountRelTable.getRelTableIDs()) {
|
|
29
|
+
relTables.push_back(storageManager->getTable(tableID)->ptrCast<RelTable>());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Determine rel data direction from extend direction
|
|
33
|
+
auto extendDirection = logicalCountRelTable.getDirection();
|
|
34
|
+
RelDataDirection relDirection;
|
|
35
|
+
if (extendDirection == ExtendDirection::FWD) {
|
|
36
|
+
relDirection = RelDataDirection::FWD;
|
|
37
|
+
} else if (extendDirection == ExtendDirection::BWD) {
|
|
38
|
+
relDirection = RelDataDirection::BWD;
|
|
39
|
+
} else {
|
|
40
|
+
// For BOTH, we'll scan FWD (shouldn't reach here as optimizer filters BOTH)
|
|
41
|
+
relDirection = RelDataDirection::FWD;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get the output position for the count expression
|
|
45
|
+
auto countOutputPos = getDataPos(*logicalCountRelTable.getCountExpr(), *outSchema);
|
|
46
|
+
|
|
47
|
+
auto printInfo = std::make_unique<CountRelTablePrintInfo>(
|
|
48
|
+
logicalCountRelTable.getRelGroupEntry()->getName());
|
|
49
|
+
|
|
50
|
+
return std::make_unique<CountRelTable>(std::move(nodeTables), std::move(relTables),
|
|
51
|
+
relDirection, countOutputPos, getOperatorID(), std::move(printInfo));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
} // namespace processor
|
|
55
|
+
} // namespace lbug
|
|
@@ -62,6 +62,9 @@ std::unique_ptr<PhysicalOperator> PlanMapper::mapOperator(const LogicalOperator*
|
|
|
62
62
|
case LogicalOperatorType::COPY_TO: {
|
|
63
63
|
physicalOperator = mapCopyTo(logicalOperator);
|
|
64
64
|
} break;
|
|
65
|
+
case LogicalOperatorType::COUNT_REL_TABLE: {
|
|
66
|
+
physicalOperator = mapCountRelTable(logicalOperator);
|
|
67
|
+
} break;
|
|
65
68
|
case LogicalOperatorType::CREATE_MACRO: {
|
|
66
69
|
physicalOperator = mapCreateMacro(logicalOperator);
|
|
67
70
|
} break;
|
|
@@ -27,6 +27,8 @@ std::string PhysicalOperatorUtils::operatorTypeToString(PhysicalOperatorType ope
|
|
|
27
27
|
return "BATCH_INSERT";
|
|
28
28
|
case PhysicalOperatorType::COPY_TO:
|
|
29
29
|
return "COPY_TO";
|
|
30
|
+
case PhysicalOperatorType::COUNT_REL_TABLE:
|
|
31
|
+
return "COUNT_REL_TABLE";
|
|
30
32
|
case PhysicalOperatorType::CREATE_MACRO:
|
|
31
33
|
return "CREATE_MACRO";
|
|
32
34
|
case PhysicalOperatorType::CREATE_SEQUENCE:
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#include "processor/operator/scan/count_rel_table.h"
|
|
2
|
+
|
|
3
|
+
#include "common/system_config.h"
|
|
4
|
+
#include "main/client_context.h"
|
|
5
|
+
#include "main/database.h"
|
|
6
|
+
#include "processor/execution_context.h"
|
|
7
|
+
#include "storage/buffer_manager/memory_manager.h"
|
|
8
|
+
#include "storage/local_storage/local_rel_table.h"
|
|
9
|
+
#include "storage/local_storage/local_storage.h"
|
|
10
|
+
#include "storage/table/column.h"
|
|
11
|
+
#include "storage/table/column_chunk_data.h"
|
|
12
|
+
#include "storage/table/csr_chunked_node_group.h"
|
|
13
|
+
#include "storage/table/csr_node_group.h"
|
|
14
|
+
#include "storage/table/rel_table_data.h"
|
|
15
|
+
#include "transaction/transaction.h"
|
|
16
|
+
|
|
17
|
+
using namespace lbug::common;
|
|
18
|
+
using namespace lbug::storage;
|
|
19
|
+
using namespace lbug::transaction;
|
|
20
|
+
|
|
21
|
+
namespace lbug {
|
|
22
|
+
namespace processor {
|
|
23
|
+
|
|
24
|
+
void CountRelTable::initLocalStateInternal(ResultSet* resultSet, ExecutionContext* /*context*/) {
|
|
25
|
+
countVector = resultSet->getValueVector(countOutputPos).get();
|
|
26
|
+
hasExecuted = false;
|
|
27
|
+
totalCount = 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Count rels by using CSR metadata, accounting for deletions and uncommitted data.
|
|
31
|
+
// This is more efficient than scanning through all edges.
|
|
32
|
+
bool CountRelTable::getNextTuplesInternal(ExecutionContext* context) {
|
|
33
|
+
if (hasExecuted) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
auto transaction = Transaction::Get(*context->clientContext);
|
|
38
|
+
auto* memoryManager = context->clientContext->getDatabase()->getMemoryManager();
|
|
39
|
+
|
|
40
|
+
for (auto* relTable : relTables) {
|
|
41
|
+
// Get the RelTableData for the specified direction
|
|
42
|
+
auto* relTableData = relTable->getDirectedTableData(direction);
|
|
43
|
+
auto numNodeGroups = relTableData->getNumNodeGroups();
|
|
44
|
+
auto* csrLengthColumn = relTableData->getCSRLengthColumn();
|
|
45
|
+
|
|
46
|
+
// For each node group in the rel table
|
|
47
|
+
for (node_group_idx_t nodeGroupIdx = 0; nodeGroupIdx < numNodeGroups; nodeGroupIdx++) {
|
|
48
|
+
auto* nodeGroup = relTableData->getNodeGroup(nodeGroupIdx);
|
|
49
|
+
if (!nodeGroup) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
auto& csrNodeGroup = nodeGroup->cast<CSRNodeGroup>();
|
|
54
|
+
|
|
55
|
+
// Count from persistent (checkpointed) data
|
|
56
|
+
if (auto* persistentGroup = csrNodeGroup.getPersistentChunkedGroup()) {
|
|
57
|
+
// Sum the actual relationship lengths from the CSR header instead of using
|
|
58
|
+
// getNumRows() which includes dummy rows added for CSR offset array gaps
|
|
59
|
+
auto& csrPersistentGroup = persistentGroup->cast<ChunkedCSRNodeGroup>();
|
|
60
|
+
auto& csrHeader = csrPersistentGroup.getCSRHeader();
|
|
61
|
+
|
|
62
|
+
// Get the number of nodes in this CSR header
|
|
63
|
+
auto numNodes = csrHeader.length->getNumValues();
|
|
64
|
+
if (numNodes == 0) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create an in-memory chunk to scan the CSR length column into
|
|
69
|
+
auto lengthChunk =
|
|
70
|
+
ColumnChunkFactory::createColumnChunkData(*memoryManager, LogicalType::UINT64(),
|
|
71
|
+
false /*enableCompression*/, StorageConfig::NODE_GROUP_SIZE,
|
|
72
|
+
ResidencyState::IN_MEMORY, false /*initializeToZero*/);
|
|
73
|
+
|
|
74
|
+
// Initialize scan state and scan the length column from disk
|
|
75
|
+
ChunkState chunkState;
|
|
76
|
+
csrHeader.length->initializeScanState(chunkState, csrLengthColumn);
|
|
77
|
+
csrLengthColumn->scan(chunkState, lengthChunk.get(), 0 /*offsetInChunk*/, numNodes);
|
|
78
|
+
|
|
79
|
+
// Sum all the lengths
|
|
80
|
+
auto* lengthData = reinterpret_cast<const uint64_t*>(lengthChunk->getData());
|
|
81
|
+
row_idx_t groupRelCount = 0;
|
|
82
|
+
for (offset_t i = 0; i < numNodes; ++i) {
|
|
83
|
+
groupRelCount += lengthData[i];
|
|
84
|
+
}
|
|
85
|
+
totalCount += groupRelCount;
|
|
86
|
+
|
|
87
|
+
// Subtract deletions from persistent data
|
|
88
|
+
if (persistentGroup->hasVersionInfo()) {
|
|
89
|
+
auto numDeletions =
|
|
90
|
+
persistentGroup->getNumDeletions(transaction, 0, groupRelCount);
|
|
91
|
+
totalCount -= numDeletions;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Count in-memory committed data (not yet checkpointed)
|
|
96
|
+
// This data is stored in chunkedGroups within the NodeGroup
|
|
97
|
+
auto numChunkedGroups = csrNodeGroup.getNumChunkedGroups();
|
|
98
|
+
for (node_group_idx_t i = 0; i < numChunkedGroups; i++) {
|
|
99
|
+
auto* chunkedGroup = csrNodeGroup.getChunkedNodeGroup(i);
|
|
100
|
+
if (chunkedGroup) {
|
|
101
|
+
auto numRows = chunkedGroup->getNumRows();
|
|
102
|
+
totalCount += numRows;
|
|
103
|
+
// Subtract deletions from in-memory committed data
|
|
104
|
+
if (chunkedGroup->hasVersionInfo()) {
|
|
105
|
+
auto numDeletions = chunkedGroup->getNumDeletions(transaction, 0, numRows);
|
|
106
|
+
totalCount -= numDeletions;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add uncommitted insertions from local storage
|
|
113
|
+
if (transaction->isWriteTransaction()) {
|
|
114
|
+
if (auto* localTable =
|
|
115
|
+
transaction->getLocalStorage()->getLocalTable(relTable->getTableID())) {
|
|
116
|
+
auto& localRelTable = localTable->cast<LocalRelTable>();
|
|
117
|
+
// Count entries in the CSR index for this direction.
|
|
118
|
+
// We can't use getNumTotalRows() because it includes deleted rows.
|
|
119
|
+
auto& csrIndex = localRelTable.getCSRIndex(direction);
|
|
120
|
+
for (const auto& [nodeOffset, rowIndices] : csrIndex) {
|
|
121
|
+
totalCount += rowIndices.size();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
hasExecuted = true;
|
|
128
|
+
|
|
129
|
+
// Write the count to the output vector (single value)
|
|
130
|
+
countVector->state->getSelVectorUnsafe().setToUnfiltered(1);
|
|
131
|
+
countVector->setValue<int64_t>(0, static_cast<int64_t>(totalCount));
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
} // namespace processor
|
|
137
|
+
} // namespace lbug
|
|
@@ -10,6 +10,7 @@ TEST(StringFormat, Basic) {
|
|
|
10
10
|
"Some formatted data: a and 423");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
#if !USE_STD_FORMAT
|
|
13
14
|
TEST(StringFormat, Escape) {
|
|
14
15
|
ASSERT_EQ(stringFormat("Escape this {{}} but not this {{ }}"),
|
|
15
16
|
"Escape this {} but not this {{ }}");
|
|
@@ -29,6 +30,7 @@ TEST(StringFormat, TooManyArguments) {
|
|
|
29
30
|
TEST(StringFormat, TooFewArguments) {
|
|
30
31
|
ASSERT_THROW(stringFormat("Format with arguments {}"), InternalException);
|
|
31
32
|
}
|
|
33
|
+
#endif
|
|
32
34
|
|
|
33
35
|
TEST(StringFormat, Format8BitTypes) {
|
|
34
36
|
enum TestEnum : uint8_t {
|
|
@@ -38,7 +40,9 @@ TEST(StringFormat, Format8BitTypes) {
|
|
|
38
40
|
char literal_character = 'a';
|
|
39
41
|
TestEnum enum_val = TestEnum::NO;
|
|
40
42
|
int8_t signed_int8 = 4;
|
|
41
|
-
ASSERT_EQ(
|
|
43
|
+
ASSERT_EQ(
|
|
44
|
+
stringFormat("{} {} {}", literal_character, static_cast<uint8_t>(enum_val), signed_int8),
|
|
45
|
+
"a 1 4");
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
TEST(StringFormat, FormatString) {
|
|
@@ -64,5 +68,9 @@ TEST(StringFormat, FormatIntegers) {
|
|
|
64
68
|
TEST(StringFormat, FormatFloats) {
|
|
65
69
|
float a = 2.3;
|
|
66
70
|
double b = 5.4;
|
|
71
|
+
#if USE_STD_FORMAT
|
|
72
|
+
ASSERT_EQ(stringFormat("{} {}", a, b), "2.3 5.4");
|
|
73
|
+
#else
|
|
67
74
|
ASSERT_EQ(stringFormat("{} {}", a, b), "2.300000 5.400000");
|
|
75
|
+
#endif
|
|
68
76
|
}
|
|
@@ -280,7 +280,7 @@ TEST_F(CopyTest, NodeInsertBMExceptionDuringCommitRecovery) {
|
|
|
280
280
|
.executeFunc =
|
|
281
281
|
[](main::Connection* conn, int) {
|
|
282
282
|
const auto queryString = common::stringFormat(
|
|
283
|
-
"UNWIND RANGE(1,{}) AS i CREATE (a:account {ID:i})", numValues);
|
|
283
|
+
"UNWIND RANGE(1,{}) AS i CREATE (a:account {{ID:i}})", numValues);
|
|
284
284
|
return conn->query(queryString);
|
|
285
285
|
},
|
|
286
286
|
.earlyExitOnFailureFunc = [](main::QueryResult*) { return false; },
|
|
@@ -300,7 +300,7 @@ TEST_F(CopyTest, RelInsertBMExceptionDuringCommitRecovery) {
|
|
|
300
300
|
conn->query("CREATE NODE TABLE account(ID INT64, PRIMARY KEY(ID))");
|
|
301
301
|
conn->query("CREATE REL TABLE follows(FROM account TO account);");
|
|
302
302
|
const auto queryString = common::stringFormat(
|
|
303
|
-
"UNWIND RANGE(1,{}) AS i CREATE (a:account {ID:i})", numNodes);
|
|
303
|
+
"UNWIND RANGE(1,{}) AS i CREATE (a:account {{ID:i}})", numNodes);
|
|
304
304
|
ASSERT_TRUE(conn->query(queryString)->isSuccess());
|
|
305
305
|
failureFrequency = 32;
|
|
306
306
|
},
|
|
@@ -309,7 +309,7 @@ TEST_F(CopyTest, RelInsertBMExceptionDuringCommitRecovery) {
|
|
|
309
309
|
return conn->query(common::stringFormat(
|
|
310
310
|
"UNWIND RANGE(1,{}) AS i MATCH (a:account), (b:account) WHERE a.ID = i AND "
|
|
311
311
|
"b.ID = i + 1 CREATE (a)-[f:follows]->(b)",
|
|
312
|
-
numNodes));
|
|
312
|
+
numNodes - 1));
|
|
313
313
|
},
|
|
314
314
|
.earlyExitOnFailureFunc = [](main::QueryResult*) { return false; },
|
|
315
315
|
.checkFunc =
|
|
@@ -407,7 +407,7 @@ TEST_F(CopyTest, NodeInsertBMExceptionDuringCheckpointRecovery) {
|
|
|
407
407
|
.executeFunc =
|
|
408
408
|
[](main::Connection* conn, int) {
|
|
409
409
|
return conn->query(common::stringFormat(
|
|
410
|
-
"UNWIND RANGE(1,{}) AS i CREATE (a:account {ID:i})", numValues));
|
|
410
|
+
"UNWIND RANGE(1,{}) AS i CREATE (a:account {{ID:i}})", numValues));
|
|
411
411
|
},
|
|
412
412
|
.earlyExitOnFailureFunc = [](main::QueryResult*) { return true; },
|
|
413
413
|
.checkFunc =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "graph_test/private_graph_test.h"
|
|
2
2
|
#include "planner/operator/logical_plan_util.h"
|
|
3
|
+
#include "planner/operator/scan/logical_count_rel_table.h"
|
|
3
4
|
#include "test_runner/test_runner.h"
|
|
4
5
|
|
|
5
6
|
namespace lbug {
|
|
@@ -17,6 +18,19 @@ public:
|
|
|
17
18
|
std::unique_ptr<planner::LogicalPlan> getRoot(const std::string& query) {
|
|
18
19
|
return TestRunner::getLogicalPlan(query, *conn);
|
|
19
20
|
}
|
|
21
|
+
|
|
22
|
+
// Helper to check if a specific operator type exists in the plan
|
|
23
|
+
static bool hasOperatorType(planner::LogicalOperator* op, planner::LogicalOperatorType type) {
|
|
24
|
+
if (op->getOperatorType() == type) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
for (auto i = 0u; i < op->getNumChildren(); ++i) {
|
|
28
|
+
if (hasOperatorType(op->getChild(i).get(), type)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
20
34
|
};
|
|
21
35
|
|
|
22
36
|
TEST_F(OptimizerTest, JoinHint) {
|
|
@@ -211,5 +225,37 @@ TEST_F(OptimizerTest, SubqueryHint) {
|
|
|
211
225
|
ASSERT_STREQ(getEncodedPlan(q6).c_str(), "Filter()HJ(a._ID){S(a)}{E(a)Filter()S(b)}");
|
|
212
226
|
}
|
|
213
227
|
|
|
228
|
+
TEST_F(OptimizerTest, CountRelTableOptimizer) {
|
|
229
|
+
// Test that COUNT(*) over a single rel table is optimized to COUNT_REL_TABLE
|
|
230
|
+
auto q1 = "MATCH (a:person)-[e:knows]->(b:person) RETURN COUNT(*);";
|
|
231
|
+
auto plan1 = getRoot(q1);
|
|
232
|
+
ASSERT_TRUE(hasOperatorType(plan1->getLastOperator().get(),
|
|
233
|
+
planner::LogicalOperatorType::COUNT_REL_TABLE));
|
|
234
|
+
// Verify the query returns the correct result
|
|
235
|
+
auto result1 = conn->query(q1);
|
|
236
|
+
ASSERT_TRUE(result1->isSuccess());
|
|
237
|
+
ASSERT_EQ(result1->getNumTuples(), 1);
|
|
238
|
+
auto tuple1 = result1->getNext();
|
|
239
|
+
ASSERT_EQ(tuple1->getValue(0)->getValue<int64_t>(), 14);
|
|
240
|
+
|
|
241
|
+
// Test that COUNT(*) with GROUP BY is NOT optimized (has keys)
|
|
242
|
+
auto q2 = "MATCH (a:person)-[e:knows]->(b:person) RETURN a.fName, COUNT(*);";
|
|
243
|
+
auto plan2 = getRoot(q2);
|
|
244
|
+
ASSERT_FALSE(hasOperatorType(plan2->getLastOperator().get(),
|
|
245
|
+
planner::LogicalOperatorType::COUNT_REL_TABLE));
|
|
246
|
+
|
|
247
|
+
// Test that COUNT(*) with WHERE clause is NOT optimized (has filter)
|
|
248
|
+
auto q3 = "MATCH (a:person)-[e:knows]->(b:person) WHERE a.ID > 0 RETURN COUNT(*);";
|
|
249
|
+
auto plan3 = getRoot(q3);
|
|
250
|
+
ASSERT_FALSE(hasOperatorType(plan3->getLastOperator().get(),
|
|
251
|
+
planner::LogicalOperatorType::COUNT_REL_TABLE));
|
|
252
|
+
|
|
253
|
+
// Test that COUNT(DISTINCT ...) is NOT optimized
|
|
254
|
+
auto q4 = "MATCH (a:person)-[e:knows]->(b:person) RETURN COUNT(DISTINCT a);";
|
|
255
|
+
auto plan4 = getRoot(q4);
|
|
256
|
+
ASSERT_FALSE(hasOperatorType(plan4->getLastOperator().get(),
|
|
257
|
+
planner::LogicalOperatorType::COUNT_REL_TABLE));
|
|
258
|
+
}
|
|
259
|
+
|
|
214
260
|
} // namespace testing
|
|
215
261
|
} // namespace lbug
|
|
@@ -111,23 +111,21 @@ std::string InsertDatasetByRow::TableInfo::getBodyForLoad() const {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
std::string InsertDatasetByRow::NodeTableInfo::getLoadFromQuery() const {
|
|
114
|
-
const std::string query = "LOAD WITH HEADERS ({}) FROM {} "
|
|
115
|
-
"CREATE (:{} {});";
|
|
116
114
|
const auto header = getHeaderForLoad();
|
|
117
115
|
const auto body = getBodyForLoad();
|
|
118
|
-
return stringFormat(
|
|
116
|
+
return stringFormat("LOAD WITH HEADERS ({}) FROM {} CREATE (:{} {{{}}});", header, filePath,
|
|
117
|
+
name, body);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
std::string InsertDatasetByRow::RelTableInfo::getLoadFromQuery() const {
|
|
122
|
-
const std::string query = "LOAD WITH HEADERS ({}) FROM {} "
|
|
123
|
-
"MATCH (a:{}), (b:{}) WHERE a.{} = aid_ AND b.{} = bid_ "
|
|
124
|
-
"CREATE (a)-[:{} {}]->(b);";
|
|
125
121
|
auto header = stringFormat("aid_ {},bid_ {}", from.type, to.type);
|
|
126
122
|
auto headerRest = getHeaderForLoad();
|
|
127
123
|
header += headerRest.length() == 0 ? "" : "," + getHeaderForLoad();
|
|
128
124
|
const auto body = getBodyForLoad();
|
|
129
|
-
return stringFormat(
|
|
130
|
-
|
|
125
|
+
return stringFormat("LOAD WITH HEADERS ({}) FROM {} "
|
|
126
|
+
"MATCH (a:{}), (b:{}) WHERE a.{} = aid_ AND b.{} = bid_ "
|
|
127
|
+
"CREATE (a)-[:{} {{{}}}]->(b);",
|
|
128
|
+
header, filePath, from.name, to.name, from.property, to.property, name, body);
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
} // namespace testing
|
|
@@ -62,8 +62,7 @@ void SplitMultiCopyRandom::init() {
|
|
|
62
62
|
|
|
63
63
|
const std::string tmpDir = TestHelper::getTempDir("multi_copy");
|
|
64
64
|
auto totalFilePath = TestHelper::joinPath(tmpDir, tableName + ".csv");
|
|
65
|
-
|
|
66
|
-
loadQuery = stringFormat(loadQuery, source, totalFilePath);
|
|
65
|
+
auto loadQuery = stringFormat("COPY (LOAD FROM {} RETURN *) TO '{}';", source, totalFilePath);
|
|
67
66
|
spdlog::info("QUERY: {}", loadQuery);
|
|
68
67
|
validateQuery(connection, loadQuery);
|
|
69
68
|
|
|
@@ -96,9 +95,8 @@ void SplitMultiCopyRandom::init() {
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
void SplitMultiCopyRandom::run() {
|
|
99
|
-
const std::string query = "COPY {} FROM \"{}\";";
|
|
100
98
|
for (auto file : splitFilePaths) {
|
|
101
|
-
auto currQuery = stringFormat(
|
|
99
|
+
auto currQuery = stringFormat("COPY {} FROM \"{}\";", tableName, file);
|
|
102
100
|
spdlog::info("QUERY: {}", currQuery);
|
|
103
101
|
validateQuery(connection, currQuery);
|
|
104
102
|
}
|
|
@@ -37,7 +37,7 @@ public:
|
|
|
37
37
|
conn->query("CALL auto_checkpoint=false");
|
|
38
38
|
conn->query("CREATE NODE TABLE test(id INT64 PRIMARY KEY, name STRING);");
|
|
39
39
|
for (auto i = 0; i < 5000; i++) {
|
|
40
|
-
conn->query(stringFormat("CREATE (a:test {id: {}, name: 'name_{}'});", i, i));
|
|
40
|
+
conn->query(stringFormat("CREATE (a:test {{id: {}, name: 'name_{}'}});", i, i));
|
|
41
41
|
}
|
|
42
42
|
auto context = getClientContext(*conn);
|
|
43
43
|
flakyCheckpointer.setCheckpointer(*context);
|
|
@@ -138,7 +138,8 @@ static void insertNodes(uint64_t startID, uint64_t num, lbug::main::Database& da
|
|
|
138
138
|
auto conn = std::make_unique<lbug::main::Connection>(&database);
|
|
139
139
|
for (uint64_t i = 0; i < num; ++i) {
|
|
140
140
|
auto id = startID + i;
|
|
141
|
-
auto res =
|
|
141
|
+
auto res =
|
|
142
|
+
conn->query(stringFormat("CREATE (:test {{id: {}, name: 'Person{}'}});", id, id));
|
|
142
143
|
ASSERT_TRUE(res->isSuccess())
|
|
143
144
|
<< "Failed to insert test" << id << ": " << res->getErrorMessage();
|
|
144
145
|
}
|
|
@@ -181,7 +182,7 @@ static void insertNodesWithMixedTypes(uint64_t startID, uint64_t num,
|
|
|
181
182
|
auto score = 95.5 + (id % 10);
|
|
182
183
|
auto isActive = (id % 2 == 0) ? "true" : "false";
|
|
183
184
|
auto res = conn->query(
|
|
184
|
-
stringFormat("CREATE (:mixed_test {id: {}, score: {}, active: {}, name: 'User{}'});",
|
|
185
|
+
stringFormat("CREATE (:mixed_test {{id: {}, score: {}, active: {}, name: 'User{}'}});",
|
|
185
186
|
id, score, isActive, id));
|
|
186
187
|
ASSERT_TRUE(res->isSuccess())
|
|
187
188
|
<< "Failed to insert mixed_test" << id << ": " << res->getErrorMessage();
|
|
@@ -227,7 +228,7 @@ static void insertRelationships(uint64_t startID, uint64_t num, lbug::main::Data
|
|
|
227
228
|
auto toID = (startID + i + 1) % (num * 4);
|
|
228
229
|
auto weight = 1.0 + (i % 10) * 0.1;
|
|
229
230
|
auto res = conn->query(stringFormat("MATCH (a:person), (b:person) WHERE a.id = {} AND b.id "
|
|
230
|
-
"= {} CREATE (a)-[:knows {weight: {}}]->(b);",
|
|
231
|
+
"= {} CREATE (a)-[:knows {{weight: {}}}]->(b);",
|
|
231
232
|
fromID, toID, weight));
|
|
232
233
|
ASSERT_TRUE(res->isSuccess()) << "Failed to insert relationship from " << fromID << " to "
|
|
233
234
|
<< toID << ": " << res->getErrorMessage();
|
|
@@ -248,7 +249,8 @@ TEST_F(EmptyDBTransactionTest, ConcurrentRelationshipInsertions) {
|
|
|
248
249
|
|
|
249
250
|
conn->query("BEGIN TRANSACTION;");
|
|
250
251
|
for (auto i = 0; i < numTotalInsertions; ++i) {
|
|
251
|
-
auto res =
|
|
252
|
+
auto res =
|
|
253
|
+
conn->query(stringFormat("CREATE (:person {{id: {}, name: 'Person{}'}});", i, i));
|
|
252
254
|
ASSERT_TRUE(res->isSuccess());
|
|
253
255
|
}
|
|
254
256
|
conn->query("COMMIT;");
|
|
@@ -286,7 +288,7 @@ static void insertComplexRelationships(uint64_t startID, uint64_t num,
|
|
|
286
288
|
auto isVerified = (i % 3 == 0) ? "true" : "false";
|
|
287
289
|
auto res =
|
|
288
290
|
conn->query(stringFormat("MATCH (u:user), (p:product) WHERE u.id = {} AND p.id = {} "
|
|
289
|
-
"CREATE (u)-[:rates {rating: {}, verified: {}}]->(p);",
|
|
291
|
+
"CREATE (u)-[:rates {{rating: {}, verified: {}}}]->(p);",
|
|
290
292
|
userID, productID, rating, isVerified));
|
|
291
293
|
ASSERT_TRUE(res->isSuccess())
|
|
292
294
|
<< "Failed to insert rating from user " << userID << " to product " << productID << ": "
|
|
@@ -309,12 +311,12 @@ TEST_F(EmptyDBTransactionTest, ConcurrentComplexRelationshipInsertions) {
|
|
|
309
311
|
|
|
310
312
|
conn->query("BEGIN TRANSACTION;");
|
|
311
313
|
for (auto i = 0; i < numTotalInsertions; ++i) {
|
|
312
|
-
auto res = conn->query(stringFormat("CREATE (:user {id: {}, name: 'User{}'});", i, i));
|
|
314
|
+
auto res = conn->query(stringFormat("CREATE (:user {{id: {}, name: 'User{}'}});", i, i));
|
|
313
315
|
ASSERT_TRUE(res->isSuccess());
|
|
314
316
|
}
|
|
315
317
|
for (auto i = 0; i < numTotalInsertions * 2; ++i) {
|
|
316
318
|
auto res =
|
|
317
|
-
conn->query(stringFormat("CREATE (:product {id: {}, title: 'Product{}'});", i, i));
|
|
319
|
+
conn->query(stringFormat("CREATE (:product {{id: {}, title: 'Product{}'}});", i, i));
|
|
318
320
|
ASSERT_TRUE(res->isSuccess());
|
|
319
321
|
}
|
|
320
322
|
conn->query("COMMIT;");
|
|
@@ -367,7 +369,7 @@ TEST_F(EmptyDBTransactionTest, ConcurrentNodeUpdates) {
|
|
|
367
369
|
|
|
368
370
|
// First insert all nodes
|
|
369
371
|
for (auto i = 0; i < numTotalNodes; ++i) {
|
|
370
|
-
auto res = conn->query(stringFormat("CREATE (:test {id: {}, name: 'Person{}'});", i, i));
|
|
372
|
+
auto res = conn->query(stringFormat("CREATE (:test {{id: {}, name: 'Person{}'}});", i, i));
|
|
371
373
|
ASSERT_TRUE(res->isSuccess());
|
|
372
374
|
}
|
|
373
375
|
|
|
@@ -429,8 +431,8 @@ TEST_F(EmptyDBTransactionTest, ConcurrentMixedTypeUpdates) {
|
|
|
429
431
|
auto score = 95.5 + (i % 10);
|
|
430
432
|
auto isActive = (i % 2 == 0) ? "true" : "false";
|
|
431
433
|
auto res = conn->query(
|
|
432
|
-
stringFormat("CREATE (:mixed_test {id: {}, score: {}, active: {}, name: 'User{}'});",
|
|
433
|
-
score, isActive, i));
|
|
434
|
+
stringFormat("CREATE (:mixed_test {{id: {}, score: {}, active: {}, name: 'User{}'}});",
|
|
435
|
+
i, score, isActive, i));
|
|
434
436
|
ASSERT_TRUE(res->isSuccess());
|
|
435
437
|
}
|
|
436
438
|
|
|
@@ -503,7 +505,8 @@ TEST_F(EmptyDBTransactionTest, ConcurrentRelationshipUpdates) {
|
|
|
503
505
|
|
|
504
506
|
// Create nodes
|
|
505
507
|
for (auto i = 0; i < numTotalUpdates; ++i) {
|
|
506
|
-
auto res =
|
|
508
|
+
auto res =
|
|
509
|
+
conn->query(stringFormat("CREATE (:person {{id: {}, name: 'Person{}'}});", i, i));
|
|
507
510
|
ASSERT_TRUE(res->isSuccess());
|
|
508
511
|
}
|
|
509
512
|
|
|
@@ -513,7 +516,7 @@ TEST_F(EmptyDBTransactionTest, ConcurrentRelationshipUpdates) {
|
|
|
513
516
|
auto toID = (i + 1) % numTotalUpdates;
|
|
514
517
|
auto weight = 1.0 + (i % 10) * 0.1;
|
|
515
518
|
auto res = conn->query(stringFormat("MATCH (a:person), (b:person) WHERE a.id = {} AND b.id "
|
|
516
|
-
"= {} CREATE (a)-[:knows {weight: {}}]->(b);",
|
|
519
|
+
"= {} CREATE (a)-[:knows {{weight: {}}}]->(b);",
|
|
517
520
|
fromID, toID, weight));
|
|
518
521
|
ASSERT_TRUE(res->isSuccess());
|
|
519
522
|
}
|
|
@@ -577,7 +580,7 @@ TEST_F(EmptyDBTransactionTest, ConcurrentNodeUpdatesWithMixedTransactions) {
|
|
|
577
580
|
|
|
578
581
|
// Insert initial nodes
|
|
579
582
|
for (auto i = 0; i < numTotalNodes; ++i) {
|
|
580
|
-
auto res = conn->query(stringFormat("CREATE (:test {id: {}, name: 'Person{}'});", i, i));
|
|
583
|
+
auto res = conn->query(stringFormat("CREATE (:test {{id: {}, name: 'Person{}'}});", i, i));
|
|
581
584
|
ASSERT_TRUE(res->isSuccess());
|
|
582
585
|
}
|
|
583
586
|
|
|
@@ -650,7 +653,8 @@ TEST_F(EmptyDBTransactionTest, ConcurrentRelationshipUpdatesWithMixedTransaction
|
|
|
650
653
|
|
|
651
654
|
// Create nodes
|
|
652
655
|
for (auto i = 0; i < numTotalUpdates; ++i) {
|
|
653
|
-
auto res =
|
|
656
|
+
auto res =
|
|
657
|
+
conn->query(stringFormat("CREATE (:person {{id: {}, name: 'Person{}'}});", i, i));
|
|
654
658
|
ASSERT_TRUE(res->isSuccess());
|
|
655
659
|
}
|
|
656
660
|
|
|
@@ -660,7 +664,7 @@ TEST_F(EmptyDBTransactionTest, ConcurrentRelationshipUpdatesWithMixedTransaction
|
|
|
660
664
|
auto toID = i;
|
|
661
665
|
auto weight = 20.0;
|
|
662
666
|
auto res = conn->query(stringFormat("MATCH (a:person), (b:person) WHERE a.id = {} AND b.id "
|
|
663
|
-
"= {} CREATE (a)-[:knows {weight: {}}]->(b);",
|
|
667
|
+
"= {} CREATE (a)-[:knows {{weight: {}}}]->(b);",
|
|
664
668
|
fromID, toID, weight));
|
|
665
669
|
ASSERT_TRUE(res->isSuccess());
|
|
666
670
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-NAME count_rel_table
|
|
2
|
+
-PRERUN CREATE NODE TABLE account(ID INT64 PRIMARY KEY); CREATE REL TABLE follows(FROM account TO account); COPY account FROM "dataset/snap/amazon0601/parquet/amazon-nodes.parquet"; COPY follows FROM "dataset/snap/amazon0601/parquet/amazon-edges.parquet";
|
|
3
|
+
-QUERY MATCH ()-[r:follows]->() RETURN COUNT(*)
|
|
4
|
+
---- 1
|
|
5
|
+
3387388
|
|
@@ -543,6 +543,17 @@ std::vector<std::unique_ptr<QueryResult>> EmbeddedShell::processInput(std::strin
|
|
|
543
543
|
historyLine = input;
|
|
544
544
|
return queryResults;
|
|
545
545
|
}
|
|
546
|
+
// Normalize trailing semicolons
|
|
547
|
+
if (!unicodeInput.empty() && unicodeInput.back() == ';') {
|
|
548
|
+
// trim trailing ;
|
|
549
|
+
while (!unicodeInput.empty() && unicodeInput.back() == ';') {
|
|
550
|
+
unicodeInput.pop_back();
|
|
551
|
+
}
|
|
552
|
+
if (unicodeInput.empty()) {
|
|
553
|
+
return queryResults;
|
|
554
|
+
}
|
|
555
|
+
unicodeInput += ';';
|
|
556
|
+
}
|
|
546
557
|
// process shell commands
|
|
547
558
|
if (!continueLine && unicodeInput[0] == ':') {
|
|
548
559
|
processShellCommands(unicodeInput);
|
|
@@ -3306,7 +3306,7 @@ bool cypherComplete(const char* z) {
|
|
|
3306
3306
|
/* Token: */
|
|
3307
3307
|
/* State: ** SEMI WS OTHER */
|
|
3308
3308
|
/* 0 INVALID: */ {
|
|
3309
|
-
|
|
3309
|
+
2,
|
|
3310
3310
|
0,
|
|
3311
3311
|
2,
|
|
3312
3312
|
},
|
|
@@ -3553,8 +3553,8 @@ static int linenoiseEdit(int stdin_fd, int stdout_fd, char* buf, size_t buflen,
|
|
|
3553
3553
|
// check if this forms a complete Cypher statement or not or if enter is pressed in
|
|
3554
3554
|
// the middle of a line
|
|
3555
3555
|
l.buf[l.len] = '\0';
|
|
3556
|
-
if (l.buf[0] != ':' &&
|
|
3557
|
-
|
|
3556
|
+
if (l.buf[0] != ':' && l.pos == l.len && !linenoiseAllWhitespace(l.buf) &&
|
|
3557
|
+
!cypherComplete(l.buf)) {
|
|
3558
3558
|
if (linenoiseEditInsertMulti(&l, "\r\n")) {
|
|
3559
3559
|
return -1;
|
|
3560
3560
|
}
|