Skip to content
This repository was archived by the owner on Mar 29, 2026. It is now read-only.

Commit be5081f

Browse files
authored
Merge pull request #34 from sphildreth/sph-2026-02-21.02
Sph 2026 02 21.02
2 parents 7255cd8 + ee91032 commit be5081f

13 files changed

Lines changed: 500 additions & 318 deletions

File tree

2.27 KB
Loading

benchmarks/embedded_compare/assets/decentdb-benchmarks.svg

Lines changed: 235 additions & 294 deletions
Loading

benchmarks/embedded_compare/data/bench_summary.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"engines": {
33
"DecentDB": {
4-
"commit_p95_ms": 3.021554,
5-
"insert_rows_per_sec": 1108156.028368794,
6-
"join_p95_ms": 0.486754,
7-
"read_p95_ms": 0.001663
4+
"commit_p95_ms": 3.01407,
5+
"insert_rows_per_sec": 1109262.340543539,
6+
"join_p95_ms": 0.366649,
7+
"read_p95_ms": 0.001212
88
},
99
"DuckDB": {
10-
"commit_p95_ms": 11.964168,
11-
"insert_rows_per_sec": 10717.36695728593,
12-
"join_p95_ms": 1.28361,
13-
"read_p95_ms": 0.197822
10+
"commit_p95_ms": 11.938741,
11+
"insert_rows_per_sec": 10514.98174599169,
12+
"join_p95_ms": 1.805541,
13+
"read_p95_ms": 0.204874
1414
},
1515
"H2": {
1616
"read_p95_ms": 0.01785111746
@@ -20,17 +20,17 @@
2020
"read_p95_ms": 0.018636946209999998
2121
},
2222
"SQLite": {
23-
"commit_p95_ms": 3.021293,
24-
"insert_rows_per_sec": 1108893.324462187,
25-
"join_p95_ms": 0.383901,
26-
"read_p95_ms": 0.001974
23+
"commit_p95_ms": 3.017577,
24+
"insert_rows_per_sec": 1110987.668036885,
25+
"join_p95_ms": 0.39961,
26+
"read_p95_ms": 0.002304
2727
}
2828
},
2929
"metadata": {
3030
"durability_profile": "safe",
3131
"machine": "batman (AMD Ryzen 9 3900X 12-Core Processor)",
3232
"notes": "Generated from raw benchmark outputs in benchmarks/embedded_compare/raw/sample; merged extra engines from benchmarks/python_embedded_compare/out/results_merged.json",
33-
"run_id": "20260215_230240",
33+
"run_id": "20260221_202357",
3434
"units": {
3535
"commit_p95_ms": "ms (lower is better)",
3636
"insert_rows_per_sec": "rows/sec (higher is better)",

bindings/dotnet/src/DecentDB.EntityFrameworkCore.NodaTime/Storage/Internal/DecentDBNodaTimeTypeMappingSource.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public DecentDBNodaTimeTypeMappingSource(
2525
var longMapping = new LongTypeMapping("INTEGER", DbType.Int64);
2626
var floatMapping = new FloatTypeMapping("REAL", DbType.Single);
2727
var doubleMapping = new DoubleTypeMapping("REAL", DbType.Double);
28-
var decimalMapping = new DecimalTypeMapping("DECIMAL", DbType.Decimal, precision: null, scale: null);
28+
var decimalMapping = new DecimalTypeMapping("DECIMAL(18,4)", DbType.Decimal, precision: 18, scale: 4);
2929
var stringMapping = new StringTypeMapping("TEXT", DbType.String);
3030
var blobMapping = new ByteArrayTypeMapping("BLOB", DbType.Binary);
3131

@@ -155,6 +155,11 @@ public DecentDBNodaTimeTypeMappingSource(
155155
var clrType = Nullable.GetUnderlyingType(mappingInfo.ClrType ?? typeof(object)) ?? mappingInfo.ClrType;
156156
if (clrType != null && _clrMappings.TryGetValue(clrType, out var clrMapping))
157157
{
158+
if (clrType == typeof(decimal))
159+
{
160+
return CreateDecimalMapping(mappingInfo, mappingInfo.StoreTypeName);
161+
}
162+
158163
return clrMapping;
159164
}
160165

@@ -164,13 +169,66 @@ public DecentDBNodaTimeTypeMappingSource(
164169
var normalized = NormalizeStoreTypeName(storeType);
165170
if (_storeMappings.TryGetValue(normalized, out var storeMapping))
166171
{
172+
if (normalized is "DECIMAL" or "NUMERIC")
173+
{
174+
return CreateDecimalMapping(mappingInfo, mappingInfo.StoreTypeName ?? storeType);
175+
}
176+
167177
return storeMapping;
168178
}
169179
}
170180

171181
return null;
172182
}
173183

184+
private static DecimalTypeMapping CreateDecimalMapping(
185+
in RelationalTypeMappingInfo mappingInfo,
186+
string? storeTypeName)
187+
{
188+
const int defaultPrecision = 18;
189+
const int defaultScale = 4;
190+
191+
var precision = mappingInfo.Precision;
192+
var scale = mappingInfo.Scale;
193+
194+
if (!precision.HasValue && !scale.HasValue && !string.IsNullOrWhiteSpace(storeTypeName))
195+
{
196+
(precision, scale) = ParsePrecisionScale(storeTypeName);
197+
}
198+
199+
var p = precision ?? defaultPrecision;
200+
var s = scale ?? defaultScale;
201+
202+
return new DecimalTypeMapping($"DECIMAL({p},{s})", DbType.Decimal, precision: p, scale: s);
203+
}
204+
205+
private static (int? precision, int? scale) ParsePrecisionScale(string storeTypeName)
206+
{
207+
var openParen = storeTypeName.IndexOf('(');
208+
var closeParen = storeTypeName.IndexOf(')');
209+
if (openParen < 0 || closeParen <= openParen)
210+
{
211+
return (null, null);
212+
}
213+
214+
var inner = storeTypeName.AsSpan()[(openParen + 1)..closeParen];
215+
var commaIdx = inner.IndexOf(',');
216+
217+
if (commaIdx >= 0
218+
&& int.TryParse(inner[..commaIdx].Trim(), out var p)
219+
&& int.TryParse(inner[(commaIdx + 1)..].Trim(), out var s))
220+
{
221+
return (p, s);
222+
}
223+
224+
if (int.TryParse(inner.Trim(), out var pOnly))
225+
{
226+
return (pOnly, null);
227+
}
228+
229+
return (null, null);
230+
}
231+
174232
private static string NormalizeStoreTypeName(string storeTypeName)
175233
{
176234
var idx = storeTypeName.IndexOf('(');

decentdb.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version = "1.2.0"
1+
version = "1.3.0"
22
author = "DecentDB contributors"
33
description = "DecentDB engine"
44
license = "Apache-2.0"

docs/about/changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to DecentDB will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.3.0] - 2026-02-21
9+
10+
### Added
11+
- **SQL Engine**: Hex blob literal (`X'DEADBEEF'`) syntax support — new `svBlob` value kind parsed from libpg_query `bsval` nodes, with full support across parser, binder, executor, EXPLAIN output, and storage predicate evaluator.
12+
13+
### Fixed
14+
- **SQL Engine**: Self-referencing foreign keys — `CREATE TABLE` with a foreign key referencing the same table (e.g., `parent_id REFERENCES self(id)`) now validates against the columns being defined instead of failing with "Table not found".
15+
- **SQL Engine**: UUID ↔ Blob type coercion — blob literals (e.g., `X'...'` with 16 bytes) are now accepted for UUID columns in INSERT/UPDATE, and vice versa. Previously the binder rejected these with "Type mismatch" despite the runtime already supporting 16-byte blobs as UUIDs.
16+
- .NET: `DecentDB.EntityFrameworkCore.NodaTime` DECIMAL type mapping now respects precision and scale from EF Core model configuration (e.g., `HasPrecision(18, 6)`). Previously ignored model-specified precision/scale and always emitted `DECIMAL(18,4)`.
17+
818
## [1.2.0] - 2026-02-21
919

1020
### Added

src/exec/exec.nim

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,8 @@ proc evalLiteral(value: SqlValue): Value =
14061406
for ch in value.strVal:
14071407
bytes.add(byte(ch))
14081408
Value(kind: vkText, bytes: bytes)
1409+
of svBlob:
1410+
Value(kind: vkBlob, bytes: value.blobVal)
14091411
of svParam: Value(kind: vkNull)
14101412

14111413
proc textValue(text: string): Value =
@@ -1633,7 +1635,7 @@ proc valueToSqlLiteral(v: Value): SqlValue =
16331635
of vkText, vkTextOverflow, vkTextCompressed, vkTextCompressedOverflow:
16341636
SqlValue(kind: svString, strVal: valueToString(v))
16351637
of vkBlob, vkBlobOverflow, vkBlobCompressed, vkBlobCompressedOverflow:
1636-
SqlValue(kind: svString, strVal: valueToString(v))
1638+
SqlValue(kind: svBlob, blobVal: v.bytes)
16371639

16381640
proc collectInnerTables(stmt: Statement): seq[string] =
16391641
## Collect all table names/aliases from a SELECT's FROM clause.

src/planner/explain.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ proc renderExpr*(expr: Expr): string =
3636
of svFloat: $expr.value.floatVal
3737
of svBool: $expr.value.boolVal
3838
of svString: "'" & expr.value.strVal.replace("'", "''") & "'"
39+
of svBlob: "X'" & expr.value.blobVal.mapIt(it.toHex(2)).join("") & "'"
3940
of svParam: "$" & $expr.value.paramIndex
4041
of ekColumn:
4142
if expr.table.len > 0:

src/sql/binder.nim

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ proc literalType(expr: Expr): Option[ColumnType] =
3333
return some(ctFloat64)
3434
of svString:
3535
return some(ctText)
36+
of svBlob:
37+
return some(ctBlob)
3638
of svParam:
3739
return none(ColumnType)
3840

@@ -45,7 +47,9 @@ proc checkLiteralType(expr: Expr, expected: ColumnType, columnName: string): Res
4547
(expected == ctInt64 and actual in {ctBool, ctFloat64}) or
4648
(expected == ctFloat64 and actual in {ctInt64, ctDecimal}) or
4749
(expected == ctDecimal and actual in {ctFloat64, ctInt64}) or
48-
(expected == ctBool and actual == ctInt64)
50+
(expected == ctBool and actual == ctInt64) or
51+
(expected == ctUuid and actual == ctBlob) or
52+
(expected == ctBlob and actual == ctUuid)
4953
if not compatible:
5054
return err[Void](ERR_SQL, "Type mismatch for column", columnName)
5155
okVoid()
@@ -438,6 +442,7 @@ proc inferExprType(catalog: Catalog, map: Table[string, TableMeta], expr: Expr):
438442
of svFloat: return ctFloat64
439443
of svBool: return ctBool
440444
of svString: return ctText
445+
of svBlob: return ctBlob
441446
else: return ctText
442447
of ekColumn:
443448
let res = resolveColumn(map, expr.table, expr.name)
@@ -1796,24 +1801,29 @@ proc bindCreateTable(catalog: Catalog, stmt: Statement): Result[Statement] =
17961801
return err[Statement](ERR_SQL, "ON DELETE SET NULL requires nullable child column", col.name)
17971802
if onUpdate == "SET NULL" and col.notNull:
17981803
return err[Statement](ERR_SQL, "ON UPDATE SET NULL requires nullable child column", col.name)
1799-
let parentRes = catalog.getTable(col.refTable)
1800-
if not parentRes.ok:
1801-
return err[Statement](parentRes.err.code, parentRes.err.message, col.refTable)
1804+
# Self-referencing FK: validate against the columns being created
1805+
let isSelfRef = col.refTable == stmt.createTableName
1806+
let parentColumns = if isSelfRef: tableColumns
1807+
else:
1808+
let parentRes = catalog.getTable(col.refTable)
1809+
if not parentRes.ok:
1810+
return err[Statement](parentRes.err.code, parentRes.err.message, col.refTable)
1811+
parentRes.value.columns
18021812
var parentHasColumn = false
1803-
for parentCol in parentRes.value.columns:
1813+
for parentCol in parentColumns:
18041814
if parentCol.name == col.refColumn:
18051815
parentHasColumn = true
18061816
break
18071817
if not parentHasColumn:
18081818
return err[Statement](ERR_SQL, "Referenced column not found", col.refTable & "." & col.refColumn)
1809-
let parentIdx = catalog.getIndexForColumn(col.refTable, col.refColumn, ikBtree, requireUnique = true)
18101819
var isInt64Pk = false
1811-
for parentCol in parentRes.value.columns:
1820+
for parentCol in parentColumns:
18121821
if parentCol.name == col.refColumn and parentCol.primaryKey and parentCol.kind == ctInt64:
18131822
isInt64Pk = true
18141823
break
18151824

1816-
if not isInt64Pk:
1825+
if not isInt64Pk and not isSelfRef:
1826+
let parentIdx = catalog.getIndexForColumn(col.refTable, col.refColumn, ikBtree, requireUnique = true)
18171827
if isNone(parentIdx):
18181828
return err[Statement](ERR_SQL, "Referenced column must be indexed uniquely", col.refTable & "." & col.refColumn)
18191829
if primaryCount > 1:

src/sql/sql.nim

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import tables
22
import json
33
import strutils
4+
import sequtils
45
import ../errors
56
import ./pg_query_ffi
67

@@ -11,6 +12,7 @@ type SqlValueKind* = enum
1112
svString
1213
svBool
1314
svParam
15+
svBlob
1416

1517
type SqlValue* = object
1618
kind*: SqlValueKind
@@ -19,6 +21,7 @@ type SqlValue* = object
1921
strVal*: string
2022
boolVal*: bool
2123
paramIndex*: int
24+
blobVal*: seq[byte]
2225

2326
type ExprKind* = enum
2427
ekLiteral
@@ -358,6 +361,18 @@ proc parseAConst*(node: JsonNode): Result[Expr] =
358361
return ok(Expr(kind: ekLiteral, value: SqlValue(kind: svBool, boolVal: boolNode["boolval"].getBool)))
359362
# pg_query omits boolval field when false (protobuf default); empty object means false.
360363
return ok(Expr(kind: ekLiteral, value: SqlValue(kind: svBool, boolVal: false)))
364+
if nodeHas(node, "bsval"):
365+
# Hex blob literal: X'DEADBEEF' → bsval: "xDEADBEEF"
366+
let bsNode = node["bsval"]
367+
let hexStr = if nodeHas(bsNode, "bsval"): bsNode["bsval"].getStr else: bsNode.getStr
368+
var hex = hexStr
369+
if hex.len > 0 and (hex[0] == 'x' or hex[0] == 'X'):
370+
hex = hex[1..^1]
371+
var bytes: seq[byte] = @[]
372+
for i in countup(0, hex.len - 1, 2):
373+
let pair = if i + 1 < hex.len: hex[i..i+1] else: hex[i..i]
374+
bytes.add(byte(parseHexInt(pair)))
375+
return ok(Expr(kind: ekLiteral, value: SqlValue(kind: svBlob, blobVal: bytes)))
361376
err[Expr](ERR_SQL, "Unsupported A_Const")
362377

363378
proc parseColumnRef(node: JsonNode): Result[Expr] =
@@ -1228,6 +1243,8 @@ proc exprToCanonicalSql*(expr: Expr): string =
12281243
quoteSqlString(expr.value.strVal)
12291244
of svBool:
12301245
if expr.value.boolVal: "TRUE" else: "FALSE"
1246+
of svBlob:
1247+
"X'" & expr.value.blobVal.mapIt(it.toHex(2)).join("") & "'"
12311248
of svParam:
12321249
"$" & $expr.value.paramIndex
12331250
of ekColumn:

0 commit comments

Comments
 (0)