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.
Files changed (46) hide show
  1. package/lbug-source/.github/workflows/ci-workflow.yml +9 -2
  2. package/lbug-source/CMakeLists.txt +15 -6
  3. package/lbug-source/Makefile +1 -2
  4. package/lbug-source/benchmark/serializer.py +13 -2
  5. package/lbug-source/extension/httpfs/test/test_files/http.test +1 -0
  6. package/lbug-source/scripts/generate_binary_demo.sh +1 -1
  7. package/lbug-source/src/include/optimizer/count_rel_table_optimizer.h +49 -0
  8. package/lbug-source/src/include/optimizer/logical_operator_visitor.h +6 -0
  9. package/lbug-source/src/include/planner/operator/logical_operator.h +1 -0
  10. package/lbug-source/src/include/planner/operator/scan/logical_count_rel_table.h +84 -0
  11. package/lbug-source/src/include/processor/operator/physical_operator.h +1 -0
  12. package/lbug-source/src/include/processor/operator/scan/count_rel_table.h +62 -0
  13. package/lbug-source/src/include/processor/plan_mapper.h +2 -0
  14. package/lbug-source/src/optimizer/CMakeLists.txt +1 -0
  15. package/lbug-source/src/optimizer/count_rel_table_optimizer.cpp +217 -0
  16. package/lbug-source/src/optimizer/logical_operator_visitor.cpp +6 -0
  17. package/lbug-source/src/optimizer/optimizer.cpp +6 -0
  18. package/lbug-source/src/planner/operator/logical_operator.cpp +2 -0
  19. package/lbug-source/src/planner/operator/scan/CMakeLists.txt +1 -0
  20. package/lbug-source/src/planner/operator/scan/logical_count_rel_table.cpp +24 -0
  21. package/lbug-source/src/processor/map/CMakeLists.txt +1 -0
  22. package/lbug-source/src/processor/map/map_count_rel_table.cpp +55 -0
  23. package/lbug-source/src/processor/map/plan_mapper.cpp +3 -0
  24. package/lbug-source/src/processor/operator/physical_operator.cpp +2 -0
  25. package/lbug-source/src/processor/operator/scan/CMakeLists.txt +1 -0
  26. package/lbug-source/src/processor/operator/scan/count_rel_table.cpp +137 -0
  27. package/lbug-source/test/common/string_format.cpp +9 -1
  28. package/lbug-source/test/copy/copy_test.cpp +4 -4
  29. package/lbug-source/test/graph_test/CMakeLists.txt +1 -1
  30. package/lbug-source/test/optimizer/optimizer_test.cpp +46 -0
  31. package/lbug-source/test/test_helper/CMakeLists.txt +1 -1
  32. package/lbug-source/test/test_runner/CMakeLists.txt +1 -1
  33. package/lbug-source/test/test_runner/insert_by_row.cpp +6 -8
  34. package/lbug-source/test/test_runner/multi_copy_split.cpp +2 -4
  35. package/lbug-source/test/transaction/checkpoint_test.cpp +1 -1
  36. package/lbug-source/test/transaction/transaction_test.cpp +19 -15
  37. package/lbug-source/tools/benchmark/count_rel_table.benchmark +5 -0
  38. package/lbug-source/tools/shell/embedded_shell.cpp +11 -0
  39. package/lbug-source/tools/shell/linenoise.cpp +3 -3
  40. package/lbug-source/tools/shell/test/test_helper.py +1 -1
  41. package/lbug-source/tools/shell/test/test_shell_basics.py +12 -0
  42. package/package.json +1 -1
  43. package/prebuilt/lbugjs-darwin-arm64.node +0 -0
  44. package/prebuilt/lbugjs-linux-arm64.node +0 -0
  45. package/prebuilt/lbugjs-linux-x64.node +0 -0
  46. 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
@@ -7,6 +7,7 @@ add_library(lbug_processor_mapper
7
7
  map_acc_hash_join.cpp
8
8
  map_accumulate.cpp
9
9
  map_aggregate.cpp
10
+ map_count_rel_table.cpp
10
11
  map_standalone_call.cpp
11
12
  map_table_function_call.cpp
12
13
  map_copy_to.cpp
@@ -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:
@@ -1,5 +1,6 @@
1
1
  add_library(lbug_processor_operator_scan
2
2
  OBJECT
3
+ count_rel_table.cpp
3
4
  primary_key_scan_node_table.cpp
4
5
  scan_multi_rel_tables.cpp
5
6
  scan_node_table.cpp
@@ -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(stringFormat("{} {} {}", literal_character, enum_val, signed_int8), "a 1 4");
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 =
@@ -8,7 +8,7 @@ target_include_directories(
8
8
  PUBLIC
9
9
  ../include/
10
10
  )
11
- target_link_libraries(graph_test PUBLIC GTEST_LIB lbug)
11
+ target_link_libraries(graph_test PUBLIC GTEST_LIB lbug_shared)
12
12
 
13
13
  add_library(
14
14
  api_graph_test
@@ -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
@@ -8,7 +8,7 @@ target_include_directories(
8
8
  PUBLIC
9
9
  ../include/
10
10
  )
11
- target_link_libraries(test_helper PUBLIC lbug)
11
+ target_link_libraries(test_helper PUBLIC lbug_shared)
12
12
 
13
13
  add_library(
14
14
  api_test_helper
@@ -15,4 +15,4 @@ target_include_directories(
15
15
  ../include/
16
16
  )
17
17
 
18
- target_link_libraries(test_runner PUBLIC GTEST_LIB lbug)
18
+ target_link_libraries(test_runner PUBLIC GTEST_LIB lbug_shared)
@@ -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(query, header, filePath, name, "{" + body + "}");
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(query, header, filePath, from.name, to.name, from.property, to.property,
130
- name, "{" + body + "}");
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
- std::string loadQuery = "COPY (LOAD FROM {} RETURN *) TO '{}';";
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(query, tableName, file);
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 = conn->query(stringFormat("CREATE (:test {id: {}, name: 'Person{}'});", id, id));
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 = conn->query(stringFormat("CREATE (:person {id: {}, name: 'Person{}'});", i, i));
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{}'});", i,
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 = conn->query(stringFormat("CREATE (:person {id: {}, name: 'Person{}'});", i, i));
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 = conn->query(stringFormat("CREATE (:person {id: {}, name: 'Person{}'});", i, i));
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
- 1,
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
- (l.pos != l.len || linenoiseAllWhitespace(l.buf) || !cypherComplete(l.buf))) {
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
  }