ladybug 0.22.0.dev20261024__tar.gz → 0.22.3__tar.gz

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 (78) hide show
  1. sdist/PKG-INFO +1 -1
  2. sdist/ladybug-source/CMakeLists.txt +2 -2
  3. sdist/ladybug-source/pixi.toml +9 -1
  4. sdist/ladybug-source/src/binder/bind/bind_ddl.cpp +30 -18
  5. sdist/ladybug-source/src/binder/bind/bind_file_scan.cpp +3 -3
  6. sdist/ladybug-source/src/binder/bind/copy/bind_copy_from.cpp +22 -14
  7. sdist/ladybug-source/src/binder/bind_expression/bind_case_expression.cpp +32 -12
  8. sdist/ladybug-source/src/binder/query/query_graph.cpp +10 -4
  9. sdist/ladybug-source/src/c_api/version.cpp +1 -1
  10. sdist/ladybug-source/src/catalog/catalog_entry/node_table_catalog_entry.cpp +2 -0
  11. sdist/ladybug-source/src/catalog/catalog_entry/rel_group_catalog_entry.cpp +2 -0
  12. sdist/ladybug-source/src/common/enums/storage_format.cpp +22 -2
  13. sdist/ladybug-source/src/common/serializer/buffered_file.cpp +23 -11
  14. sdist/ladybug-source/src/common/vector/value_vector.cpp +1 -1
  15. sdist/ladybug-source/src/expression_evaluator/lambda_evaluator.cpp +163 -9
  16. sdist/ladybug-source/src/expression_evaluator/list_slice_info.cpp +0 -33
  17. sdist/ladybug-source/src/function/list/list_filter.cpp +1 -7
  18. sdist/ladybug-source/src/function/list/list_slice_function.cpp +19 -2
  19. sdist/ladybug-source/src/function/list/list_sort_function.cpp +14 -2
  20. sdist/ladybug-source/src/function/list/list_transform.cpp +0 -4
  21. sdist/ladybug-source/src/function/list/quantifier_functions.cpp +0 -6
  22. sdist/ladybug-source/src/function/path/length_function.cpp +4 -1
  23. sdist/ladybug-source/src/function/string/CMakeLists.txt +1 -0
  24. sdist/ladybug-source/src/function/string/base_pad_function.cpp +39 -0
  25. sdist/ladybug-source/src/function/string/concat_ws.cpp +4 -17
  26. sdist/ladybug-source/src/function/string/regex_full_match_function.cpp +3 -6
  27. sdist/ladybug-source/src/function/string/regex_replace_function.cpp +6 -7
  28. sdist/ladybug-source/src/function/string/split_part.cpp +0 -5
  29. sdist/ladybug-source/src/function/string/string_split_function.cpp +2 -3
  30. sdist/ladybug-source/src/function/vector_string_functions.cpp +20 -3
  31. sdist/ladybug-source/src/graph/on_disk_graph.cpp +7 -1
  32. sdist/ladybug-source/src/include/binder/query/query_graph.h +2 -0
  33. sdist/ladybug-source/src/include/common/enums/storage_format.h +2 -1
  34. sdist/ladybug-source/src/include/common/types/string_t.h +4 -0
  35. sdist/ladybug-source/src/include/expression_evaluator/lambda_evaluator.h +4 -0
  36. sdist/ladybug-source/src/include/expression_evaluator/list_slice_info.h +10 -7
  37. sdist/ladybug-source/src/include/function/function.h +4 -0
  38. sdist/ladybug-source/src/include/function/list/functions/list_len_function.h +3 -3
  39. sdist/ladybug-source/src/include/function/string/functions/base_pad_function.h +19 -0
  40. sdist/ladybug-source/src/include/function/string/functions/base_regexp_function.h +7 -0
  41. sdist/ladybug-source/src/include/function/string/functions/repeat_function.h +1 -1
  42. sdist/ladybug-source/src/include/function/string/functions/substr_function.h +17 -6
  43. sdist/ladybug-source/src/include/optimizer/remove_unnecessary_distinct_optimizer.h +4 -3
  44. sdist/ladybug-source/src/include/optimizer/remove_unnecessary_order_by_optimizer.h +10 -9
  45. sdist/ladybug-source/src/include/parser/visitor/statement_read_write_analyzer.h +1 -0
  46. sdist/ladybug-source/src/include/planner/operator/sip/side_way_info_passing.h +6 -5
  47. sdist/ladybug-source/src/include/processor/operator/intersect/intersect.h +9 -3
  48. sdist/ladybug-source/src/include/processor/operator/scan/scan_rel_table.h +5 -0
  49. sdist/ladybug-source/src/include/storage/storage_version_info.h +2 -1
  50. sdist/ladybug-source/src/include/storage/table/columnar_node_table_base.h +5 -3
  51. sdist/ladybug-source/src/include/storage/table/columnar_rel_table_base.h +3 -1
  52. sdist/ladybug-source/src/include/storage/table/external_table_hooks.h +102 -0
  53. sdist/ladybug-source/src/include/storage/table/ice_disk_rel_table.h +3 -1
  54. sdist/ladybug-source/src/include/storage/table/rel_table.h +3 -1
  55. sdist/ladybug-source/src/optimizer/acc_hash_join_optimizer.cpp +74 -3
  56. sdist/ladybug-source/src/optimizer/bool_folding_optimizer.cpp +12 -2
  57. sdist/ladybug-source/src/optimizer/filter_push_down_optimizer.cpp +4 -0
  58. sdist/ladybug-source/src/optimizer/limit_push_down_optimizer.cpp +18 -3
  59. sdist/ladybug-source/src/optimizer/optimizer.cpp +8 -8
  60. sdist/ladybug-source/src/optimizer/remove_unnecessary_distinct_optimizer.cpp +12 -7
  61. sdist/ladybug-source/src/optimizer/remove_unnecessary_join_optimizer.cpp +2 -1
  62. sdist/ladybug-source/src/optimizer/remove_unnecessary_order_by_optimizer.cpp +31 -3
  63. sdist/ladybug-source/src/planner/plan/append_join.cpp +1 -1
  64. sdist/ladybug-source/src/planner/plan/plan_join_order.cpp +43 -2
  65. sdist/ladybug-source/src/processor/map/map_extend.cpp +18 -4
  66. sdist/ladybug-source/src/processor/map/map_intersect.cpp +8 -2
  67. sdist/ladybug-source/src/processor/operator/intersect/intersect.cpp +32 -26
  68. sdist/ladybug-source/src/processor/operator/scan/scan_multi_rel_tables.cpp +33 -1
  69. sdist/ladybug-source/src/processor/operator/scan/scan_node_table.cpp +23 -6
  70. sdist/ladybug-source/src/processor/operator/scan/scan_rel_table.cpp +17 -3
  71. sdist/ladybug-source/src/processor/operator/simple/import_db.cpp +6 -0
  72. sdist/ladybug-source/src/storage/file_handle.cpp +3 -1
  73. sdist/ladybug-source/src/storage/storage_manager.cpp +35 -3
  74. sdist/ladybug-source/src/storage/table/CMakeLists.txt +1 -0
  75. sdist/ladybug-source/src/storage/table/column.cpp +2 -1
  76. sdist/ladybug-source/src/storage/table/external_table_hooks.cpp +88 -0
  77. sdist/ladybug.egg-info/PKG-INFO +1 -1
  78. sdist/pyproject.toml +1 -1
sdist/PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ladybug
3
- Version: 0.22.0.dev20261024
3
+ Version: 0.22.3
4
4
  Summary: Highly scalable, extremely fast, easy-to-use embeddable graph database
5
5
  Home-page: https://github.com/LadybugDB/ladybug
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  cmake_minimum_required(VERSION 3.15)
2
2
 
3
- project(Lbug VERSION 0.22.0.20261024 LANGUAGES CXX C)
3
+ project(Lbug VERSION 0.22.3 LANGUAGES CXX C)
4
4
 
5
5
  option(SINGLE_THREADED "Single-threaded mode" FALSE)
6
6
  if(SINGLE_THREADED)
@@ -553,7 +553,7 @@ unset(LBUG_LINK_LIBRARIES)
553
553
 
554
554
  add_definitions(-DLBUG_ROOT_DIRECTORY="${PROJECT_SOURCE_DIR}")
555
555
  add_definitions(-DLBUG_CMAKE_VERSION="${CMAKE_PROJECT_VERSION}")
556
- add_definitions(-DLBUG_EXTENSION_VERSION="dev")
556
+ add_definitions(-DLBUG_EXTENSION_VERSION="0.22.0")
557
557
 
558
558
  if(BUILD_LBUG OR LBUG_API_USE_PRECOMPILED_LIB)
559
559
  include_directories(
@@ -5,5 +5,13 @@ platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64", "win-64"]
5
5
 
6
6
  [dependencies]
7
7
  libadbc-driver-manager = ">=1.11.0,<2"
8
- libarrow = ">=25.0.0"
8
+ # Keep the conda libarrow on 25.x: the algo extension builds against the
9
+ # system Apache Arrow 26 (libarrow.so.2600 from packages.apache.org), while
10
+ # the pixi env only needs Arrow 25 for libadbc-driver-manager. Letting this
11
+ # float to >=26 shadows the system 2600 at runtime via LD_LIBRARY_PATH, and
12
+ # conda-forge's libarrow 26.0.0 currently resolves with simdjson 5.0.8
13
+ # (libsimdjson.so.35) while its DT_NEEDED asks for libsimdjson.so.34,
14
+ # breaking `LOAD EXTENSION 'algo'` with "libsimdjson.so.34: cannot open
15
+ # shared object file". Revisit once conda-forge's arrow/simdjson are coherent.
16
+ libarrow = ">=25.0.0,<26"
9
17
  llvm-openmp = ">=22.1.8,<23"
@@ -324,10 +324,11 @@ BoundCreateTableInfo Binder::bindCreateRelTableGroupInfo(const CreateTableInfo*
324
324
  if (!storage.empty()) {
325
325
  auto dotPos = storage.find('.');
326
326
  // Check if storage is database.table format by verifying the attached database exists
327
- // Handle special case where icebug-disk storage could contain a dot
327
+ // Handle special case where columnar storage paths (e.g. icebug-disk, lance)
328
+ // could contain a dot. Only unformatted storage is probed as database.table.
328
329
  // Otherwise, treat as file path (e.g., "dataset/demo-db/icebug-disk/demo" or
329
330
  // "data.parquet")
330
- if (storageFormat != StorageFormat::ICEBUG_DISK && dotPos != std::string::npos) {
331
+ if (storageFormat == StorageFormat::NONE && dotPos != std::string::npos) {
331
332
  std::string dbName = storage.substr(0, dotPos);
332
333
  std::string tableName = storage.substr(dotPos + 1);
333
334
  if (!dbName.empty()) {
@@ -422,22 +423,25 @@ BoundCreateTableInfo Binder::bindCreateRelTableGroupInfo(const CreateTableInfo*
422
423
  }
423
424
  }
424
425
 
425
- bool isSrcIcebugDisk = srcEntry->getType() == CatalogEntryType::NODE_TABLE_ENTRY ?
426
- srcEntry->ptrCast<NodeTableCatalogEntry>()->getStorageFormat() ==
427
- StorageFormat::ICEBUG_DISK :
428
- false;
429
- bool isDstIcebugDisk = dstEntry->getType() == CatalogEntryType::NODE_TABLE_ENTRY ?
430
- dstEntry->ptrCast<NodeTableCatalogEntry>()->getStorageFormat() ==
431
- StorageFormat::ICEBUG_DISK :
432
- false;
433
- bool isRelIcebugDisk = (storageFormat == StorageFormat::ICEBUG_DISK);
434
-
435
- // We don't allow mixing icebug-disk tables with non-icebug-disk tables
436
- // We only allow icebug-disk rel tables to connect icebug-disk node tables
437
- if ((!isRelIcebugDisk && (isSrcIcebugDisk || isDstIcebugDisk)) ||
438
- (isRelIcebugDisk && (!isSrcIcebugDisk || !isDstIcebugDisk))) {
439
- throw BinderException(
440
- "Cannot mix icebug-disk tables with non-icebug-disk tables in CREATE REL TABLE.");
426
+ auto getEntryStorageFormat = [](TableCatalogEntry* entry) {
427
+ return entry->getType() == CatalogEntryType::NODE_TABLE_ENTRY ?
428
+ entry->ptrCast<NodeTableCatalogEntry>()->getStorageFormat() :
429
+ StorageFormat::NONE;
430
+ };
431
+ const auto srcStorageFormat = getEntryStorageFormat(srcEntry);
432
+ const auto dstStorageFormat = getEntryStorageFormat(dstEntry);
433
+
434
+ // We don't allow mixing columnar-format tables (e.g. icebug-disk, lance) with
435
+ // tables of another format. A rel table must use the same storage format as the
436
+ // node tables it connects.
437
+ if (storageFormat != srcStorageFormat || storageFormat != dstStorageFormat) {
438
+ const auto oddFormat = storageFormat != StorageFormat::NONE ? storageFormat :
439
+ srcStorageFormat != StorageFormat::NONE ? srcStorageFormat :
440
+ dstStorageFormat;
441
+ const auto oddName = StorageFormatUtils::toString(oddFormat);
442
+ throw BinderException(std::format("Cannot mix {} tables with non-{} tables in CREATE "
443
+ "REL TABLE.",
444
+ oddName, oddName));
441
445
  }
442
446
 
443
447
  // Use the actual shadow table IDs, not FOREIGN_TABLE_ID
@@ -906,6 +910,14 @@ std::unique_ptr<BoundStatement> Binder::bindAlterFromToConnection(
906
910
  auto info = alter.getInfo();
907
911
  auto extraInfo = info->extraInfo->constPtrCast<ExtraAddFromToConnection>();
908
912
  auto tableName = info->tableName;
913
+ // Only a rel table has FROM/TO connections; Alter would cast any other entry to one.
914
+ auto catalog = Catalog::Get(*clientContext);
915
+ auto transaction = transaction::Transaction::Get(*clientContext);
916
+ if (catalog->containsTable(transaction, tableName) &&
917
+ catalog->getTableCatalogEntry(transaction, tableName)->getType() !=
918
+ CatalogEntryType::REL_GROUP_ENTRY) {
919
+ throw BinderException(std::format("{} is not of type REL.", tableName));
920
+ }
909
921
  auto [srcTableEntry, srcDbName] = bindNodeTableEntry(extraInfo->srcTableName);
910
922
  auto [dstTableEntry, dstDbName] = bindNodeTableEntry(extraInfo->dstTableName);
911
923
  auto srcTableID = srcTableEntry->getTableID();
@@ -198,9 +198,9 @@ std::unique_ptr<BoundBaseScanSource> Binder::bindQueryScanSource(const BaseScanS
198
198
  throw BinderException(std::format("Query returns {} columns but {} columns were expected.",
199
199
  columns.size(), columnNames.size()));
200
200
  }
201
- for (auto i = 0u; i < columns.size(); ++i) {
202
- columns[i]->setAlias(columnNames[i]);
203
- }
201
+ // The columns are matched to columnNames by position when the COPY operator is bound. They
202
+ // must not be renamed here: a RETURN list can bind the same Expression object more than once
203
+ // (e.g. RETURN i, i), and setAlias would leave that shared object holding one name only.
204
204
  auto scanInfo = BoundQueryScanSourceInfo(bindParsingOptions(options));
205
205
  return std::make_unique<BoundQueryScanSource>(std::move(boundStatement), std::move(scanInfo));
206
206
  }
@@ -116,18 +116,26 @@ static void bindExpectedRelColumns(const RelGroupCatalogEntry& entry,
116
116
  const CopyFromColumnInfo& info, std::vector<std::string>& columnNames,
117
117
  std::vector<LogicalType>& columnTypes);
118
118
 
119
+ // Returns the source column that feeds a table property. A scan source names its leading columns
120
+ // from expectedColumnNames by position, so the position decides which column a property reads.
121
+ // The name of the bound expression cannot be used for this: a query source may return one shared
122
+ // Expression object for several columns (e.g. RETURN i, i). Columns past the expected ones carry
123
+ // copy warnings. A source bound without expected names, such as a table whose only column is
124
+ // SERIAL, keeps the names it read from the file and is matched by those names.
119
125
  static std::pair<ColumnEvaluateType, std::shared_ptr<Expression>> matchColumnExpression(
120
- const expression_vector& columns, const PropertyDefinition& property,
121
- ExpressionBinder& expressionBinder) {
122
- for (auto& column : columns) {
123
- if (property.getName() == column->toString()) {
124
- if (column->dataType == property.getType()) {
125
- return {ColumnEvaluateType::REFERENCE, column};
126
- } else {
127
- return {ColumnEvaluateType::CAST,
128
- expressionBinder.forceCast(column, property.getType())};
129
- }
126
+ const expression_vector& columns, const std::vector<std::string>& expectedColumnNames,
127
+ const PropertyDefinition& property, ExpressionBinder& expressionBinder) {
128
+ for (auto i = 0u; i < columns.size(); ++i) {
129
+ auto columnName =
130
+ i < expectedColumnNames.size() ? expectedColumnNames[i] : columns[i]->toString();
131
+ if (property.getName() != columnName) {
132
+ continue;
133
+ }
134
+ auto& column = columns[i];
135
+ if (column->dataType == property.getType()) {
136
+ return {ColumnEvaluateType::REFERENCE, column};
130
137
  }
138
+ return {ColumnEvaluateType::CAST, expressionBinder.forceCast(column, property.getType())};
131
139
  }
132
140
  return {ColumnEvaluateType::DEFAULT, expressionBinder.bindExpression(*property.defaultExpr)};
133
141
  }
@@ -163,8 +171,8 @@ BoundCopyFromInfo Binder::bindCopyNodeFromInfo(std::string tableName,
163
171
  expression_vector columns;
164
172
  std::vector<ColumnEvaluateType> evaluateTypes;
165
173
  for (auto& property : properties) {
166
- auto [evaluateType, column] =
167
- matchColumnExpression(boundSource->getColumns(), property, expressionBinder);
174
+ auto [evaluateType, column] = matchColumnExpression(boundSource->getColumns(),
175
+ expectedColumnNames, property, expressionBinder);
168
176
  columns.push_back(column);
169
177
  evaluateTypes.push_back(evaluateType);
170
178
  }
@@ -247,8 +255,8 @@ BoundCopyFromInfo Binder::bindCopyRelFromInfo(std::string tableName,
247
255
  ColumnEvaluateType::REFERENCE, ColumnEvaluateType::REFERENCE};
248
256
  for (auto i = 1u; i < properties.size(); ++i) { // skip internal ID
249
257
  auto& property = properties[i];
250
- auto [evaluateType, column] =
251
- matchColumnExpression(boundSource->getColumns(), property, expressionBinder);
258
+ auto [evaluateType, column] = matchColumnExpression(boundSource->getColumns(),
259
+ expectedColumnNames, property, expressionBinder);
252
260
  columnExprs.push_back(column);
253
261
  evaluateTypes.push_back(evaluateType);
254
262
  }
@@ -1,7 +1,10 @@
1
+ #include <algorithm>
2
+
1
3
  #include "binder/binder.h"
2
4
  #include "binder/expression/case_expression.h"
3
5
  #include "binder/expression/expression_util.h"
4
6
  #include "binder/expression_binder.h"
7
+ #include "function/cast/vector_cast_functions.h"
5
8
  #include "parser/expression/parsed_case_expression.h"
6
9
 
7
10
  using namespace lbug::common;
@@ -10,23 +13,40 @@ using namespace lbug::parser;
10
13
  namespace lbug {
11
14
  namespace binder {
12
15
 
16
+ // Whether implicitCastIfNecessary would accept casting the expression to the type.
17
+ static bool canImplicitlyCast(const Expression& expression, const LogicalType& type) {
18
+ return expression.dataType == type || type.containsAny() ||
19
+ ExpressionUtil::canCastStatically(expression, type) ||
20
+ function::CastFunction::hasImplicitCast(expression.dataType, type);
21
+ }
22
+
13
23
  std::shared_ptr<Expression> ExpressionBinder::bindCaseExpression(
14
24
  const ParsedExpression& parsedExpression) {
15
25
  auto& parsedCaseExpression = parsedExpression.constCast<ParsedCaseExpression>();
16
- auto resultType = LogicalType::ANY();
17
- // Resolve result type by checking each then expression type.
26
+ // The result type is the common type of every THEN and the ELSE, as for coalesce.
27
+ expression_vector results;
18
28
  for (auto i = 0u; i < parsedCaseExpression.getNumCaseAlternative(); ++i) {
19
- auto alternative = parsedCaseExpression.getCaseAlternative(i);
20
- auto boundThen = bindExpression(*alternative->thenExpression);
21
- if (boundThen->getDataType().getLogicalTypeID() != LogicalTypeID::ANY) {
22
- resultType = boundThen->getDataType().copy();
23
- }
29
+ results.push_back(
30
+ bindExpression(*parsedCaseExpression.getCaseAlternative(i)->thenExpression));
31
+ }
32
+ if (parsedCaseExpression.hasElseExpression()) {
33
+ results.push_back(bindExpression(*parsedCaseExpression.getElseExpression()));
24
34
  }
25
- // Resolve result type by else expression if above resolving fails.
26
- if (resultType.getLogicalTypeID() == LogicalTypeID::ANY &&
27
- parsedCaseExpression.hasElseExpression()) {
28
- auto elseExpression = bindExpression(*parsedCaseExpression.getElseExpression());
29
- resultType = elseExpression->getDataType().copy();
35
+ auto resultType = LogicalType::ANY();
36
+ if (!ExpressionUtil::tryCombineDataType(results, resultType) ||
37
+ !std::all_of(results.begin(), results.end(),
38
+ [&](const auto& result) { return canImplicitlyCast(*result, resultType); })) {
39
+ // No common type: take the last typed THEN, else the ELSE, and cast the rest to it.
40
+ resultType = LogicalType::ANY();
41
+ for (auto i = 0u; i < parsedCaseExpression.getNumCaseAlternative(); ++i) {
42
+ if (results[i]->getDataType().getLogicalTypeID() != LogicalTypeID::ANY) {
43
+ resultType = results[i]->getDataType().copy();
44
+ }
45
+ }
46
+ if (resultType.getLogicalTypeID() == LogicalTypeID::ANY &&
47
+ parsedCaseExpression.hasElseExpression()) {
48
+ resultType = results.back()->getDataType().copy();
49
+ }
30
50
  }
31
51
  auto name = binder->getUniqueExpressionName(parsedExpression.getRawName());
32
52
  // bind ELSE ...
@@ -99,16 +99,22 @@ std::vector<uint32_t> SubqueryGraph::getConnectedNodePos(const SubqueryGraph& nb
99
99
 
100
100
  std::unordered_set<uint32_t> SubqueryGraph::getNodePositionsIgnoringNodeSelector() const {
101
101
  std::unordered_set<uint32_t> result;
102
+ const auto nodes = getNodesSelectorWithRelEndpoints();
102
103
  for (auto nodePos = 0u; nodePos < queryGraph.getNumQueryNodes(); ++nodePos) {
103
- if (queryNodesSelector[nodePos]) {
104
+ if (nodes[nodePos]) {
104
105
  result.insert(nodePos);
105
106
  }
106
107
  }
108
+ return result;
109
+ }
110
+
111
+ std::bitset<MAX_NUM_QUERY_VARIABLES> SubqueryGraph::getNodesSelectorWithRelEndpoints() const {
112
+ auto result = queryNodesSelector;
107
113
  for (auto relPos = 0u; relPos < queryGraph.getNumQueryRels(); ++relPos) {
108
- auto rel = queryGraph.getQueryRel(relPos);
109
114
  if (queryRelsSelector[relPos]) {
110
- result.insert(queryGraph.getQueryNodeIdx(rel->getSrcNodeName()));
111
- result.insert(queryGraph.getQueryNodeIdx(rel->getDstNodeName()));
115
+ auto rel = queryGraph.getQueryRel(relPos);
116
+ result[queryGraph.getQueryNodeIdx(rel->getSrcNodeName())] = true;
117
+ result[queryGraph.getQueryNodeIdx(rel->getDstNodeName())] = true;
112
118
  }
113
119
  }
114
120
  return result;
@@ -7,7 +7,7 @@ char* lbug_get_version() {
7
7
  LBUG_C_API_GUARD_BEGIN
8
8
  auto version = lbug::main::Version::getVersion();
9
9
  if (version == nullptr || version[0] == '\0') {
10
- version = "0.21.2";
10
+ version = "0.22.3";
11
11
  }
12
12
  return convertToOwnedCString(version);
13
13
  LBUG_C_API_GUARD_END(nullptr)
@@ -31,6 +31,8 @@ static void upgradeLegacyStorageFormat(const std::string& storage,
31
31
  const auto lowerStorage = common::StringUtils::getLower(storage);
32
32
  if (lowerStorage.ends_with("parquet")) {
33
33
  storageFormat = common::StorageFormat::ICEBUG_DISK;
34
+ } else if (lowerStorage.ends_with(".lance")) {
35
+ storageFormat = common::StorageFormat::LANCE;
34
36
  }
35
37
  }
36
38
 
@@ -23,6 +23,8 @@ static void upgradeLegacyStorageFormat(const std::string& storage,
23
23
  const auto lowerStorage = common::StringUtils::getLower(storage);
24
24
  if (lowerStorage.ends_with("parquet")) {
25
25
  storageFormat = common::StorageFormat::ICEBUG_DISK;
26
+ } else if (lowerStorage.ends_with(".lance")) {
27
+ storageFormat = common::StorageFormat::LANCE;
26
28
  }
27
29
  }
28
30
 
@@ -10,8 +10,28 @@ StorageFormat StorageFormatUtils::fromString(const std::string& str) {
10
10
  if (str == "icebug-disk") {
11
11
  return StorageFormat::ICEBUG_DISK;
12
12
  }
13
- throw BinderException(
14
- std::format("Unsupported storage format '{}'. Valid options are: icebug-disk.", str));
13
+ if (str == "lance") {
14
+ return StorageFormat::LANCE;
15
+ }
16
+ throw BinderException(std::format(
17
+ "Unsupported storage format '{}'. Valid options are: icebug-disk, lance.", str));
18
+ }
19
+
20
+ std::string StorageFormatUtils::toString(StorageFormat format) {
21
+ switch (format) {
22
+ case StorageFormat::NONE: {
23
+ return "none";
24
+ }
25
+ case StorageFormat::ICEBUG_DISK: {
26
+ return "icebug-disk";
27
+ }
28
+ case StorageFormat::LANCE: {
29
+ return "lance";
30
+ }
31
+ default: {
32
+ return "unknown";
33
+ }
34
+ }
15
35
  }
16
36
 
17
37
  } // namespace common
@@ -1,5 +1,6 @@
1
1
  #include "common/serializer/buffered_file.h"
2
2
 
3
+ #include <algorithm>
3
4
  #include <cstring>
4
5
 
5
6
  #include "common/assert.h"
@@ -78,20 +79,31 @@ void BufferedFileReader::read(uint8_t* data, uint64_t size) {
78
79
  // Clear read buffer.
79
80
  fileOffset -= bufferSize;
80
81
  fileOffset += bufferOffset;
82
+ bufferOffset = 0;
83
+ bufferSize = 0;
84
+ // readFromFile silently short-reads at EOF, so check the range first and throw a
85
+ // catchable error instead of returning uninitialized bytes. A torn WAL tail (e.g. a
86
+ // crash or ENOSPC mid-commit) must surface as a replay failure, not a native crash.
87
+ if (fileOffset > fileSize || size > fileSize - fileOffset) {
88
+ throw RuntimeException(
89
+ std::format("Reading past the end of the file {} with size {} at offset {}",
90
+ fileInfo.path, fileSize, fileOffset));
91
+ }
81
92
  fileInfo.readFromFile(data, size, fileOffset);
82
93
  fileOffset += size;
83
- bufferOffset = bufferSize;
84
- } else if (bufferOffset + size <= bufferSize) {
85
- memcpy(data, &buffer[bufferOffset], size);
86
- bufferOffset += size;
87
94
  } else {
88
- auto toCopy = bufferSize - bufferOffset;
89
- memcpy(data, &buffer[bufferOffset], toCopy);
90
- bufferOffset += toCopy;
91
- readNextPage();
92
- auto remaining = size - toCopy;
93
- memcpy(data + toCopy, buffer.get(), remaining);
94
- bufferOffset += remaining;
95
+ // Loop across page boundaries: the final page of a torn file may hold fewer bytes
96
+ // than requested, and copying the full remainder would overrun the heap buffer.
97
+ while (size > 0) {
98
+ if (bufferOffset >= bufferSize) {
99
+ readNextPage();
100
+ }
101
+ const auto toCopy = std::min(size, bufferSize - bufferOffset);
102
+ memcpy(data, &buffer[bufferOffset], toCopy);
103
+ bufferOffset += toCopy;
104
+ data += toCopy;
105
+ size -= toCopy;
106
+ }
95
107
  }
96
108
  }
97
109
 
@@ -643,7 +643,7 @@ void ListVector::copyListEntryAndBufferMetaData(ValueVector& vector,
643
643
  for (auto i = 0u; i < otherSelVector.getSelSize(); ++i) {
644
644
  auto pos = selVector[i];
645
645
  auto otherPos = otherSelVector[i];
646
- vector.setNull(pos, other.isNull(pos));
646
+ vector.setNull(pos, other.isNull(otherPos));
647
647
  if (!other.isNull(otherPos)) {
648
648
  vector.setValue(pos, other.getValue<list_entry_t>(otherPos));
649
649
  }
@@ -1,5 +1,7 @@
1
1
  #include "expression_evaluator/lambda_evaluator.h"
2
2
 
3
+ #include <cstring>
4
+
3
5
  #include "binder/expression/lambda_expression.h"
4
6
  #include "common/exception/runtime.h"
5
7
  #include "expression_evaluator/expression_evaluator_visitor.h"
@@ -18,6 +20,141 @@ using namespace lbug::storage;
18
20
  namespace lbug {
19
21
  namespace evaluator {
20
22
 
23
+ // Points dst's lists at src's list data, so evaluators that keep a list's data vector when they
24
+ // are compiled, such as properties(), read the elements that gatherSlice references.
25
+ static void shareListData(const ValueVector& dst, const ValueVector& src) {
26
+ switch (src.dataType.getPhysicalType()) {
27
+ case PhysicalTypeID::STRUCT: {
28
+ const auto& dstFields = StructVector::getFieldVectors(&dst);
29
+ const auto& srcFields = StructVector::getFieldVectors(&src);
30
+ for (auto i = 0u; i < srcFields.size(); ++i) {
31
+ shareListData(*dstFields[i], *srcFields[i]);
32
+ }
33
+ } break;
34
+ case PhysicalTypeID::LIST:
35
+ case PhysicalTypeID::ARRAY: {
36
+ // Sharing the data vector also shares the structs and lists inside it.
37
+ ListVector::setDataVector(&dst, ListVector::getSharedDataVector(&src));
38
+ } break;
39
+ case PhysicalTypeID::ANY:
40
+ case PhysicalTypeID::BOOL:
41
+ case PhysicalTypeID::INT64:
42
+ case PhysicalTypeID::INT32:
43
+ case PhysicalTypeID::INT16:
44
+ case PhysicalTypeID::INT8:
45
+ case PhysicalTypeID::UINT64:
46
+ case PhysicalTypeID::UINT32:
47
+ case PhysicalTypeID::UINT16:
48
+ case PhysicalTypeID::UINT8:
49
+ case PhysicalTypeID::INT128:
50
+ case PhysicalTypeID::DOUBLE:
51
+ case PhysicalTypeID::FLOAT:
52
+ case PhysicalTypeID::INTERVAL:
53
+ case PhysicalTypeID::INTERNAL_ID:
54
+ case PhysicalTypeID::ALP_EXCEPTION_FLOAT:
55
+ case PhysicalTypeID::ALP_EXCEPTION_DOUBLE:
56
+ case PhysicalTypeID::UINT128:
57
+ case PhysicalTypeID::STRING:
58
+ case PhysicalTypeID::JSON:
59
+ case PhysicalTypeID::POINTER:
60
+ break;
61
+ }
62
+ }
63
+
64
+ static_assert(sizeof(bool) == sizeof(uint8_t), "BOOL values are gathered as uint8_t");
65
+ static_assert(sizeof(float) == sizeof(uint32_t), "FLOAT values are gathered as uint32_t");
66
+ static_assert(sizeof(double) == sizeof(uint64_t), "DOUBLE values are gathered as uint64_t");
67
+ static_assert(sizeof(uint128_t) == sizeof(int128_t), "UINT128 values are gathered as int128_t");
68
+
69
+ template<typename T>
70
+ static void gatherValues(ValueVector& dst, const ValueVector& src, const SelectionVector& srcSel) {
71
+ DASSERT(src.getNumBytesPerValue() == sizeof(T));
72
+ auto* dstValues = reinterpret_cast<T*>(dst.getData());
73
+ const auto* srcValues = reinterpret_cast<const T*>(src.getData());
74
+ for (sel_t i = 0; i < srcSel.getSelSize(); ++i) {
75
+ dstValues[i] = srcValues[srcSel[i]];
76
+ }
77
+ }
78
+
79
+ static void gatherBytes(ValueVector& dst, const ValueVector& src, const SelectionVector& srcSel) {
80
+ const auto numBytes = src.getNumBytesPerValue();
81
+ for (sel_t i = 0; i < srcSel.getSelSize(); ++i) {
82
+ memcpy(dst.getData() + i * numBytes, src.getData() + srcSel[i] * numBytes, numBytes);
83
+ }
84
+ }
85
+
86
+ // Copies the values of src at srcSel's positions to positions 0 to srcSel.getSelSize() - 1 of dst.
87
+ // Strings and the elements of lists are referenced rather than copied, which holds because src is
88
+ // not modified while the lambda evaluates the slice.
89
+ static void gatherSlice(ValueVector& dst, const SelectionVector& dstSel, const ValueVector& src,
90
+ const SelectionVector& srcSel) {
91
+ if (src.hasNoNullsGuarantee()) {
92
+ dst.setAllNonNull();
93
+ } else {
94
+ for (sel_t i = 0; i < srcSel.getSelSize(); ++i) {
95
+ dst.setNull(i, src.isNull(srcSel[i]));
96
+ }
97
+ }
98
+ switch (src.dataType.getPhysicalType()) {
99
+ case PhysicalTypeID::STRUCT: {
100
+ const auto& dstFields = StructVector::getFieldVectors(&dst);
101
+ const auto& srcFields = StructVector::getFieldVectors(&src);
102
+ for (auto i = 0u; i < srcFields.size(); ++i) {
103
+ gatherSlice(*dstFields[i], dstSel, *srcFields[i], srcSel);
104
+ }
105
+ } break;
106
+ case PhysicalTypeID::LIST:
107
+ case PhysicalTypeID::ARRAY: {
108
+ // Evaluators compiled against dst's data vector at init would read stale elements.
109
+ if (ListVector::getDataVector(&dst) != ListVector::getDataVector(&src)) {
110
+ throw RuntimeException("A list lambda's input replaced its data vector after init.");
111
+ }
112
+ ListVector::copyListEntryAndBufferMetaData(dst, dstSel, src, srcSel);
113
+ } break;
114
+ case PhysicalTypeID::BOOL:
115
+ case PhysicalTypeID::INT8:
116
+ case PhysicalTypeID::UINT8: {
117
+ gatherValues<uint8_t>(dst, src, srcSel);
118
+ } break;
119
+ case PhysicalTypeID::INT16:
120
+ case PhysicalTypeID::UINT16: {
121
+ gatherValues<uint16_t>(dst, src, srcSel);
122
+ } break;
123
+ case PhysicalTypeID::INT32:
124
+ case PhysicalTypeID::UINT32:
125
+ case PhysicalTypeID::FLOAT: {
126
+ gatherValues<uint32_t>(dst, src, srcSel);
127
+ } break;
128
+ case PhysicalTypeID::INT64:
129
+ case PhysicalTypeID::UINT64:
130
+ case PhysicalTypeID::DOUBLE: {
131
+ gatherValues<uint64_t>(dst, src, srcSel);
132
+ } break;
133
+ case PhysicalTypeID::INT128:
134
+ case PhysicalTypeID::UINT128: {
135
+ gatherValues<int128_t>(dst, src, srcSel);
136
+ } break;
137
+ case PhysicalTypeID::INTERVAL: {
138
+ gatherValues<interval_t>(dst, src, srcSel);
139
+ } break;
140
+ case PhysicalTypeID::INTERNAL_ID: {
141
+ gatherValues<internalID_t>(dst, src, srcSel);
142
+ } break;
143
+ case PhysicalTypeID::STRING:
144
+ case PhysicalTypeID::JSON: {
145
+ gatherValues<string_t>(dst, src, srcSel);
146
+ } break;
147
+ // Not list element types: ANY and POINTER vectors are never built, and ALP exceptions exist
148
+ // only in storage.
149
+ case PhysicalTypeID::ANY:
150
+ case PhysicalTypeID::POINTER:
151
+ case PhysicalTypeID::ALP_EXCEPTION_FLOAT:
152
+ case PhysicalTypeID::ALP_EXCEPTION_DOUBLE: {
153
+ gatherBytes(dst, src, srcSel);
154
+ } break;
155
+ }
156
+ }
157
+
21
158
  void ListLambdaEvaluator::init(const ResultSet& resultSet, ClientContext* clientContext) {
22
159
  for (auto& child : children) {
23
160
  child->init(resultSet, clientContext);
@@ -30,20 +167,23 @@ void ListLambdaEvaluator::init(const ResultSet& resultSet, ClientContext* client
30
167
  auto evaluators = collector.getEvaluators();
31
168
  auto lambdaVarState = std::make_shared<DataChunkState>();
32
169
  memoryManager = MemoryManager::Get(*clientContext);
170
+ const auto& elementType = ListType::getChildType(listInputVector->dataType);
171
+ if (listLambdaType != ListLambdaType::LIST_REDUCE && !evaluators.empty()) {
172
+ sliceParamVector = std::make_shared<ValueVector>(elementType.copy(), memoryManager);
173
+ }
33
174
  for (auto& evaluator : evaluators) {
34
- // For list_filter, list_transform:
35
- // The resultVector of lambdaEvaluator should be the list dataVector.
36
- // For list_reduce:
37
- // We should create two vectors for each lambda variable resultVector since we are going to
38
- // update the list elements during execution.
175
+ // list_reduce updates each lambda variable separately during execution, so each gets its
176
+ // own vector. The other lambdas read every use of their variable from sliceParamVector.
39
177
  evaluator->resultVector =
40
- listLambdaType != ListLambdaType::LIST_REDUCE ?
41
- ListVector::getSharedDataVector(listInputVector) :
42
- std::make_shared<ValueVector>(
43
- ListType::getChildType(listInputVector->dataType).copy(), memoryManager);
178
+ sliceParamVector != nullptr ?
179
+ sliceParamVector :
180
+ std::make_shared<ValueVector>(elementType.copy(), memoryManager);
44
181
  evaluator->resultVector->state = lambdaVarState;
45
182
  lambdaParamEvaluators.push_back(evaluator->ptrCast<LambdaParamEvaluator>());
46
183
  }
184
+ if (sliceParamVector != nullptr) {
185
+ shareListData(*sliceParamVector, *ListVector::getDataVector(listInputVector));
186
+ }
47
187
  lambdaRootEvaluator->init(resultSet, clientContext);
48
188
  resolveResultVector(resultSet, memoryManager);
49
189
  params.push_back(children[0]->resultVector);
@@ -63,10 +203,24 @@ void ListLambdaEvaluator::evaluateInternal() {
63
203
  auto selVectors = SelectionVector::fromValueVectors(params);
64
204
  do {
65
205
  sliceInfo.nextSlice();
206
+ if (sliceParamVector != nullptr) {
207
+ copySliceToParamVector(sliceInfo, *inputVector);
208
+ }
66
209
  execFunc(params, selVectors, *resultVector, resultVector->getSelVectorPtr(), &bindData);
67
210
  } while (!sliceInfo.done());
68
211
  }
69
212
 
213
+ // Many evaluators read an operand at the position they write its result to. The lambda variable
214
+ // therefore holds the slice's elements at positions 0 to sliceSize - 1, where the vectors computed
215
+ // from it hold their values, instead of selecting them at their offsets in the list data vector.
216
+ void ListLambdaEvaluator::copySliceToParamVector(const ListSliceInfo& sliceInfo,
217
+ const ValueVector& listVector) const {
218
+ auto& paramSelVector = sliceParamVector->state->getSelVectorUnsafe();
219
+ paramSelVector.setToUnfiltered(sliceInfo.getSliceSize());
220
+ gatherSlice(*sliceParamVector, paramSelVector, *ListVector::getDataVector(&listVector),
221
+ sliceInfo.getDataSelVector());
222
+ }
223
+
70
224
  void ListLambdaEvaluator::evaluate() {
71
225
  DASSERT(children.size() == 1);
72
226
  children[0]->evaluate();