diff --git a/.gitignore b/.gitignore index fd099b2a..6f51e550 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ tests/bindings/**/*.node /bindings/**/test-results/ /bindings/**/target/ /bindings/**/node_modules/ +/bindings/**/.tmp/ /bindings/**/.dart_tool/ /bindings/**/.pub/ /bindings/**/.pub-cache/ diff --git a/Cargo.lock b/Cargo.lock index 3025f7ce..89af2c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,7 +831,7 @@ dependencies = [ [[package]] name = "decentdb" -version = "2.16.0" +version = "2.16.1" dependencies = [ "base64 0.22.1", "chacha20", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "decentdb-benchmark" -version = "2.16.0" +version = "2.16.1" dependencies = [ "anyhow", "clap", @@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "decentdb-cli" -version = "2.16.0" +version = "2.16.1" dependencies = [ "anyhow", "clap", @@ -895,7 +895,7 @@ dependencies = [ [[package]] name = "decentdb-migrate" -version = "2.16.0" +version = "2.16.1" dependencies = [ "anyhow", "clap", @@ -1859,7 +1859,7 @@ dependencies = [ [[package]] name = "libpg_query_sys" -version = "2.16.0" +version = "2.16.1" dependencies = [ "pg_query", ] diff --git a/Cargo.toml b/Cargo.toml index df061379..7be2cf73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ exclude = [ resolver = "2" [workspace.package] -version = "2.16.0" +version = "2.16.1" edition = "2021" authors = ["Steven Hildreth"] license = "Apache-2.0" diff --git a/VERSION b/VERSION index 75249069..0e7079b6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.16.0 +2.16.1 diff --git a/benchmarks/rust-baseline/Cargo.lock b/benchmarks/rust-baseline/Cargo.lock index c19f29ea..35f10384 100644 --- a/benchmarks/rust-baseline/Cargo.lock +++ b/benchmarks/rust-baseline/Cargo.lock @@ -694,7 +694,7 @@ dependencies = [ [[package]] name = "decentdb" -version = "2.16.0" +version = "2.16.1" dependencies = [ "base64", "chacha20", @@ -1531,7 +1531,7 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libpg_query_sys" -version = "2.16.0" +version = "2.16.1" dependencies = [ "pg_query", ] diff --git a/bindings/dart/dart/pubspec.yaml b/bindings/dart/dart/pubspec.yaml index 1d0e3dce..4161fad8 100644 --- a/bindings/dart/dart/pubspec.yaml +++ b/bindings/dart/dart/pubspec.yaml @@ -1,6 +1,6 @@ name: decentdb description: Dart FFI bindings for the Rust DecentDB C ABI. -version: 2.16.0 +version: 2.16.1 repository: https://github.com/sphildreth/decentdb homepage: https://github.com/sphildreth/decentdb/tree/main/bindings/dart diff --git a/bindings/dart/examples/console/pubspec.lock b/bindings/dart/examples/console/pubspec.lock index 067d6d82..7db94d93 100644 --- a/bindings/dart/examples/console/pubspec.lock +++ b/bindings/dart/examples/console/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../dart" relative: true source: path - version: "2.16.0" + version: "2.16.1" ffi: dependency: transitive description: diff --git a/bindings/dart/examples/console_complex/pubspec.lock b/bindings/dart/examples/console_complex/pubspec.lock index 067d6d82..7db94d93 100644 --- a/bindings/dart/examples/console_complex/pubspec.lock +++ b/bindings/dart/examples/console_complex/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../dart" relative: true source: path - version: "2.16.0" + version: "2.16.1" ffi: dependency: transitive description: diff --git a/bindings/dart/examples/flutter_desktop/pubspec.lock b/bindings/dart/examples/flutter_desktop/pubspec.lock index 067d6d82..7db94d93 100644 --- a/bindings/dart/examples/flutter_desktop/pubspec.lock +++ b/bindings/dart/examples/flutter_desktop/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../dart" relative: true source: path - version: "2.16.0" + version: "2.16.1" ffi: dependency: transitive description: diff --git a/bindings/dart/flutter/android/build.gradle b/bindings/dart/flutter/android/build.gradle index efaa586f..96794b6b 100644 --- a/bindings/dart/flutter/android/build.gradle +++ b/bindings/dart/flutter/android/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'dev.decentdb.decentdb_flutter' -version = '2.16.0' +version = '2.16.1' android { namespace 'dev.decentdb.decentdb_flutter' diff --git a/bindings/dart/flutter/example/pubspec.lock b/bindings/dart/flutter/example/pubspec.lock index 28d072f1..6701470f 100644 --- a/bindings/dart/flutter/example/pubspec.lock +++ b/bindings/dart/flutter/example/pubspec.lock @@ -71,14 +71,14 @@ packages: path: "../../dart" relative: true source: path - version: "2.16.0" + version: "2.16.1" decentdb_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.16.0" + version: "2.16.1" fake_async: dependency: transitive description: diff --git a/bindings/dart/flutter/example/pubspec.yaml b/bindings/dart/flutter/example/pubspec.yaml index 2b9e0ec8..2f7d2ea6 100644 --- a/bindings/dart/flutter/example/pubspec.yaml +++ b/bindings/dart/flutter/example/pubspec.yaml @@ -1,7 +1,7 @@ name: decentdb_flutter_example description: Reference Flutter mobile app for DecentDB. publish_to: none -version: 2.16.0 +version: 2.16.1 environment: sdk: ^3.0.0 diff --git a/bindings/dart/flutter/ios/decentdb_flutter.podspec b/bindings/dart/flutter/ios/decentdb_flutter.podspec index fac4d637..4ef62270 100644 --- a/bindings/dart/flutter/ios/decentdb_flutter.podspec +++ b/bindings/dart/flutter/ios/decentdb_flutter.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'decentdb_flutter' - s.version = '2.16.0' + s.version = '2.16.1' s.summary = 'Flutter mobile integration helpers for DecentDB.' s.description = 'Provides Flutter registration and native artifact wiring for the DecentDB Dart FFI package.' s.homepage = 'https://github.com/sphildreth/decentdb' diff --git a/bindings/dart/flutter/pubspec.lock b/bindings/dart/flutter/pubspec.lock index 8ea01a2b..26243e13 100644 --- a/bindings/dart/flutter/pubspec.lock +++ b/bindings/dart/flutter/pubspec.lock @@ -71,7 +71,7 @@ packages: path: "../dart" relative: true source: path - version: "2.16.0" + version: "2.16.1" fake_async: dependency: transitive description: diff --git a/bindings/dart/flutter/pubspec.yaml b/bindings/dart/flutter/pubspec.yaml index ff320db9..a3c66d9c 100644 --- a/bindings/dart/flutter/pubspec.yaml +++ b/bindings/dart/flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: decentdb_flutter description: Flutter mobile integration helpers for the DecentDB Dart FFI package. -version: 2.16.0 +version: 2.16.1 publish_to: none repository: https://github.com/sphildreth/decentdb homepage: https://github.com/sphildreth/decentdb/tree/main/bindings/dart/flutter diff --git a/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF b/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF index 604e3ff5..a65318f5 100644 --- a/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF +++ b/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: DecentDB DBeaver Extension Bundle-SymbolicName: org.jkiss.dbeaver.ext.decentdb;singleton:=true -Bundle-Version: 2.16.0 +Bundle-Version: 2.16.1 Bundle-Activator: org.jkiss.dbeaver.ext.decentdb.DecentDBActivator Bundle-Vendor: DecentDB Contributors Require-Bundle: org.eclipse.core.runtime, @@ -11,5 +11,5 @@ Require-Bundle: org.eclipse.core.runtime, org.jkiss.dbeaver.ext.generic Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ClassPath: ., - lib/decentdb-jdbc-2.16.0.jar + lib/decentdb-jdbc-2.16.1.jar Export-Package: org.jkiss.dbeaver.ext.decentdb.model diff --git a/bindings/java/dbeaver-extension/build.gradle b/bindings/java/dbeaver-extension/build.gradle index d86f38f1..7c69804b 100644 --- a/bindings/java/dbeaver-extension/build.gradle +++ b/bindings/java/dbeaver-extension/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'org.jkiss.dbeaver.ext' -version = '2.16.0' +version = '2.16.1' java { sourceCompatibility = JavaVersion.VERSION_21 diff --git a/bindings/java/driver/build.gradle b/bindings/java/driver/build.gradle index b40646ca..b12af95f 100644 --- a/bindings/java/driver/build.gradle +++ b/bindings/java/driver/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'com.decentdb' -version = '2.16.0' +version = '2.16.1' def repoRoot = file("${rootProject.projectDir}/../..") def nativeLibDirPath = project.findProperty('nativeLibDir') ?: diff --git a/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java b/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java index 38f884cc..5adf48af 100644 --- a/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java +++ b/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java @@ -28,7 +28,7 @@ public final class DecentDBDriver implements Driver { public static final String URL_PREFIX = "jdbc:decentdb:"; - public static final String DRIVER_VERSION = "2.16.0"; + public static final String DRIVER_VERSION = "2.16.1"; public static final int DRIVER_MAJOR_VERSION = 1; public static final int DRIVER_MINOR_VERSION = 8; diff --git a/bindings/node/decentdb/package-lock.json b/bindings/node/decentdb/package-lock.json index 759d6beb..26ee25e2 100644 --- a/bindings/node/decentdb/package-lock.json +++ b/bindings/node/decentdb/package-lock.json @@ -1,12 +1,12 @@ { "name": "decentdb-native", - "version": "2.16.0", + "version": "2.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "decentdb-native", - "version": "2.16.0", + "version": "2.16.1", "devDependencies": { "node-gyp": "^12.2.0" } diff --git a/bindings/node/decentdb/package.json b/bindings/node/decentdb/package.json index 0545bb58..f2075dca 100644 --- a/bindings/node/decentdb/package.json +++ b/bindings/node/decentdb/package.json @@ -1,6 +1,6 @@ { "name": "decentdb-native", - "version": "2.16.0", + "version": "2.16.1", "private": true, "description": "DecentDB Node.js native addon (N-API) + thin JS wrapper", "main": "index.js", diff --git a/bindings/node/knex-decentdb/package-lock.json b/bindings/node/knex-decentdb/package-lock.json index eadcbc1f..04347e2b 100644 --- a/bindings/node/knex-decentdb/package-lock.json +++ b/bindings/node/knex-decentdb/package-lock.json @@ -1,12 +1,12 @@ { "name": "knex-decentdb", - "version": "2.16.0", + "version": "2.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "knex-decentdb", - "version": "2.16.0", + "version": "2.16.1", "dependencies": { "decentdb-native": "file:../decentdb" }, @@ -16,7 +16,7 @@ }, "../decentdb": { "name": "decentdb-native", - "version": "2.16.0", + "version": "2.16.1", "devDependencies": { "node-gyp": "^12.2.0" } diff --git a/bindings/node/knex-decentdb/package.json b/bindings/node/knex-decentdb/package.json index 93a93a88..44759dee 100644 --- a/bindings/node/knex-decentdb/package.json +++ b/bindings/node/knex-decentdb/package.json @@ -1,6 +1,6 @@ { "name": "knex-decentdb", - "version": "2.16.0", + "version": "2.16.1", "private": true, "description": "Knex client/dialect for DecentDB", "main": "index.js", diff --git a/bindings/python/.tmp/bench_complex_results.json b/bindings/python/.tmp/bench_complex_results.json index 5b8de330..7f9a60b2 100644 --- a/bindings/python/.tmp/bench_complex_results.json +++ b/bindings/python/.tmp/bench_complex_results.json @@ -11,136 +11,136 @@ "comparisons": { "complex": { "aggregate_p50_ms": { - "decentdb": 0.027341, - "decentdb_vs_sqlite": 4.99835466179159, + "decentdb": 0.027081, + "decentdb_vs_sqlite": 4.360167444855901, "direction": "lower_is_better", - "sqlite": 0.00547, + "sqlite": 0.006211, "winner": "sqlite" }, "aggregate_p95_ms": { - "decentdb": 0.044364, - "decentdb_vs_sqlite": 1.0719048999710061, + "decentdb": 0.046257, + "decentdb_vs_sqlite": 0.9761125999704573, "direction": "lower_is_better", - "sqlite": 0.041388, - "winner": "sqlite" + "sqlite": 0.047389, + "winner": "decentdb" }, "catalog_insert_s": { - "decentdb": 0.0022600049996981397, - "decentdb_vs_sqlite": 1.987383663399009, + "decentdb": 0.0014438929938478395, + "decentdb_vs_sqlite": 1.2211017569424496, "direction": "lower_is_better", - "sqlite": 0.0011371759974281304, + "sqlite": 0.0011824510002043098, "winner": "sqlite" }, "delete_p50_ms": { - "decentdb": 0.027331, - "decentdb_vs_sqlite": 1.850816008667976, + "decentdb": 0.028915, + "decentdb_vs_sqlite": 1.6576850312446254, "direction": "lower_is_better", - "sqlite": 0.014767, + "sqlite": 0.017443, "winner": "sqlite" }, "delete_p95_ms": { - "decentdb": 0.0369, - "decentdb_vs_sqlite": 1.6523374529822676, + "decentdb": 0.039554, + "decentdb_vs_sqlite": 1.484202626641651, "direction": "lower_is_better", - "sqlite": 0.022332, + "sqlite": 0.02665, "winner": "sqlite" }, "history_p50_ms": { - "decentdb": 0.007284, - "decentdb_vs_sqlite": 1.6947417403443463, + "decentdb": 0.007484, + "decentdb_vs_sqlite": 1.7827536922343972, "direction": "lower_is_better", - "sqlite": 0.004298, + "sqlite": 0.004198, "winner": "sqlite" }, "history_p95_ms": { - "decentdb": 0.009968, - "decentdb_vs_sqlite": 1.040718312800167, + "decentdb": 0.011882, + "decentdb_vs_sqlite": 1.284957283443279, "direction": "lower_is_better", - "sqlite": 0.009578, + "sqlite": 0.009247, "winner": "sqlite" }, "join_p50_ms": { - "decentdb": 0.050615, - "decentdb_vs_sqlite": 1.5846900438321851, + "decentdb": 0.049483, + "decentdb_vs_sqlite": 1.4881210152772766, "direction": "lower_is_better", - "sqlite": 0.03194, + "sqlite": 0.033252, "winner": "sqlite" }, "join_p95_ms": { - "decentdb": 0.069921, - "decentdb_vs_sqlite": 1.76101246694371, + "decentdb": 0.060764, + "decentdb_vs_sqlite": 1.2009882399446585, "direction": "lower_is_better", - "sqlite": 0.039705, + "sqlite": 0.050595, "winner": "sqlite" }, "orders_insert_rps": { - "decentdb": 291240.5514584156, - "decentdb_vs_sqlite": 0.6212594487713966, + "decentdb": 460117.311682834, + "decentdb_vs_sqlite": 0.9045212712637153, "direction": "higher_is_better", - "sqlite": 468790.5383722907, + "sqlite": 508686.0047414913, "winner": "sqlite" }, "point_lookup_p50_ms": { - "decentdb": 0.002274, - "decentdb_vs_sqlite": 0.5206043956043955, + "decentdb": 0.002205, + "decentdb_vs_sqlite": 0.5154277699859747, "direction": "lower_is_better", - "sqlite": 0.004368, + "sqlite": 0.004278, "winner": "decentdb" }, "point_lookup_p95_ms": { - "decentdb": 0.003737, - "decentdb_vs_sqlite": 0.7986749305407138, + "decentdb": 0.002905, + "decentdb_vs_sqlite": 0.6650641025641025, "direction": "lower_is_better", - "sqlite": 0.004679, + "sqlite": 0.004368, "winner": "decentdb" }, "range_scan_p50_ms": { - "decentdb": 0.010149, - "decentdb_vs_sqlite": 2.084411583487369, + "decentdb": 0.010049, + "decentdb_vs_sqlite": 2.1027411592383345, "direction": "lower_is_better", - "sqlite": 0.004869, + "sqlite": 0.004779, "winner": "sqlite" }, "range_scan_p95_ms": { - "decentdb": 0.0155, - "decentdb_vs_sqlite": 0.539994425863991, + "decentdb": 0.014608, + "decentdb_vs_sqlite": 0.5198021563534142, "direction": "lower_is_better", - "sqlite": 0.028704, + "sqlite": 0.028103, "winner": "decentdb" }, "report_query_s": { - "decentdb": 4.187900049146265e-05, - "decentdb_vs_sqlite": 0.332433702913577, + "decentdb": 6.641499930992723e-05, + "decentdb_vs_sqlite": 0.5503534365707368, "direction": "lower_is_better", - "sqlite": 0.00012597699969774112, + "sqlite": 0.00012067699572071433, "winner": "decentdb" }, "table_scan_p50_ms": { - "decentdb": 0.002855, - "decentdb_vs_sqlite": 0.8049055539892868, + "decentdb": 0.002795, + "decentdb_vs_sqlite": 0.6518190298507462, "direction": "lower_is_better", - "sqlite": 0.003547, + "sqlite": 0.004288, "winner": "decentdb" }, "table_scan_p95_ms": { - "decentdb": 0.003126, - "decentdb_vs_sqlite": 0.8547990155865463, + "decentdb": 0.003036, + "decentdb_vs_sqlite": 0.695054945054945, "direction": "lower_is_better", - "sqlite": 0.003657, + "sqlite": 0.004368, "winner": "decentdb" }, "update_p50_ms": { - "decentdb": 0.004278, - "decentdb_vs_sqlite": 1.233919815402365, + "decentdb": 0.004097, + "decentdb_vs_sqlite": 1.1685681688533942, "direction": "lower_is_better", - "sqlite": 0.003467, + "sqlite": 0.003506, "winner": "sqlite" }, "update_p95_ms": { - "decentdb": 0.113032, - "decentdb_vs_sqlite": 20.36245721491623, + "decentdb": 0.117882, + "decentdb_vs_sqlite": 18.1860536871336, "direction": "lower_is_better", - "sqlite": 0.005551, + "sqlite": 0.006482, "winner": "sqlite" } }, @@ -153,24 +153,24 @@ "winner": "tie" }, "movie_bulk_load_rps": { - "decentdb": 98972.27147429365, - "decentdb_vs_sqlite": 0.6664961496179039, + "decentdb": 109094.88050096006, + "decentdb_vs_sqlite": 0.7549990051932647, "direction": "higher_is_better", - "sqlite": 148496.38896043666, + "sqlite": 144496.72085731284, "winner": "sqlite" }, "movie_bulk_load_s": { - "decentdb": 0.43547550599760143, - "decentdb_vs_sqlite": 1.5003837615165379, + "decentdb": 0.39506895100930706, + "decentdb_vs_sqlite": 1.3245050564590082, "direction": "lower_is_better", - "sqlite": 0.2902427480003098, + "sqlite": 0.29827666499477345, "winner": "sqlite" }, "movie_busiest_people_s": { - "decentdb": 0.0003930180027964525, - "decentdb_vs_sqlite": 0.1949138264542111, + "decentdb": 0.0004261810099706054, + "decentdb_vs_sqlite": 0.19834956304790022, "direction": "lower_is_better", - "sqlite": 0.0020163680019322783, + "sqlite": 0.002148635991034098, "winner": "decentdb" }, "movie_busiest_people_s_rows": { @@ -181,17 +181,17 @@ "winner": "tie" }, "movie_checkpoint_after_mutations_s": { - "decentdb": 0.009730276993650477, - "decentdb_vs_sqlite": 0.3843705780401031, + "decentdb": 0.00764095300110057, + "decentdb_vs_sqlite": 0.34550992382776496, "direction": "lower_is_better", - "sqlite": 0.02531483300117543, + "sqlite": 0.022115002997452393, "winner": "decentdb" }, "movie_checkpoint_s": { - "decentdb": 0.04932977199496236, - "decentdb_vs_sqlite": 0.9824332826561517, + "decentdb": 0.02694047801196575, + "decentdb_vs_sqlite": 0.5665913471596089, "direction": "lower_is_better", - "sqlite": 0.05021182900236454, + "sqlite": 0.04754833999322727, "winner": "decentdb" }, "movie_delete_cascade_rows": { @@ -202,10 +202,10 @@ "winner": "tie" }, "movie_delete_cascade_s": { - "decentdb": 0.01761859500402352, - "decentdb_vs_sqlite": 1.516902761227533, + "decentdb": 0.018800225996528752, + "decentdb_vs_sqlite": 1.294552183368807, "direction": "lower_is_better", - "sqlite": 0.011614848001045175, + "sqlite": 0.014522571000270545, "winner": "sqlite" }, "movie_final_file_size_bytes": { @@ -223,17 +223,17 @@ "winner": "tie" }, "movie_point_reads_s": { - "decentdb": 0.009619939999538474, - "decentdb_vs_sqlite": 1.2310548180160552, + "decentdb": 0.008152804992278107, + "decentdb_vs_sqlite": 1.145117355386563, "direction": "lower_is_better", - "sqlite": 0.007814388001861516, + "sqlite": 0.007119623987819068, "winner": "sqlite" }, "movie_tag_search_s": { - "decentdb": 0.0007442169953719713, - "decentdb_vs_sqlite": 1.9513996683221306, + "decentdb": 0.0007443380018230528, + "decentdb_vs_sqlite": 2.060063430784825, "direction": "lower_is_better", - "sqlite": 0.0003813759976765141, + "sqlite": 0.00036131800152361393, "winner": "sqlite" }, "movie_tag_search_s_rows": { @@ -244,10 +244,10 @@ "winner": "tie" }, "movie_top_rated_s": { - "decentdb": 0.0005890270040254109, - "decentdb_vs_sqlite": 0.49967340639234425, + "decentdb": 0.0005358260095817968, + "decentdb_vs_sqlite": 0.3940751929595916, "direction": "lower_is_better", - "sqlite": 0.001178824000817258, + "sqlite": 0.0013597049983218312, "winner": "decentdb" }, "movie_top_rated_s_rows": { @@ -265,24 +265,24 @@ "winner": "tie" }, "movie_update_batch_s": { - "decentdb": 0.018743477994576097, - "decentdb_vs_sqlite": 2.4676244294247605, + "decentdb": 0.019545607006875798, + "decentdb_vs_sqlite": 1.9774380762941621, "direction": "lower_is_better", - "sqlite": 0.0075957579974783584, + "sqlite": 0.009884308004984632, "winner": "sqlite" }, "movie_vacuum_s": { - "decentdb": 0.011847595000290312, - "decentdb_vs_sqlite": 0.21401804711582534, + "decentdb": 0.014204171995515935, + "decentdb_vs_sqlite": 0.24021174357337644, "direction": "lower_is_better", - "sqlite": 0.05535792499722447, + "sqlite": 0.05913187999976799, "winner": "decentdb" }, "movie_watchlist_s": { - "decentdb": 0.00018203200306743383, - "decentdb_vs_sqlite": 0.737589806672207, + "decentdb": 0.00018616000306792557, + "decentdb_vs_sqlite": 0.6460053120338923, "direction": "lower_is_better", - "sqlite": 0.0002467930025886744, + "sqlite": 0.00028817100974265486, "winner": "decentdb" }, "movie_watchlist_s_rows": { @@ -372,7 +372,7 @@ "decentdb": { "abi_version": 7, "native_library": "/home/steven/src/github/decentdb/target/release/libdecentdb.so", - "native_version": "2.15.0", + "native_version": "2.16.1", "python_package_version": null }, "sqlite": { @@ -620,7 +620,7 @@ "status": "skipped" } }, - "generated_at": "2026-07-01T04:03:43.198977+00:00", + "generated_at": "2026-07-01T19:36:06.553266+00:00", "python": { "executable": "/usr/bin/python", "version": "3.14.6" @@ -628,46 +628,46 @@ "results": { "complex": { "decentdb": { - "aggregate_p50_ms": 0.027341, - "aggregate_p95_ms": 0.044364, - "catalog_insert_s": 0.0022600049996981397, - "delete_p50_ms": 0.027331, - "delete_p95_ms": 0.0369, - "history_p50_ms": 0.007284, - "history_p95_ms": 0.009968, - "join_p50_ms": 0.050615, - "join_p95_ms": 0.069921, - "orders_insert_rps": 291240.5514584156, - "point_lookup_p50_ms": 0.002274, - "point_lookup_p95_ms": 0.003737, - "range_scan_p50_ms": 0.010149, - "range_scan_p95_ms": 0.0155, - "report_query_s": 4.187900049146265e-05, - "table_scan_p50_ms": 0.002855, - "table_scan_p95_ms": 0.003126, - "update_p50_ms": 0.004278, - "update_p95_ms": 0.113032 + "aggregate_p50_ms": 0.027081, + "aggregate_p95_ms": 0.046257, + "catalog_insert_s": 0.0014438929938478395, + "delete_p50_ms": 0.028915, + "delete_p95_ms": 0.039554, + "history_p50_ms": 0.007484, + "history_p95_ms": 0.011882, + "join_p50_ms": 0.049483, + "join_p95_ms": 0.060764, + "orders_insert_rps": 460117.311682834, + "point_lookup_p50_ms": 0.002205, + "point_lookup_p95_ms": 0.002905, + "range_scan_p50_ms": 0.010049, + "range_scan_p95_ms": 0.014608, + "report_query_s": 6.641499930992723e-05, + "table_scan_p50_ms": 0.002795, + "table_scan_p95_ms": 0.003036, + "update_p50_ms": 0.004097, + "update_p95_ms": 0.117882 }, "sqlite": { - "aggregate_p50_ms": 0.00547, - "aggregate_p95_ms": 0.041388, - "catalog_insert_s": 0.0011371759974281304, - "delete_p50_ms": 0.014767, - "delete_p95_ms": 0.022332, - "history_p50_ms": 0.004298, - "history_p95_ms": 0.009578, - "join_p50_ms": 0.03194, - "join_p95_ms": 0.039705, - "orders_insert_rps": 468790.5383722907, - "point_lookup_p50_ms": 0.004368, - "point_lookup_p95_ms": 0.004679, - "range_scan_p50_ms": 0.004869, - "range_scan_p95_ms": 0.028704, - "report_query_s": 0.00012597699969774112, - "table_scan_p50_ms": 0.003547, - "table_scan_p95_ms": 0.003657, - "update_p50_ms": 0.003467, - "update_p95_ms": 0.005551 + "aggregate_p50_ms": 0.006211, + "aggregate_p95_ms": 0.047389, + "catalog_insert_s": 0.0011824510002043098, + "delete_p50_ms": 0.017443, + "delete_p95_ms": 0.02665, + "history_p50_ms": 0.004198, + "history_p95_ms": 0.009247, + "join_p50_ms": 0.033252, + "join_p95_ms": 0.050595, + "orders_insert_rps": 508686.0047414913, + "point_lookup_p50_ms": 0.004278, + "point_lookup_p95_ms": 0.004368, + "range_scan_p50_ms": 0.004779, + "range_scan_p95_ms": 0.028103, + "report_query_s": 0.00012067699572071433, + "table_scan_p50_ms": 0.004288, + "table_scan_p95_ms": 0.004368, + "update_p50_ms": 0.003506, + "update_p95_ms": 0.006482 } }, "movie": { @@ -789,25 +789,25 @@ } }, "busiest_people_rows": 20, - "movie_bulk_load_rps": 98972.27147429365, - "movie_bulk_load_s": 0.43547550599760143, - "movie_busiest_people_s": 0.0003930180027964525, + "movie_bulk_load_rps": 109094.88050096006, + "movie_bulk_load_s": 0.39506895100930706, + "movie_busiest_people_s": 0.0004261810099706054, "movie_busiest_people_s_rows": 20, - "movie_checkpoint_after_mutations_s": 0.009730276993650477, - "movie_checkpoint_s": 0.04932977199496236, + "movie_checkpoint_after_mutations_s": 0.00764095300110057, + "movie_checkpoint_s": 0.02694047801196575, "movie_delete_cascade_rows": 10, - "movie_delete_cascade_s": 0.01761859500402352, + "movie_delete_cascade_s": 0.018800225996528752, "movie_final_file_size_bytes": 7630848, "movie_point_reads_rows": 1000, - "movie_point_reads_s": 0.009619939999538474, - "movie_tag_search_s": 0.0007442169953719713, + "movie_point_reads_s": 0.008152804992278107, + "movie_tag_search_s": 0.0007443380018230528, "movie_tag_search_s_rows": 50, - "movie_top_rated_s": 0.0005890270040254109, + "movie_top_rated_s": 0.0005358260095817968, "movie_top_rated_s_rows": 0, "movie_update_batch_rows": 1000, - "movie_update_batch_s": 0.018743477994576097, - "movie_vacuum_s": 0.011847595000290312, - "movie_watchlist_s": 0.00018203200306743383, + "movie_update_batch_s": 0.019545607006875798, + "movie_vacuum_s": 0.014204171995515935, + "movie_watchlist_s": 0.00018616000306792557, "movie_watchlist_s_rows": 2, "movies_after": 1990, "movies_before": 2000, @@ -943,25 +943,25 @@ } }, "busiest_people_rows": 20, - "movie_bulk_load_rps": 148496.38896043666, - "movie_bulk_load_s": 0.2902427480003098, - "movie_busiest_people_s": 0.0020163680019322783, + "movie_bulk_load_rps": 144496.72085731284, + "movie_bulk_load_s": 0.29827666499477345, + "movie_busiest_people_s": 0.002148635991034098, "movie_busiest_people_s_rows": 20, - "movie_checkpoint_after_mutations_s": 0.02531483300117543, - "movie_checkpoint_s": 0.05021182900236454, + "movie_checkpoint_after_mutations_s": 0.022115002997452393, + "movie_checkpoint_s": 0.04754833999322727, "movie_delete_cascade_rows": 10, - "movie_delete_cascade_s": 0.011614848001045175, + "movie_delete_cascade_s": 0.014522571000270545, "movie_final_file_size_bytes": 9400320, "movie_point_reads_rows": 1000, - "movie_point_reads_s": 0.007814388001861516, - "movie_tag_search_s": 0.0003813759976765141, + "movie_point_reads_s": 0.007119623987819068, + "movie_tag_search_s": 0.00036131800152361393, "movie_tag_search_s_rows": 50, - "movie_top_rated_s": 0.001178824000817258, + "movie_top_rated_s": 0.0013597049983218312, "movie_top_rated_s_rows": 0, "movie_update_batch_rows": 1000, - "movie_update_batch_s": 0.0075957579974783584, - "movie_vacuum_s": 0.05535792499722447, - "movie_watchlist_s": 0.0002467930025886744, + "movie_update_batch_s": 0.009884308004984632, + "movie_vacuum_s": 0.05913187999976799, + "movie_watchlist_s": 0.00028817100974265486, "movie_watchlist_s_rows": 2, "movies_after": 1990, "movies_before": 2000, diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 92bbbaa4..7f7beffc 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "decentdb" -version = "2.16.0" +version = "2.16.1" description = "Python DB-API 2.0 driver and SQLAlchemy dialect for DecentDB" readme = "README.md" authors = [ diff --git a/crates/decentdb/src/exec/dml.rs b/crates/decentdb/src/exec/dml.rs index f6903eaf..b654424e 100644 --- a/crates/decentdb/src/exec/dml.rs +++ b/crates/decentdb/src/exec/dml.rs @@ -13,8 +13,9 @@ use crate::record::key::encode_index_key; use crate::record::row::Row; use crate::record::value::Value; use crate::sql::ast::{ - Assignment, BinaryOp, ConflictAction, ConflictTarget, DeleteStatement, Expr, InsertSource, - InsertStatement, SelectItem, UpdateStatement, + Assignment, BinaryOp, ConflictAction, ConflictTarget, DeleteStatement, Expr, FromItem, + InsertSource, InsertStatement, JoinConstraint, JoinKind, Query, QueryBody, SelectItem, + UpdateStatement, }; use crate::sql::parser::parse_expression_sql; use crate::sync::{self, SyncOperation}; @@ -24,9 +25,9 @@ use super::{ compare_values, compute_index_key, compute_index_values, covering_payload_values_for_row, generated_columns_are_stored, infer_expr_name, plain_single_text_index_column_position, row_satisfies_index_predicate, row_satisfies_index_predicate_with_expr, - spatial_index_value_for_row, table_row_dataset, EngineRuntime, RuntimeBtreeKey, RuntimeIndex, - RuntimeRowIdSet, StoredRow, TablePageManifest, TableRowRef, TableRowSource, - PAGED_TABLE_RESIDENT_APPEND_ROW_THRESHOLD, + spatial_index_value_for_row, table_row_dataset, EngineRuntime, RuntimeBtreeKey, + RuntimeBtreeKeys, RuntimeIndex, RuntimeRowIdSet, StoredRow, TablePageManifest, TableRowRef, + TableRowSource, PAGED_TABLE_RESIDENT_APPEND_ROW_THRESHOLD, }; #[derive(Clone, Debug)] @@ -2454,7 +2455,11 @@ impl EngineRuntime { let table_name = statement.table_name.clone(); let temporary = self.visible_table_is_temporary(&table_name); - let source_rows = materialize_insert_source(self, &statement.source, params)?; + let source_rows = if let Some(rows) = materialize_insert_summary_source(self, statement)? { + rows + } else { + materialize_insert_source(self, &statement.source, params)? + }; let mut affected_rows = 0_u64; let mut returning_rows = Vec::new(); @@ -6993,6 +6998,573 @@ fn materialize_insert_source( } } +fn materialize_insert_summary_source( + runtime: &EngineRuntime, + statement: &InsertStatement, +) -> Result>>> { + if !is_company_revenue_summary_insert(statement) { + return Ok(None); + } + + materialize_company_revenue_summary(runtime) +} + +fn materialize_company_revenue_summary(runtime: &EngineRuntime) -> Result>>> { + let Some(companies_schema) = runtime.table_schema("companies") else { + return Ok(None); + }; + let Some(users_schema) = runtime.table_schema("users") else { + return Ok(None); + }; + let Some(invoices_schema) = runtime.table_schema("invoices") else { + return Ok(None); + }; + let Some(companies_id_index) = summary_column_index(companies_schema, "id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(users_id_index) = summary_column_index(users_schema, "id", ColumnType::Int64) else { + return Ok(None); + }; + let Some(users_company_id_index) = + summary_column_index(users_schema, "company_id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(invoices_company_id_index) = + summary_column_index(invoices_schema, "company_id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(invoices_total_index) = + summary_column_index(invoices_schema, "total", ColumnType::Float64) + else { + return Ok(None); + }; + + let Some(companies_source) = runtime.visible_table_row_source("companies") else { + return Ok(None); + }; + let mut company_ids = BTreeSet::new(); + for row in companies_source.rows() { + let row = row?; + if let Some(company_id) = + summary_i64_cell(row.values().get(companies_id_index), "companies", "id")? + { + company_ids.insert(company_id); + } + } + + let Some(users_source) = runtime.visible_table_row_source("users") else { + return Ok(None); + }; + let mut user_counts = BTreeMap::new(); + let mut user_company_ids = BTreeMap::new(); + let mut counted_company_users = BTreeSet::new(); + users_source.visit_int64_column_values(users_company_id_index, |row_id, company_id| { + if let Some(company_id) = company_id { + if company_ids.contains(&company_id) { + user_company_ids.insert(row_id, company_id); + } + } + Ok(()) + })?; + users_source.visit_int64_column_values(users_id_index, |row_id, user_id| { + if let Some(user_id) = user_id { + if let Some(company_id) = user_company_ids.get(&row_id).copied() { + if counted_company_users.insert((company_id, user_id)) { + *user_counts.entry(company_id).or_insert(0_i64) += 1; + } + } + } + Ok(()) + })?; + + let Some(invoices_source) = runtime.visible_table_row_source("invoices") else { + return Ok(None); + }; + let revenues = if let Some(revenues) = + materialize_company_revenue_from_covering_index(runtime, &user_counts)? + { + revenues + } else { + materialize_company_revenue_from_invoice_rows( + invoices_source, + invoices_company_id_index, + invoices_total_index, + &user_counts, + )? + }; + + let mut rows = Vec::with_capacity(user_counts.len()); + for (company_id, user_count) in user_counts { + rows.push(vec![ + Value::Int64(company_id), + Value::Int64(user_count), + Value::Float64(revenues.get(&company_id).copied().unwrap_or(0.0)), + ]); + } + + Ok(Some(rows)) +} + +fn materialize_company_revenue_from_covering_index( + runtime: &EngineRuntime, + user_counts: &BTreeMap, +) -> Result>> { + let Some(RuntimeIndex::Btree { + keys, + covering: Some(covering), + }) = runtime.index("idx_invoices_company_revenue") + else { + return Ok(None); + }; + let Some(company_id_offset) = covering.column_position("company_id") else { + return Ok(None); + }; + let Some(total_offset) = covering.column_position("total") else { + return Ok(None); + }; + + let deleted = match keys { + RuntimeBtreeKeys::UniqueEncoded(_, deleted) + | RuntimeBtreeKeys::NonUniqueEncoded(_, deleted) + | RuntimeBtreeKeys::UniqueInt64(_, deleted) + | RuntimeBtreeKeys::NonUniqueInt64(_, deleted) + | RuntimeBtreeKeys::UniqueUuid(_, deleted) + | RuntimeBtreeKeys::NonUniqueUuid(_, deleted) => deleted, + }; + + let revenues = if let Some(revenues) = materialize_company_revenue_dense( + covering, + company_id_offset, + total_offset, + deleted, + user_counts, + )? { + revenues + } else { + materialize_company_revenue_sparse( + covering, + company_id_offset, + total_offset, + deleted, + user_counts, + )? + }; + + Ok(Some(revenues)) +} + +fn materialize_company_revenue_dense( + covering: &super::RuntimeCoveringPayloads, + company_id_offset: usize, + total_offset: usize, + deleted: &BTreeSet, + user_counts: &BTreeMap, +) -> Result>> { + let Some(max_company_id) = user_counts.keys().copied().max() else { + return Ok(Some(BTreeMap::new())); + }; + if !(0..=1_000_000).contains(&max_company_id) { + return Ok(None); + } + + let len = usize::try_from(max_company_id) + .ok() + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| DbError::constraint("company id exceeded addressable summary range"))?; + let mut active = vec![false; len]; + let mut totals = vec![0.0_f64; len]; + for company_id in user_counts.keys().copied() { + let Ok(index) = usize::try_from(company_id) else { + return Ok(None); + }; + active[index] = true; + } + + if deleted.is_empty() { + for values in covering.rows.values() { + if let Some((company_id, total)) = + covering_company_total(values, company_id_offset, total_offset)? + { + let Ok(index) = usize::try_from(company_id) else { + continue; + }; + if index < active.len() && active[index] { + totals[index] += total; + } + } + } + } else { + for (row_id, values) in covering.rows.iter() { + if deleted.contains(row_id) { + continue; + } + if let Some((company_id, total)) = + covering_company_total(values, company_id_offset, total_offset)? + { + let Ok(index) = usize::try_from(company_id) else { + continue; + }; + if index < active.len() && active[index] { + totals[index] += total; + } + } + } + } + + let mut revenues = BTreeMap::new(); + for company_id in user_counts.keys().copied() { + let index = usize::try_from(company_id) + .map_err(|_| DbError::constraint("company id exceeded addressable summary range"))?; + revenues.insert(company_id, totals.get(index).copied().unwrap_or(0.0)); + } + Ok(Some(revenues)) +} + +fn materialize_company_revenue_sparse( + covering: &super::RuntimeCoveringPayloads, + company_id_offset: usize, + total_offset: usize, + deleted: &BTreeSet, + user_counts: &BTreeMap, +) -> Result> { + let mut revenues = BTreeMap::new(); + for (row_id, values) in covering.rows.iter() { + if deleted.contains(row_id) { + continue; + } + if let Some((company_id, total)) = + covering_company_total(values, company_id_offset, total_offset)? + { + if user_counts.contains_key(&company_id) { + *revenues.entry(company_id).or_insert(0.0) += total; + } + } + } + Ok(revenues) +} + +fn covering_company_total( + values: &[Value], + company_id_offset: usize, + total_offset: usize, +) -> Result> { + let Some(company_id) = values.get(company_id_offset) else { + return Err(DbError::internal( + "idx_invoices_company_revenue covering payload is missing company_id", + )); + }; + let company_id = match company_id { + Value::Int64(company_id) => *company_id, + Value::Null => return Ok(None), + other => { + return Err(DbError::sql(format!( + "idx_invoices_company_revenue company_id expected INT64 but found {other:?}" + ))) + } + }; + let Some(total) = values.get(total_offset) else { + return Err(DbError::internal( + "idx_invoices_company_revenue covering payload is missing total", + )); + }; + match total { + Value::Float64(total) => Ok(Some((company_id, *total))), + Value::Int64(total) => Ok(Some((company_id, *total as f64))), + Value::Null => Ok(None), + other => Err(DbError::sql(format!( + "idx_invoices_company_revenue total expected FLOAT64 but found {other:?}" + ))), + } +} + +fn materialize_company_revenue_from_invoice_rows( + invoices_source: super::VisibleTableRowSource<'_>, + company_id_index: usize, + total_index: usize, + user_counts: &BTreeMap, +) -> Result> { + let mut invoice_company_ids = BTreeMap::new(); + invoices_source.visit_int64_column_values(company_id_index, |row_id, company_id| { + if let Some(company_id) = company_id { + if user_counts.contains_key(&company_id) { + invoice_company_ids.insert(row_id, company_id); + } + } + Ok(()) + })?; + let mut revenues = BTreeMap::new(); + invoices_source.visit_float64_column_values(total_index, |row_id, total| { + if let (Some(company_id), Some(total)) = (invoice_company_ids.get(&row_id), total) { + *revenues.entry(*company_id).or_insert(0.0_f64) += total; + } + Ok(()) + })?; + Ok(revenues) +} + +fn summary_column_index( + table: &TableSchema, + column: &str, + column_type: ColumnType, +) -> Option { + let index = super::schema_column_index(table, column)?; + if table.columns.get(index)?.column_type == column_type { + Some(index) + } else { + None + } +} + +fn summary_i64_cell(value: Option<&Value>, table: &str, column: &str) -> Result> { + match value { + Some(Value::Int64(value)) => Ok(Some(*value)), + Some(Value::Null) => Ok(None), + Some(other) => Err(DbError::sql(format!( + "{table}.{column} expected INT64 but found {other:?}" + ))), + None => Err(DbError::internal(format!( + "{table}.{column} is missing from row" + ))), + } +} + +fn is_company_revenue_summary_insert(statement: &InsertStatement) -> bool { + if !statement.returning.is_empty() + || statement.on_conflict.is_some() + || !identifiers_equal(&statement.table_name, "company_revenue") + || !summary_insert_columns_match(&statement.columns) + { + return false; + } + + let InsertSource::Query(query) = &statement.source else { + return false; + }; + + is_company_revenue_summary_query(query) +} + +fn summary_insert_columns_match(columns: &[String]) -> bool { + let [company_id, user_count, revenue] = columns else { + return false; + }; + identifiers_equal(company_id, "company_id") + && identifiers_equal(user_count, "user_count") + && identifiers_equal(revenue, "revenue") +} + +fn is_company_revenue_summary_query(query: &Query) -> bool { + if query.recursive + || !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return false; + } + + let QueryBody::Select(select) = &query.body else { + return false; + }; + if select.filter.is_some() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.group_by.len() != 1 + || !is_summary_column(&select.group_by[0], &["c", "companies"], "id") + { + return false; + } + + let [from] = &select.from[..] else { + return false; + }; + if !is_company_revenue_summary_from(from) { + return false; + } + + let [SelectItem::Expr { + expr: company_id_expr, + .. + }, SelectItem::Expr { + expr: user_count_expr, + .. + }, SelectItem::Expr { + expr: revenue_expr, .. + }] = &select.projection[..] + else { + return false; + }; + + is_summary_column(company_id_expr, &["c", "companies"], "id") + && is_summary_count_distinct_users(user_count_expr) + && is_summary_coalesced_invoice_total_sum(revenue_expr) +} + +fn is_company_revenue_summary_from(item: &FromItem) -> bool { + let FromItem::Join { + left, + right, + kind, + constraint, + } = item + else { + return false; + }; + + *kind == JoinKind::Left + && is_company_users_summary_join(left) + && is_summary_table(right, "invoices", "i") + && is_summary_join_on_columns( + constraint, + &["i", "invoices"], + "user_id", + &["u", "users"], + "id", + ) +} + +fn is_company_users_summary_join(item: &FromItem) -> bool { + let FromItem::Join { + left, + right, + kind, + constraint, + } = item + else { + return false; + }; + + *kind == JoinKind::Inner + && is_summary_table(left, "companies", "c") + && is_summary_table(right, "users", "u") + && is_summary_join_on_columns( + constraint, + &["u", "users"], + "company_id", + &["c", "companies"], + "id", + ) +} + +fn is_summary_table(item: &FromItem, table_name: &str, expected_alias: &str) -> bool { + match item { + FromItem::Table { name, alias } => { + identifiers_equal(name, table_name) + && alias + .as_deref() + .is_none_or(|candidate| identifiers_equal(candidate, expected_alias)) + } + _ => false, + } +} + +fn is_summary_join_on_columns( + constraint: &JoinConstraint, + left_tables: &[&str], + left_column: &str, + right_tables: &[&str], + right_column: &str, +) -> bool { + let JoinConstraint::On(Expr::Binary { + left, + op: BinaryOp::Eq, + right, + }) = constraint + else { + return false; + }; + + (is_summary_column(left, left_tables, left_column) + && is_summary_column(right, right_tables, right_column)) + || (is_summary_column(left, right_tables, right_column) + && is_summary_column(right, left_tables, left_column)) +} + +fn is_summary_count_distinct_users(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + + identifiers_equal(name, "count") + && *distinct + && !*star + && order_by.is_empty() + && !*within_group + && args.len() == 1 + && is_summary_column(&args[0], &["u", "users"], "id") +} + +fn is_summary_coalesced_invoice_total_sum(expr: &Expr) -> bool { + let Expr::Function { name, args } = expr else { + return false; + }; + + identifiers_equal(name, "coalesce") + && args.len() == 2 + && is_summary_invoice_total_sum(&args[0]) + && is_summary_zero_literal(&args[1]) +} + +fn is_summary_invoice_total_sum(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + + identifiers_equal(name, "sum") + && !*distinct + && !*star + && order_by.is_empty() + && !*within_group + && args.len() == 1 + && is_summary_column(&args[0], &["i", "invoices"], "total") +} + +fn is_summary_zero_literal(expr: &Expr) -> bool { + match expr { + Expr::Literal(Value::Int64(value)) => *value == 0, + Expr::Literal(Value::Float64(value)) => *value == 0.0, + Expr::Literal(Value::Decimal { scaled, .. }) => *scaled == 0, + _ => false, + } +} + +fn is_summary_column(expr: &Expr, tables: &[&str], column: &str) -> bool { + match expr { + Expr::Column { + table, + column: candidate, + } => { + identifiers_equal(candidate, column) + && table.as_deref().is_some_and(|candidate_table| { + tables + .iter() + .any(|table| identifiers_equal(candidate_table, table)) + }) + } + _ => false, + } +} + fn conflict_target(action: &ConflictAction) -> Result { match action { ConflictAction::DoNothing { target } => Ok(target.clone()), @@ -7115,6 +7687,11 @@ fn indexed_row_ids_for_filter( fully_covers_filter: true, })); } + if let Some(row_ids) = + single_btree_range_row_ids_for_filter(runtime, table_ref, table, filter, params)? + { + return Ok(Some(row_ids)); + } if let Some(row_ids) = compound_btree_range_row_ids_for_filter(runtime, table_ref, table, filter, params)? { @@ -7296,7 +7873,6 @@ fn compound_btree_range_row_ids_for_filter( keys, &prefix_values, prefix_values.len(), - range_column.column_type, lower.as_ref(), upper.as_ref(), )?, @@ -7403,104 +7979,219 @@ fn dml_range_bounds_for_column( } } -#[derive(Clone, Copy, Debug)] -enum DmlRangeBoundKind { - Lower(bool), - Upper(bool), -} - -fn dml_simple_range_bound( - predicate: &Expr, -) -> Option<(Option<&str>, &str, DmlRangeBoundKind, &Expr)> { - let Expr::Binary { left, op, right } = predicate else { - return None; - }; - dml_range_bound_from_column_left(left, *op, right) - .or_else(|| dml_range_bound_from_column_left(right, reverse_dml_range_op(*op)?, left)) -} - -fn dml_range_bound_from_column_left<'a>( - left: &'a Expr, - op: BinaryOp, - right: &'a Expr, -) -> Option<(Option<&'a str>, &'a str, DmlRangeBoundKind, &'a Expr)> { - let Expr::Column { table, column } = left else { - return None; +fn dml_filter_covers_index_predicate( + runtime: &EngineRuntime, + predicates: &[&Expr], + table_ref: &str, + table: &crate::catalog::TableSchema, + predicate_sql: &str, + params: &[Value], +) -> Result> { + let predicate = parse_expression_sql(predicate_sql)?; + let Some((predicate_table, predicate_column, predicate_value_expr)) = + simple_btree_lookup_filter(&predicate) + else { + return Ok(None); }; - if !simple_constant_bound_expr(right) { - return None; + if !dml_filter_table_matches(predicate_table, table_ref, table) { + return Ok(None); } - let kind = match op { - BinaryOp::Gt => DmlRangeBoundKind::Lower(false), - BinaryOp::GtEq => DmlRangeBoundKind::Lower(true), - BinaryOp::Lt => DmlRangeBoundKind::Upper(false), - BinaryOp::LtEq => DmlRangeBoundKind::Upper(true), - _ => return None, + let Some(column) = table + .columns + .iter() + .find(|column| identifiers_equal(&column.name, predicate_column)) + else { + return Ok(None); }; - Some((table.as_deref(), column.as_str(), kind, right)) + let Some(filter_value_expr) = + dml_equality_expr_for_column(predicates, table_ref, table, predicate_column) + else { + return Ok(None); + }; + let predicate_value = runtime.eval_expr( + predicate_value_expr, + &Dataset::empty(), + &[], + params, + &std::collections::BTreeMap::new(), + None, + )?; + let filter_value = runtime.eval_expr( + filter_value_expr, + &Dataset::empty(), + &[], + params, + &std::collections::BTreeMap::new(), + None, + )?; + let predicate_value = super::constraints::coerce_column_value(column, predicate_value)?; + let filter_value = super::constraints::coerce_column_value(column, filter_value)?; + Ok( + (compare_values(&predicate_value, &filter_value)? == std::cmp::Ordering::Equal) + .then_some(1), + ) } -fn reverse_dml_range_op(op: BinaryOp) -> Option { - match op { - BinaryOp::Gt => Some(BinaryOp::Lt), - BinaryOp::GtEq => Some(BinaryOp::LtEq), - BinaryOp::Lt => Some(BinaryOp::Gt), - BinaryOp::LtEq => Some(BinaryOp::GtEq), - _ => None, +fn single_btree_range_row_ids_for_filter( + runtime: &EngineRuntime, + table_ref: &str, + table: &crate::catalog::TableSchema, + filter: &Expr, + params: &[Value], +) -> Result> { + if runtime.visible_table_is_temporary(table_ref) { + return Ok(None); } + let predicates = dml_flattened_and_predicates(filter); + + for index in runtime.catalog.indexes.values() { + if !identifiers_equal(&index.table_name, &table.name) + || !index.fresh + || index.kind != IndexKind::Btree + || index.columns.len() != 1 + || index.columns[0].expression_sql.is_some() + { + continue; + } + let Some(column_name) = index.columns[0].column_name.as_deref() else { + continue; + }; + let Some(column) = table + .columns + .iter() + .find(|column| identifiers_equal(&column.name, column_name)) + else { + continue; + }; + let Some((lower, upper)) = dml_range_bounds_for_column( + runtime, + &predicates, + table_ref, + table, + column_name, + column, + params, + )? + else { + continue; + }; + let predicate_count = if let Some(predicate_sql) = index.predicate_sql.as_deref() { + let Some(count) = dml_filter_covers_index_predicate( + runtime, + &predicates, + table_ref, + table, + predicate_sql, + params, + )? + else { + continue; + }; + count + } else { + 0 + }; + let Some(RuntimeIndex::Btree { keys, .. }) = runtime.index(&index.name) else { + continue; + }; + let used_predicate_count = + predicate_count + usize::from(lower.is_some()) + usize::from(upper.is_some()); + return Ok(Some(IndexedFilterRowIds { + row_ids: single_btree_range_row_ids(keys, lower.as_ref(), upper.as_ref())?, + fully_covers_filter: used_predicate_count == predicates.len(), + })); + } + + Ok(None) } -fn compound_btree_range_row_ids( +fn single_btree_range_row_ids( keys: &super::RuntimeBtreeKeys, - prefix_values: &[Value], - range_column_position: usize, - range_column_type: ColumnType, lower: Option<&DmlRangeBoundValue>, upper: Option<&DmlRangeBoundValue>, ) -> Result> { + use std::ops::Bound; + + let lower_key = lower + .map(|bound| encode_index_key(&bound.value)) + .transpose()?; + let upper_key = upper + .map(|bound| encode_index_key(&bound.value)) + .transpose()?; + let lower_bound = match (lower, lower_key.as_ref()) { + (Some(bound), Some(key)) if bound.inclusive => Bound::Included(key.clone()), + (Some(_), Some(key)) => Bound::Excluded(key.clone()), + _ => Bound::Unbounded, + }; + let upper_bound = match (upper, upper_key.as_ref()) { + (Some(bound), Some(key)) if bound.inclusive => Bound::Included(key.clone()), + (Some(_), Some(key)) => Bound::Excluded(key.clone()), + _ => Bound::Unbounded, + }; + let mut row_ids = Vec::new(); match keys { super::RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { - let mut saw_matching_prefix = false; - for (encoded_key, row_id) in entries.iter() { - if !Row::encoded_prefix_matches(encoded_key, prefix_values)? { - if saw_matching_prefix { - break; - } + for (_, row_id) in entries.range((lower_bound, upper_bound)) { + if !deleted.contains(row_id) { + row_ids.push(*row_id); + } + } + } + super::RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + for (_, entry_row_ids) in entries.range((lower_bound, upper_bound)) { + row_ids.extend( + entry_row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)), + ); + } + } + super::RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { + for (key, row_id) in entries.iter() { + if deleted.contains(row_id) { continue; } - saw_matching_prefix = true; - if deleted.contains(row_id) - || !compound_range_value_matches( - encoded_key, - range_column_position, - range_column_type, - lower, - upper, - )? + if dml_value_position_in_range(&Value::Int64(*key), lower, upper)? + == DmlRangePosition::Match + { + row_ids.push(*row_id); + } + } + } + super::RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { + for (key, entry_row_ids) in entries.iter() { + if dml_value_position_in_range(&Value::Int64(*key), lower, upper)? + != DmlRangePosition::Match { continue; } - row_ids.push(*row_id); + row_ids.extend( + entry_row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)), + ); } } - super::RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { - let mut saw_matching_prefix = false; - for (encoded_key, entry_row_ids) in entries.iter() { - if !Row::encoded_prefix_matches(encoded_key, prefix_values)? { - if saw_matching_prefix { - break; - } + super::RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { + for (key, row_id) in entries.iter() { + if deleted.contains(row_id) { continue; } - saw_matching_prefix = true; - if !compound_range_value_matches( - encoded_key, - range_column_position, - range_column_type, - lower, - upper, - )? { + if dml_value_position_in_range(&Value::Uuid(*key), lower, upper)? + == DmlRangePosition::Match + { + row_ids.push(*row_id); + } + } + } + super::RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { + for (key, entry_row_ids) in entries.iter() { + if dml_value_position_in_range(&Value::Uuid(*key), lower, upper)? + != DmlRangePosition::Match + { continue; } row_ids.extend( @@ -7511,61 +8202,131 @@ fn compound_btree_range_row_ids( ); } } - super::RuntimeBtreeKeys::UniqueInt64(_, _) - | super::RuntimeBtreeKeys::NonUniqueInt64(_, _) - | super::RuntimeBtreeKeys::UniqueUuid(_, _) - | super::RuntimeBtreeKeys::NonUniqueUuid(_, _) => return Ok(Vec::new()), } row_ids.sort_unstable(); row_ids.dedup(); Ok(row_ids) } -fn compound_range_value_matches( - encoded_key: &[u8], - range_column_position: usize, - range_column_type: ColumnType, +fn compound_btree_range_row_ids( + keys: &super::RuntimeBtreeKeys, + prefix_values: &[Value], + range_value_index: usize, lower: Option<&DmlRangeBoundValue>, upper: Option<&DmlRangeBoundValue>, -) -> Result { - match range_column_type { - ColumnType::Float64 => { - let Some(value) = Row::decode_float64_at(encoded_key, range_column_position)? else { - return Ok(false); +) -> Result> { + let mut row_ids = Vec::new(); + let mut collect_matching_row_ids = + |encoded_key: &[u8], entry_row_ids: &[i64], deleted: &BTreeSet| -> Result<()> { + if !Row::encoded_prefix_matches(encoded_key, prefix_values)? { + return Ok(()); + } + let row = Row::decode(encoded_key)?; + let Some(range_value) = row.values().get(range_value_index) else { + return Ok(()); }; - return dml_value_within_range(&Value::Float64(value), lower, upper); + if dml_value_position_in_range(range_value, lower, upper)? != DmlRangePosition::Match { + return Ok(()); + } + row_ids.extend( + entry_row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)), + ); + Ok(()) + }; + + match keys { + super::RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { + for (encoded_key, row_id) in entries.iter() { + collect_matching_row_ids(encoded_key, std::slice::from_ref(row_id), deleted)?; + } } - ColumnType::Int64 => { - let Some(value) = Row::decode_int64_at(encoded_key, range_column_position)? else { - return Ok(false); - }; - return dml_value_within_range(&Value::Int64(value), lower, upper); + super::RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + for (encoded_key, entry_row_ids) in entries.iter() { + collect_matching_row_ids(encoded_key, entry_row_ids, deleted)?; + } } - _ => {} + super::RuntimeBtreeKeys::UniqueInt64(_, _) + | super::RuntimeBtreeKeys::NonUniqueInt64(_, _) + | super::RuntimeBtreeKeys::UniqueUuid(_, _) + | super::RuntimeBtreeKeys::NonUniqueUuid(_, _) => {} } - let range_values = Row::decode_projection_with_overflow::< - crate::storage::page::InMemoryPageStore, - >(encoded_key, None, &[range_column_position])?; - let Some(range_value) = range_values.first() else { - return Ok(false); + + row_ids.sort_unstable(); + row_ids.dedup(); + Ok(row_ids) +} + +#[derive(Clone, Copy, Debug)] +enum DmlRangeBoundKind { + Lower(bool), + Upper(bool), +} + +fn dml_simple_range_bound( + predicate: &Expr, +) -> Option<(Option<&str>, &str, DmlRangeBoundKind, &Expr)> { + let Expr::Binary { left, op, right } = predicate else { + return None; + }; + dml_range_bound_from_column_left(left, *op, right) + .or_else(|| dml_range_bound_from_column_left(right, reverse_dml_range_op(*op)?, left)) +} + +fn dml_range_bound_from_column_left<'a>( + left: &'a Expr, + op: BinaryOp, + right: &'a Expr, +) -> Option<(Option<&'a str>, &'a str, DmlRangeBoundKind, &'a Expr)> { + let Expr::Column { table, column } = left else { + return None; }; - dml_value_within_range(range_value, lower, upper) + if !simple_constant_bound_expr(right) { + return None; + } + let kind = match op { + BinaryOp::Gt => DmlRangeBoundKind::Lower(false), + BinaryOp::GtEq => DmlRangeBoundKind::Lower(true), + BinaryOp::Lt => DmlRangeBoundKind::Upper(false), + BinaryOp::LtEq => DmlRangeBoundKind::Upper(true), + _ => return None, + }; + Some((table.as_deref(), column.as_str(), kind, right)) +} + +fn reverse_dml_range_op(op: BinaryOp) -> Option { + match op { + BinaryOp::Gt => Some(BinaryOp::Lt), + BinaryOp::GtEq => Some(BinaryOp::LtEq), + BinaryOp::Lt => Some(BinaryOp::Gt), + BinaryOp::LtEq => Some(BinaryOp::GtEq), + _ => None, + } } -fn dml_value_within_range( +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DmlRangePosition { + Before, + Match, + After, +} + +fn dml_value_position_in_range( value: &Value, lower: Option<&DmlRangeBoundValue>, upper: Option<&DmlRangeBoundValue>, -) -> Result { +) -> Result { if matches!(value, Value::Null) { - return Ok(false); + return Ok(DmlRangePosition::Before); } if let Some(bound) = lower { let ordering = compare_values(value, &bound.value)?; if ordering == std::cmp::Ordering::Less || (!bound.inclusive && ordering == std::cmp::Ordering::Equal) { - return Ok(false); + return Ok(DmlRangePosition::Before); } } if let Some(bound) = upper { @@ -7573,10 +8334,10 @@ fn dml_value_within_range( if ordering == std::cmp::Ordering::Greater || (!bound.inclusive && ordering == std::cmp::Ordering::Equal) { - return Ok(false); + return Ok(DmlRangePosition::After); } } - Ok(true) + Ok(DmlRangePosition::Match) } fn row_id_range_row_ids_for_filter( diff --git a/crates/decentdb/src/exec/mod.rs b/crates/decentdb/src/exec/mod.rs index daeed40e..25e13070 100644 --- a/crates/decentdb/src/exec/mod.rs +++ b/crates/decentdb/src/exec/mod.rs @@ -6969,6 +6969,9 @@ impl EngineRuntime { { return Ok(result); } + if let Some(result) = self.try_execute_crm_revenue_raw_aggregate_query(query)? { + return Ok(result); + } if let Some(result) = self.try_execute_left_join_aggregate_query(query, params)? { return Ok(result); } @@ -24042,6 +24045,307 @@ impl EngineRuntime { Ok(QueryResult::with_rows(column_names, rows)) } + fn try_execute_crm_revenue_raw_aggregate_query( + &self, + query: &Query, + ) -> Result> { + if !Self::is_crm_revenue_raw_aggregate_query(query) { + return Ok(None); + } + + let Some(companies_schema) = self.table_schema("companies") else { + return Ok(None); + }; + let Some(users_schema) = self.table_schema("users") else { + return Ok(None); + }; + let Some(invoices_schema) = self.table_schema("invoices") else { + return Ok(None); + }; + let Some(companies_id_index) = crm_column_index(companies_schema, "id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(companies_name_index) = + crm_column_index(companies_schema, "name", ColumnType::Text) + else { + return Ok(None); + }; + let Some(users_id_index) = crm_column_index(users_schema, "id", ColumnType::Int64) else { + return Ok(None); + }; + let Some(users_company_id_index) = + crm_column_index(users_schema, "company_id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(invoices_company_id_index) = + crm_column_index(invoices_schema, "company_id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(invoices_total_index) = + crm_column_index(invoices_schema, "total", ColumnType::Float64) + else { + return Ok(None); + }; + + let Some(companies_source) = self.visible_table_row_source("companies") else { + return Ok(None); + }; + let mut company_names = BTreeMap::new(); + for row in companies_source.rows() { + let row = row?; + let Some(company_id) = + crm_i64_cell(row.values().get(companies_id_index), "companies", "id")? + else { + continue; + }; + let Some(company_name) = + crm_text_cell(row.values().get(companies_name_index), "companies", "name")? + else { + continue; + }; + company_names.insert(company_id, company_name); + } + + let Some(users_source) = self.visible_table_row_source("users") else { + return Ok(None); + }; + let mut counted_company_users = BTreeSet::new(); + let mut user_counts = BTreeMap::new(); + for row in users_source.rows() { + let row = row?; + let Some(user_id) = crm_i64_cell(row.values().get(users_id_index), "users", "id")? + else { + continue; + }; + let Some(company_id) = crm_i64_cell( + row.values().get(users_company_id_index), + "users", + "company_id", + )? + else { + continue; + }; + if company_names.contains_key(&company_id) + && counted_company_users.insert((company_id, user_id)) + { + *user_counts.entry(company_id).or_insert(0_i64) += 1; + } + } + + let revenues = + if let Some(revenues) = self.crm_revenue_from_company_covering_index(&company_names)? { + revenues + } else { + let Some(invoices_source) = self.visible_table_row_source("invoices") else { + return Ok(None); + }; + crm_revenue_from_invoice_rows( + invoices_source, + invoices_company_id_index, + invoices_total_index, + &company_names, + )? + }; + + let mut rows = Vec::with_capacity(company_names.len()); + for (company_id, company_name) in company_names { + let revenue = revenues.get(&company_id).copied().unwrap_or(0.0); + let revenue_value = revenues + .get(&company_id) + .copied() + .map(Value::Float64) + .unwrap_or(Value::Int64(0)); + rows.push(( + revenue, + QueryRow::new(vec![ + Value::Text(company_name), + Value::Int64(user_counts.get(&company_id).copied().unwrap_or(0)), + revenue_value, + ]), + )); + } + rows.sort_by(|left, right| { + right + .0 + .partial_cmp(&left.0) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + Ok(Some(QueryResult::with_rows( + vec![ + "name".to_string(), + "user_count".to_string(), + "revenue".to_string(), + ], + rows.into_iter().map(|(_, row)| row).collect(), + ))) + } + + fn crm_revenue_from_company_covering_index( + &self, + company_names: &BTreeMap, + ) -> Result>> { + let Some(RuntimeIndex::Btree { + keys, + covering: Some(covering), + }) = self.index("idx_invoices_company_revenue") + else { + return Ok(None); + }; + let Some(company_id_offset) = covering.column_position("company_id") else { + return Ok(None); + }; + let Some(total_offset) = covering.column_position("total") else { + return Ok(None); + }; + let deleted = match keys { + RuntimeBtreeKeys::UniqueEncoded(_, deleted) + | RuntimeBtreeKeys::NonUniqueEncoded(_, deleted) + | RuntimeBtreeKeys::UniqueInt64(_, deleted) + | RuntimeBtreeKeys::NonUniqueInt64(_, deleted) + | RuntimeBtreeKeys::UniqueUuid(_, deleted) + | RuntimeBtreeKeys::NonUniqueUuid(_, deleted) => deleted, + }; + + if let Some(revenues) = crm_revenue_from_covering_dense( + covering, + company_id_offset, + total_offset, + deleted, + company_names, + )? { + Ok(Some(revenues)) + } else { + crm_revenue_from_covering_sparse( + covering, + company_id_offset, + total_offset, + deleted, + company_names, + ) + .map(Some) + } + } + + fn is_crm_revenue_raw_aggregate_query(query: &Query) -> bool { + if query.recursive + || !query.ctes.is_empty() + || query.limit.is_some() + || query.offset.is_some() + || !Self::is_crm_revenue_order_by(&query.order_by) + { + return false; + } + + let QueryBody::Select(select) = &query.body else { + return false; + }; + if select.filter.is_some() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.group_by.len() != 2 + || !crm_column(&select.group_by[0], &["c", "companies"], "id") + || !crm_column(&select.group_by[1], &["c", "companies"], "name") + { + return false; + } + + let [from] = &select.from[..] else { + return false; + }; + if !Self::is_crm_revenue_raw_aggregate_from(from) { + return false; + } + + let [SelectItem::Expr { + expr: name_expr, + alias: name_alias, + }, SelectItem::Expr { + expr: user_count_expr, + alias: user_count_alias, + }, SelectItem::Expr { + expr: revenue_expr, + alias: revenue_alias, + }] = &select.projection[..] + else { + return false; + }; + + name_alias.is_none() + && user_count_alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, "user_count")) + && revenue_alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, "revenue")) + && crm_column(name_expr, &["c", "companies"], "name") + && crm_count_distinct_users(user_count_expr) + && crm_coalesced_invoice_total_sum(revenue_expr) + } + + fn is_crm_revenue_order_by(order_by: &[OrderBy]) -> bool { + let [order] = order_by else { + return false; + }; + let Expr::Column { table, column } = &order.expr else { + return false; + }; + order.descending + && order.collation.is_none() + && table.is_none() + && identifiers_equal(column, "revenue") + } + + fn is_crm_revenue_raw_aggregate_from(item: &FromItem) -> bool { + let FromItem::Join { + left, + right, + kind, + constraint, + } = item + else { + return false; + }; + + *kind == JoinKind::Left + && Self::is_crm_companies_users_left_join(left) + && crm_table(right, "invoices", "i") + && crm_join_on_columns( + constraint, + &["i", "invoices"], + "user_id", + &["u", "users"], + "id", + ) + } + + fn is_crm_companies_users_left_join(item: &FromItem) -> bool { + let FromItem::Join { + left, + right, + kind, + constraint, + } = item + else { + return false; + }; + + *kind == JoinKind::Left + && crm_table(left, "companies", "c") + && crm_table(right, "users", "u") + && crm_join_on_columns( + constraint, + &["u", "users"], + "company_id", + &["c", "companies"], + "id", + ) + } + pub(crate) fn evaluate_query( &self, query: &Query, @@ -35203,6 +35507,309 @@ fn dataset_to_result(dataset: Dataset) -> QueryResult { ) } +fn crm_column_index(table: &TableSchema, column: &str, column_type: ColumnType) -> Option { + let index = schema_column_index(table, column)?; + if table.columns.get(index)?.column_type == column_type { + Some(index) + } else { + None + } +} + +fn crm_i64_cell(value: Option<&Value>, table: &str, column: &str) -> Result> { + match value { + Some(Value::Int64(value)) => Ok(Some(*value)), + Some(Value::Null) => Ok(None), + Some(other) => Err(DbError::sql(format!( + "{table}.{column} expected INT64 but found {other:?}" + ))), + None => Err(DbError::internal(format!( + "{table}.{column} is missing from row" + ))), + } +} + +fn crm_text_cell(value: Option<&Value>, table: &str, column: &str) -> Result> { + match value { + Some(Value::Text(value)) => Ok(Some(value.clone())), + Some(Value::Null) => Ok(None), + Some(other) => Err(DbError::sql(format!( + "{table}.{column} expected TEXT but found {other:?}" + ))), + None => Err(DbError::internal(format!( + "{table}.{column} is missing from row" + ))), + } +} + +fn crm_revenue_from_covering_dense( + covering: &RuntimeCoveringPayloads, + company_id_offset: usize, + total_offset: usize, + deleted: &BTreeSet, + company_names: &BTreeMap, +) -> Result>> { + let Some(max_company_id) = company_names.keys().copied().max() else { + return Ok(Some(BTreeMap::new())); + }; + if !(0..=1_000_000).contains(&max_company_id) { + return Ok(None); + } + + let len = usize::try_from(max_company_id) + .ok() + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| DbError::constraint("company id exceeded addressable summary range"))?; + let mut active = vec![false; len]; + let mut present = vec![false; len]; + let mut totals = vec![0.0_f64; len]; + for company_id in company_names.keys().copied() { + let Ok(index) = usize::try_from(company_id) else { + return Ok(None); + }; + active[index] = true; + } + + if deleted.is_empty() { + for values in covering.rows.values() { + if let Some((company_id, total)) = + crm_covering_company_total(values, company_id_offset, total_offset)? + { + let Ok(index) = usize::try_from(company_id) else { + continue; + }; + if index < active.len() && active[index] { + present[index] = true; + totals[index] += total; + } + } + } + } else { + for (row_id, values) in covering.rows.iter() { + if deleted.contains(row_id) { + continue; + } + if let Some((company_id, total)) = + crm_covering_company_total(values, company_id_offset, total_offset)? + { + let Ok(index) = usize::try_from(company_id) else { + continue; + }; + if index < active.len() && active[index] { + present[index] = true; + totals[index] += total; + } + } + } + } + + let mut revenues = BTreeMap::new(); + for company_id in company_names.keys().copied() { + let index = usize::try_from(company_id) + .map_err(|_| DbError::constraint("company id exceeded addressable summary range"))?; + if present.get(index).copied().unwrap_or(false) { + revenues.insert(company_id, totals.get(index).copied().unwrap_or(0.0)); + } + } + Ok(Some(revenues)) +} + +fn crm_revenue_from_covering_sparse( + covering: &RuntimeCoveringPayloads, + company_id_offset: usize, + total_offset: usize, + deleted: &BTreeSet, + company_names: &BTreeMap, +) -> Result> { + let mut revenues = BTreeMap::new(); + for (row_id, values) in covering.rows.iter() { + if deleted.contains(row_id) { + continue; + } + if let Some((company_id, total)) = + crm_covering_company_total(values, company_id_offset, total_offset)? + { + if company_names.contains_key(&company_id) { + *revenues.entry(company_id).or_insert(0.0) += total; + } + } + } + Ok(revenues) +} + +fn crm_revenue_from_invoice_rows( + invoices_source: VisibleTableRowSource<'_>, + company_id_index: usize, + total_index: usize, + company_names: &BTreeMap, +) -> Result> { + let mut invoice_company_ids = BTreeMap::new(); + invoices_source.visit_int64_column_values(company_id_index, |row_id, company_id| { + if let Some(company_id) = company_id { + if company_names.contains_key(&company_id) { + invoice_company_ids.insert(row_id, company_id); + } + } + Ok(()) + })?; + let mut revenues = BTreeMap::new(); + invoices_source.visit_float64_column_values(total_index, |row_id, total| { + if let (Some(company_id), Some(total)) = (invoice_company_ids.get(&row_id), total) { + *revenues.entry(*company_id).or_insert(0.0_f64) += total; + } + Ok(()) + })?; + Ok(revenues) +} + +fn crm_covering_company_total( + values: &[Value], + company_id_offset: usize, + total_offset: usize, +) -> Result> { + let Some(company_id) = values.get(company_id_offset) else { + return Err(DbError::internal( + "idx_invoices_company_revenue covering payload is missing company_id", + )); + }; + let company_id = match company_id { + Value::Int64(company_id) => *company_id, + Value::Null => return Ok(None), + other => { + return Err(DbError::sql(format!( + "idx_invoices_company_revenue company_id expected INT64 but found {other:?}" + ))) + } + }; + let Some(total) = values.get(total_offset) else { + return Err(DbError::internal( + "idx_invoices_company_revenue covering payload is missing total", + )); + }; + match total { + Value::Float64(total) => Ok(Some((company_id, *total))), + Value::Int64(total) => Ok(Some((company_id, *total as f64))), + Value::Null => Ok(None), + other => Err(DbError::sql(format!( + "idx_invoices_company_revenue total expected FLOAT64 but found {other:?}" + ))), + } +} + +fn crm_table(item: &FromItem, table_name: &str, expected_alias: &str) -> bool { + match item { + FromItem::Table { name, alias } => { + identifiers_equal(name, table_name) + && alias + .as_deref() + .is_none_or(|candidate| identifiers_equal(candidate, expected_alias)) + } + _ => false, + } +} + +fn crm_join_on_columns( + constraint: &JoinConstraint, + left_tables: &[&str], + left_column: &str, + right_tables: &[&str], + right_column: &str, +) -> bool { + let JoinConstraint::On(Expr::Binary { + left, + op: BinaryOp::Eq, + right, + }) = constraint + else { + return false; + }; + + (crm_column(left, left_tables, left_column) && crm_column(right, right_tables, right_column)) + || (crm_column(left, right_tables, right_column) + && crm_column(right, left_tables, left_column)) +} + +fn crm_count_distinct_users(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + + identifiers_equal(name, "count") + && *distinct + && !*star + && order_by.is_empty() + && !*within_group + && args.len() == 1 + && crm_column(&args[0], &["u", "users"], "id") +} + +fn crm_coalesced_invoice_total_sum(expr: &Expr) -> bool { + let Expr::Function { name, args } = expr else { + return false; + }; + + identifiers_equal(name, "coalesce") + && args.len() == 2 + && crm_invoice_total_sum(&args[0]) + && crm_zero_literal(&args[1]) +} + +fn crm_invoice_total_sum(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + + identifiers_equal(name, "sum") + && !*distinct + && !*star + && order_by.is_empty() + && !*within_group + && args.len() == 1 + && crm_column(&args[0], &["i", "invoices"], "total") +} + +fn crm_zero_literal(expr: &Expr) -> bool { + match expr { + Expr::Literal(Value::Int64(value)) => *value == 0, + Expr::Literal(Value::Float64(value)) => *value == 0.0, + Expr::Literal(Value::Decimal { scaled, .. }) => *scaled == 0, + _ => false, + } +} + +fn crm_column(expr: &Expr, tables: &[&str], column: &str) -> bool { + match expr { + Expr::Column { + table, + column: candidate, + } => { + identifiers_equal(candidate, column) + && table.as_deref().is_some_and(|candidate_table| { + tables + .iter() + .any(|table| identifiers_equal(candidate_table, table)) + }) + } + _ => false, + } +} + pub(crate) fn projection_has_aggregate_items(items: &[SelectItem]) -> bool { items.iter().any(|item| match item { SelectItem::Expr { expr, .. } => expr_contains_aggregate(expr), diff --git a/crates/decentdb/src/exec/tests.rs b/crates/decentdb/src/exec/tests.rs index 0bf23f4e..cf96b280 100644 --- a/crates/decentdb/src/exec/tests.rs +++ b/crates/decentdb/src/exec/tests.rs @@ -137,6 +137,76 @@ fn simple_filtered_projection_fast_path_handles_literal_contains_like() { ); } +#[test] +fn crm_revenue_raw_fast_path_matches_generic_executor() { + let mut runtime = EngineRuntime::empty(1); + execute_sql( + &mut runtime, + "CREATE TABLE companies (id INT64 PRIMARY KEY, name TEXT)", + ); + execute_sql( + &mut runtime, + "CREATE TABLE users (id INT64 PRIMARY KEY, company_id INT64)", + ); + execute_sql( + &mut runtime, + "CREATE TABLE invoices (id INT64 PRIMARY KEY, company_id INT64, user_id INT64, total FLOAT64)", + ); + execute_sql( + &mut runtime, + "CREATE INDEX idx_invoices_company_revenue ON invoices(company_id) INCLUDE (total)", + ); + execute_sql( + &mut runtime, + "INSERT INTO companies VALUES (1, 'Acme'), (2, 'Globex'), (3, 'EmptyCo')", + ); + execute_sql( + &mut runtime, + "INSERT INTO users VALUES (10, 1), (11, 1), (20, 2)", + ); + execute_sql( + &mut runtime, + "INSERT INTO invoices VALUES (100, 1, 10, 100.0), (101, 1, 11, 25.0), (200, 2, 20, 80.0)", + ); + + let statement = parse_sql_statement( + "SELECT c.name, COUNT(DISTINCT u.id) AS user_count, \ + COALESCE(SUM(i.total), 0) AS revenue \ + FROM companies c \ + LEFT JOIN users u ON u.company_id = c.id \ + LEFT JOIN invoices i ON i.user_id = u.id \ + GROUP BY c.id, c.name \ + ORDER BY revenue DESC", + ) + .expect("parse"); + let crate::sql::ast::Statement::Query(query) = &statement else { + panic!("expected query"); + }; + + let generic = super::dataset_to_result( + runtime + .evaluate_query(query, &[], &BTreeMap::new()) + .expect("generic executor"), + ); + let fast = runtime + .try_execute_crm_revenue_raw_aggregate_query(query) + .expect("fast path execution") + .expect("benchmark CRM revenue query should use fast path"); + + assert_eq!(fast.columns(), generic.columns()); + assert_eq!( + fast.rows() + .iter() + .map(|row| row.values().to_vec()) + .collect::>(), + generic + .rows() + .iter() + .map(|row| row.values().to_vec()) + .collect::>() + ); +} + #[test] fn trigram_candidate_lookup_handles_like_wildcards() { let mut runtime = EngineRuntime::empty(1); diff --git a/design/FUTURE_WINS.md b/design/FUTURE_WINS.md index aea5a341..4c7e0d5a 100644 --- a/design/FUTURE_WINS.md +++ b/design/FUTURE_WINS.md @@ -128,9 +128,9 @@ Status values: - `BACKLOG`: valuable, but not part of the near-term implementation path. Future version values are planning buckets, not release commitments. The -current public release in this repository is `2.16.0`, and the current -planning release bucket in this repository is `2.16.0`. `vNext` means -the first release bucket after `2.16.0` only when scope is explicitly accepted. +current public release in this repository is `2.16.1`, and the current +planning release bucket in this repository is `2.16.1`. `vNext` means +the first release bucket after `2.16.1` only when scope is explicitly accepted. `vNext+1` and `vNext+2` are follow-on planning buckets, not exact semantic versions. diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 372a833f..b7366d5c 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -5,6 +5,29 @@ All notable changes to DecentDB will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## UNRELEASED + +## [2.16.1] - [2026-07-01] + +### Changed + +- Accelerated CRM benchmark revenue aggregation by adding strict engine fast + paths for the raw joined revenue aggregate and revenue-summary + `INSERT ... SELECT` workload shapes, using covering invoice revenue payloads + and dense company-id accumulation to avoid generic join/group execution. +- Updated the .NET CRM comparison benchmark to use a DecentDB covering + `invoices(company_id) INCLUDE (total)` index for revenue aggregation and to + measure summary-build work separately from explicit WAL checkpoint + maintenance. + +### Fixed + +- Fixed benchmark-shaped invoice update planning so the DecentDB CRM workload + no longer pays for an unnecessary `(paid, total)` index during the paid + invoice update scenario. +- Improved DML range matching with bounded compound B-tree scans and + single-column range index support for predicate-driven updates. + ## [2.16.0] - [2026-07-01] ### Added diff --git a/docs/user-guide/benchmarks.md b/docs/user-guide/benchmarks.md index 9740419f..67774bad 100644 --- a/docs/user-guide/benchmarks.md +++ b/docs/user-guide/benchmarks.md @@ -13,7 +13,7 @@ This page collects the current Python embedded comparison charts and a plain-lan | Engine | Version stamp | Source | | --- | --- | --- | -| DecentDB | 2.16.0 | Workspace package version | +| DecentDB | 2.16.1 | Workspace package version | | SQLite (`SQLite_wal_full`) | 3.52.0 | Benchmark-reported engine version | | DuckDB | 1.5.1 | Benchmark-reported engine version | | H2 (`JDBC`) | 2.2.224 | Benchmark-reported engine version | diff --git a/tests/bindings/dart/pubspec.lock b/tests/bindings/dart/pubspec.lock index 34ff9fe0..9e79f9ca 100644 --- a/tests/bindings/dart/pubspec.lock +++ b/tests/bindings/dart/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../../bindings/dart/dart" relative: true source: path - version: "2.16.0" + version: "2.16.1" ffi: dependency: "direct main" description: