fix: issue #9 reserved keyword 'key' + issue #10 empty string treated as NULL
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
Issue 9: Add backtick-quoted identifier support in lexer.nim - Users can now write `key` to use reserved keywords as column/table names Issue 10: Use \N as internal NULL marker instead of empty string - isNull() now checks for \N instead of value.len == 0 - Empty strings can now be stored in NOT NULL columns - NULL values properly distinguished from empty strings in storage, wire protocol, and HTTP JSON responses
This commit is contained in:
+53
-1
@@ -161,6 +161,56 @@ All DB operations (`query`, `getRow`, `exec`, etc.) are wrapped in this lock.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 9. `key` is a Reserved SQL Keyword
|
||||||
|
|
||||||
|
**Problem:** BaraDB's SQL parser treats `key` as a reserved keyword. Using it as a column or table name causes parse errors:
|
||||||
|
|
||||||
|
```
|
||||||
|
Expected tkIdent but got tkKey
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** The `settings` table could not use `key varchar(100) primary key`.
|
||||||
|
|
||||||
|
**Fix:** Added backtick-quoted identifier support in `query/lexer.nim`. Users can now escape reserved keywords using backticks:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE settings(
|
||||||
|
`key` varchar(100) primary key,
|
||||||
|
value varchar(500) default ''
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Changes made:
|
||||||
|
- Added `readBacktickIdent()` proc in `lexer.nim`
|
||||||
|
- Added backtick case in `nextToken()` to recognize `` `identifier` `` as `tkIdent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Empty String (`""`) Treated as NULL
|
||||||
|
|
||||||
|
**Problem:** BaraDB normalized empty strings to `NULL` internally. A column defined as `NOT NULL` rejected empty string inserts, even when the application explicitly passed `''`.
|
||||||
|
|
||||||
|
**Result:** SMTP settings saved via the admin panel failed with constraint violations when fields were left blank:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO settings (skey, value) VALUES ('smtpUser', '');
|
||||||
|
-- ERROR: NOT NULL constraint violation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Changed internal NULL representation from empty string `""` to `\N` in `query/executor.nim`:
|
||||||
|
|
||||||
|
1. `isNull()` now checks for `\N` instead of `value.len == 0`
|
||||||
|
2. NULL literals in INSERT/UPDATE store `\N` instead of `""`
|
||||||
|
3. Missing columns return `\N` instead of `""`
|
||||||
|
4. `getValue()` returns `\N` for missing fields
|
||||||
|
5. JOIN padding uses `\N` for unmatched rows
|
||||||
|
6. Server wire protocol uses `\N` sentinel to send `fkNull`
|
||||||
|
7. HTTP server sends JSON `null` for NULL values
|
||||||
|
|
||||||
|
This allows empty strings to be stored in NOT NULL columns while still properly supporting NULL values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Summary Table
|
## Summary Table
|
||||||
|
|
||||||
| # | Issue | Location | Type |
|
| # | Issue | Location | Type |
|
||||||
@@ -173,7 +223,9 @@ All DB operations (`query`, `getRow`, `exec`, etc.) are wrapped in this lock.
|
|||||||
| 6 | Inconsistent agg names | N/A (engine behavior) | Naming convention |
|
| 6 | Inconsistent agg names | N/A (engine behavior) | Naming convention |
|
||||||
| 7 | Async client unstable | `forum/src/baradb_client.nim` | Client design |
|
| 7 | Async client unstable | `forum/src/baradb_client.nim` | Client design |
|
||||||
| 8 | No thread safety | `forum/src/baradb_sqlite.nim` | Adapter design |
|
| 8 | No thread safety | `forum/src/baradb_sqlite.nim` | Adapter design |
|
||||||
|
| 9 | `key` reserved keyword | `query/lexer.nim` | Parser limitation |
|
||||||
|
| 10 | Empty string = NULL | `query/executor.nim` | Data handling bug |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Document version: 2026-05-15*
|
*Document version: 2026-05-17*
|
||||||
|
|||||||
@@ -164,9 +164,10 @@ proc queryHandler(server: HttpServer): RequestHandler =
|
|||||||
var jsonRow = newJObject()
|
var jsonRow = newJObject()
|
||||||
for col in res.columns:
|
for col in res.columns:
|
||||||
let key = col
|
let key = col
|
||||||
var val = ""
|
if key in row and not isNull(row[key]):
|
||||||
if key in row: val = row[key]
|
jsonRow[key] = %row[key]
|
||||||
jsonRow[key] = %val
|
else:
|
||||||
|
jsonRow[key] = newJNull()
|
||||||
jsonRows.add(jsonRow)
|
jsonRows.add(jsonRow)
|
||||||
var jsonCols = newJArray()
|
var jsonCols = newJArray()
|
||||||
for c in res.columns:
|
for c in res.columns:
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ proc typeToFieldKind*(colType: string): FieldKind =
|
|||||||
return fkString
|
return fkString
|
||||||
|
|
||||||
proc valueToWire(val: string, colType: string): WireValue =
|
proc valueToWire(val: string, colType: string): WireValue =
|
||||||
if val.len == 0 or val.toLower() == "null":
|
if val == "\\N" or val.toLower() == "null":
|
||||||
return WireValue(kind: fkNull)
|
return WireValue(kind: fkNull)
|
||||||
let t = colType.toUpper()
|
let t = colType.toUpper()
|
||||||
if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL":
|
if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL":
|
||||||
@@ -199,7 +199,7 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
|
|||||||
for row in res.rows:
|
for row in res.rows:
|
||||||
var wireRow: seq[WireValue] = @[]
|
var wireRow: seq[WireValue] = @[]
|
||||||
for i, col in res.columns:
|
for i, col in res.columns:
|
||||||
let val = if col in row: row[col] else: ""
|
let val = if col in row: row[col] else: "\\N"
|
||||||
let cType = if i < colTypes.len: colTypes[i] else: ""
|
let cType = if i < colTypes.len: colTypes[i] else: ""
|
||||||
wireRow.add(valueToWire(val, cType))
|
wireRow.add(valueToWire(val, cType))
|
||||||
qr.rows.add(wireRow)
|
qr.rows.add(wireRow)
|
||||||
|
|||||||
@@ -428,10 +428,10 @@ proc getValue(values: seq[string], fields: seq[string], colName: string): string
|
|||||||
for i, f in fields:
|
for i, f in fields:
|
||||||
if f.toLower() == colName.toLower() and i < values.len:
|
if f.toLower() == colName.toLower() and i < values.len:
|
||||||
return values[i]
|
return values[i]
|
||||||
return ""
|
return "\\N"
|
||||||
|
|
||||||
proc isNull*(value: string): bool =
|
proc isNull*(value: string): bool =
|
||||||
value.len == 0 or value.toLower() == "null"
|
value == "\\N" or value.toLower() == "null"
|
||||||
|
|
||||||
proc escapeRowVal(v: string): string =
|
proc escapeRowVal(v: string): string =
|
||||||
v.replace("\\", "\\\\").replace(",", "\\,").replace("=", "\\=")
|
v.replace("\\", "\\\\").replace(",", "\\,").replace("=", "\\=")
|
||||||
@@ -506,8 +506,8 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
of vkInt64: return $expr.literal.int64Val
|
of vkInt64: return $expr.literal.int64Val
|
||||||
of vkFloat64: return $expr.literal.float64Val
|
of vkFloat64: return $expr.literal.float64Val
|
||||||
of vkBool: return $expr.literal.boolVal
|
of vkBool: return $expr.literal.boolVal
|
||||||
of vkNull: return ""
|
of vkNull: return "\\N"
|
||||||
else: return ""
|
else: return "\\N"
|
||||||
of irekField:
|
of irekField:
|
||||||
if expr.fieldPath.len > 0:
|
if expr.fieldPath.len > 0:
|
||||||
# Check full path first for joined columns (e.g. "u.name")
|
# Check full path first for joined columns (e.g. "u.name")
|
||||||
@@ -520,12 +520,12 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
if "$value" in row:
|
if "$value" in row:
|
||||||
let parsed = parseRowData(row["$value"])
|
let parsed = parseRowData(row["$value"])
|
||||||
if colName in parsed: return parsed[colName]
|
if colName in parsed: return parsed[colName]
|
||||||
return ""
|
return "\\N"
|
||||||
of irekStar:
|
of irekStar:
|
||||||
return "*"
|
return "*"
|
||||||
of irekJsonPath:
|
of irekJsonPath:
|
||||||
let srcVal = evalExpr(expr.jpExpr, row, ctx)
|
let srcVal = evalExpr(expr.jpExpr, row, ctx)
|
||||||
if srcVal.len == 0: return ""
|
if srcVal.len == 0 or isNull(srcVal): return "\\N"
|
||||||
try:
|
try:
|
||||||
let node = parseJson(srcVal)
|
let node = parseJson(srcVal)
|
||||||
if node.hasKey(expr.jpKey):
|
if node.hasKey(expr.jpKey):
|
||||||
@@ -1147,7 +1147,7 @@ proc execDelete*(ctx: ExecutionContext, table: string, key: string,
|
|||||||
if c in oldRow:
|
if c in oldRow:
|
||||||
oldVals.add(oldRow[c])
|
oldVals.add(oldRow[c])
|
||||||
else:
|
else:
|
||||||
oldVals.add("")
|
oldVals.add("\\N")
|
||||||
let oldIdxVal = oldVals.join("|")
|
let oldIdxVal = oldVals.join("|")
|
||||||
if oldIdxVal.len > 0 and not isNull(oldIdxVal):
|
if oldIdxVal.len > 0 and not isNull(oldIdxVal):
|
||||||
ctx.btrees[colName].remove(oldIdxVal, IndexEntry(lsmKey: fullKey, rowValue: cast[string](existingVal)))
|
ctx.btrees[colName].remove(oldIdxVal, IndexEntry(lsmKey: fullKey, rowValue: cast[string](existingVal)))
|
||||||
@@ -1196,11 +1196,11 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
|
|||||||
if c in oldRow:
|
if c in oldRow:
|
||||||
oldVals.add(oldRow[c])
|
oldVals.add(oldRow[c])
|
||||||
else:
|
else:
|
||||||
oldVals.add("")
|
oldVals.add("\\N")
|
||||||
if c in parsed:
|
if c in parsed:
|
||||||
newVals.add(parsed[c])
|
newVals.add(parsed[c])
|
||||||
else:
|
else:
|
||||||
newVals.add("")
|
newVals.add("\\N")
|
||||||
let oldIdxVal = oldVals.join("|")
|
let oldIdxVal = oldVals.join("|")
|
||||||
if oldIdxVal.len > 0 and not isNull(oldIdxVal):
|
if oldIdxVal.len > 0 and not isNull(oldIdxVal):
|
||||||
ctx.btrees[colName].remove(oldIdxVal, IndexEntry(lsmKey: fullKey, rowValue: cast[string](existing)))
|
ctx.btrees[colName].remove(oldIdxVal, IndexEntry(lsmKey: fullKey, rowValue: cast[string](existing)))
|
||||||
@@ -1220,14 +1220,15 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
|
|||||||
docId = docId * 31 + uint64(ord(ch))
|
docId = docId * 31 + uint64(ord(ch))
|
||||||
ftsIdx.removeDocument(docId)
|
ftsIdx.removeDocument(docId)
|
||||||
let colName = ftsKey[table.len + 1..^1]
|
let colName = ftsKey[table.len + 1..^1]
|
||||||
let newText = if colName in parsed: parsed[colName] else: ""
|
let newText = if colName in parsed: parsed[colName] else: "\\N"
|
||||||
if newText.len > 0:
|
if newText.len > 0 and not isNull(newText):
|
||||||
ftsIdx.addDocument(docId, newText)
|
ftsIdx.addDocument(docId, newText)
|
||||||
# Update Vector indexes: add new vector (no remove support in current HNSW)
|
# Update Vector indexes: add new vector (no remove support in current HNSW)
|
||||||
for vecKey, vecIdx in ctx.vectorIndexes:
|
for vecKey, vecIdx in ctx.vectorIndexes:
|
||||||
if vecKey.startsWith(table & "."):
|
if vecKey.startsWith(table & "."):
|
||||||
let colName = vecKey[table.len + 1..^1]
|
let colName = vecKey[table.len + 1..^1]
|
||||||
let vecStr = if colName in parsed: parsed[colName] else: ""
|
let vecStr = if colName in parsed: parsed[colName] else: "\\N"
|
||||||
|
if not isNull(vecStr):
|
||||||
let vec = parseVectorString(vecStr)
|
let vec = parseVectorString(vecStr)
|
||||||
if vec.len > 0:
|
if vec.len > 0:
|
||||||
var docId: uint64 = 0
|
var docId: uint64 = 0
|
||||||
@@ -1357,7 +1358,7 @@ proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
|||||||
if i < rowVals.len:
|
if i < rowVals.len:
|
||||||
row[f] = rowVals[i]
|
row[f] = rowVals[i]
|
||||||
else:
|
else:
|
||||||
row[f] = ""
|
row[f] = "\\N"
|
||||||
let checkExpr = lowerExpr(check.checkNode)
|
let checkExpr = lowerExpr(check.checkNode)
|
||||||
let checkResult = evalExpr(checkExpr, row, ctx)
|
let checkResult = evalExpr(checkExpr, row, ctx)
|
||||||
if checkResult != "true":
|
if checkResult != "true":
|
||||||
@@ -1878,9 +1879,9 @@ proc computeWindowValues*(rows: seq[Row], expr: IRExpr, ctx: ExecutionContext =
|
|||||||
let (_, fEnd) = resolveFrameBounds(pos, sortedIdxs.len, frameStart, frameEnd)
|
let (_, fEnd) = resolveFrameBounds(pos, sortedIdxs.len, frameStart, frameEnd)
|
||||||
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[fEnd]], ctx)
|
result[rowIdx] = evalExpr(expr.wfArgs[0], rows[sortedIdxs[fEnd]], ctx)
|
||||||
else:
|
else:
|
||||||
# Unknown window function — fill with empty
|
# Unknown window function — fill with null
|
||||||
for rowIdx in sortedIdxs:
|
for rowIdx in sortedIdxs:
|
||||||
result[rowIdx] = ""
|
result[rowIdx] = "\\N"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# IR Plan Execution (with actual filter/sort/projection)
|
# IR Plan Execution (with actual filter/sort/projection)
|
||||||
@@ -2314,14 +2315,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
if not k.startsWith("$"):
|
if not k.startsWith("$"):
|
||||||
padded[k] = v
|
padded[k] = v
|
||||||
for col in rightCols:
|
for col in rightCols:
|
||||||
if col notin padded: padded[col] = ""
|
if col notin padded: padded[col] = "\\N"
|
||||||
if leftAlias.len > 0:
|
if leftAlias.len > 0:
|
||||||
for k, v in l:
|
for k, v in l:
|
||||||
if not k.startsWith("$"):
|
if not k.startsWith("$"):
|
||||||
padded[leftAlias & "." & k] = v
|
padded[leftAlias & "." & k] = v
|
||||||
if rightAlias.len > 0:
|
if rightAlias.len > 0:
|
||||||
for col in rightCols:
|
for col in rightCols:
|
||||||
padded[rightAlias & "." & col] = ""
|
padded[rightAlias & "." & col] = "\\N"
|
||||||
result.add(padded)
|
result.add(padded)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -2361,14 +2362,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
if not k.startsWith("$"):
|
if not k.startsWith("$"):
|
||||||
padded[k] = v
|
padded[k] = v
|
||||||
for col in rightCols:
|
for col in rightCols:
|
||||||
if col notin padded: padded[col] = ""
|
if col notin padded: padded[col] = "\\N"
|
||||||
if leftAlias.len > 0:
|
if leftAlias.len > 0:
|
||||||
for k, v in l:
|
for k, v in l:
|
||||||
if not k.startsWith("$"):
|
if not k.startsWith("$"):
|
||||||
padded[leftAlias & "." & k] = v
|
padded[leftAlias & "." & k] = v
|
||||||
if rightAlias.len > 0:
|
if rightAlias.len > 0:
|
||||||
for col in rightCols:
|
for col in rightCols:
|
||||||
padded[rightAlias & "." & col] = ""
|
padded[rightAlias & "." & col] = "\\N"
|
||||||
result.add(padded)
|
result.add(padded)
|
||||||
|
|
||||||
if plan.joinKind == irjkRight or plan.joinKind == irjkFull:
|
if plan.joinKind == irjkRight or plan.joinKind == irjkFull:
|
||||||
@@ -2385,14 +2386,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
if not k.startsWith("$"):
|
if not k.startsWith("$"):
|
||||||
padded[k] = v
|
padded[k] = v
|
||||||
for col in leftCols:
|
for col in leftCols:
|
||||||
if col notin padded: padded[col] = ""
|
if col notin padded: padded[col] = "\\N"
|
||||||
if rightAlias.len > 0:
|
if rightAlias.len > 0:
|
||||||
for k, v in r:
|
for k, v in r:
|
||||||
if not k.startsWith("$"):
|
if not k.startsWith("$"):
|
||||||
padded[rightAlias & "." & k] = v
|
padded[rightAlias & "." & k] = v
|
||||||
if leftAlias.len > 0:
|
if leftAlias.len > 0:
|
||||||
for col in leftCols:
|
for col in leftCols:
|
||||||
padded[leftAlias & "." & col] = ""
|
padded[leftAlias & "." & col] = "\\N"
|
||||||
result.add(padded)
|
result.add(padded)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -2496,7 +2497,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
if col in row:
|
if col in row:
|
||||||
newRow[col] = row[col]
|
newRow[col] = row[col]
|
||||||
newRow[plan.unpivotForCol] = inCol
|
newRow[plan.unpivotForCol] = inCol
|
||||||
newRow[plan.unpivotValueCol] = (if inCol in row: row[inCol] else: "")
|
newRow[plan.unpivotValueCol] = (if inCol in row: row[inCol] else: "\\N")
|
||||||
result.add(newRow)
|
result.add(newRow)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -3058,14 +3059,14 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
elif v.kind == nkIntLit: row.add($v.intVal)
|
elif v.kind == nkIntLit: row.add($v.intVal)
|
||||||
elif v.kind == nkFloatLit: row.add($v.floatVal)
|
elif v.kind == nkFloatLit: row.add($v.floatVal)
|
||||||
elif v.kind == nkBoolLit: row.add($v.boolVal)
|
elif v.kind == nkBoolLit: row.add($v.boolVal)
|
||||||
elif v.kind == nkNullLit: row.add("")
|
elif v.kind == nkNullLit: row.add("\\N")
|
||||||
else: row.add(evalNodeToString(v))
|
else: row.add(evalNodeToString(v))
|
||||||
else:
|
else:
|
||||||
if rowNode.kind == nkStringLit: row.add(rowNode.strVal)
|
if rowNode.kind == nkStringLit: row.add(rowNode.strVal)
|
||||||
elif rowNode.kind == nkIntLit: row.add($rowNode.intVal)
|
elif rowNode.kind == nkIntLit: row.add($rowNode.intVal)
|
||||||
elif rowNode.kind == nkFloatLit: row.add($rowNode.floatVal)
|
elif rowNode.kind == nkFloatLit: row.add($rowNode.floatVal)
|
||||||
elif rowNode.kind == nkBoolLit: row.add($rowNode.boolVal)
|
elif rowNode.kind == nkBoolLit: row.add($rowNode.boolVal)
|
||||||
elif rowNode.kind == nkNullLit: row.add("")
|
elif rowNode.kind == nkNullLit: row.add("\\N")
|
||||||
else: row.add(evalNodeToString(rowNode))
|
else: row.add(evalNodeToString(rowNode))
|
||||||
values.add(row)
|
values.add(row)
|
||||||
|
|
||||||
@@ -3173,7 +3174,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
elif s.binRight.kind == nkIntLit: $s.binRight.intVal
|
elif s.binRight.kind == nkIntLit: $s.binRight.intVal
|
||||||
elif s.binRight.kind == nkFloatLit: $s.binRight.floatVal
|
elif s.binRight.kind == nkFloatLit: $s.binRight.floatVal
|
||||||
elif s.binRight.kind == nkBoolLit: $s.binRight.boolVal
|
elif s.binRight.kind == nkBoolLit: $s.binRight.boolVal
|
||||||
elif s.binRight.kind == nkNullLit: ""
|
elif s.binRight.kind == nkNullLit: "\\N"
|
||||||
else: evalNodeToString(s.binRight)
|
else: evalNodeToString(s.binRight)
|
||||||
sets[s.binLeft.identName] = val
|
sets[s.binLeft.identName] = val
|
||||||
|
|
||||||
@@ -3199,7 +3200,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
elif col.name in row:
|
elif col.name in row:
|
||||||
updValues.add(row[col.name])
|
updValues.add(row[col.name])
|
||||||
else:
|
else:
|
||||||
updValues.add("")
|
updValues.add("\\N")
|
||||||
let (valid, errMsg) = validateConstraints(ctx, stmt.updTarget, updFields, @[updValues], skipPkCheck = true)
|
let (valid, errMsg) = validateConstraints(ctx, stmt.updTarget, updFields, @[updValues], skipPkCheck = true)
|
||||||
if not valid: return errResult(errMsg)
|
if not valid: return errResult(errMsg)
|
||||||
# Fire BEFORE UPDATE triggers
|
# Fire BEFORE UPDATE triggers
|
||||||
@@ -3296,7 +3297,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
let valExpr = lowerExpr(v)
|
let valExpr = lowerExpr(v)
|
||||||
values.add(evalExpr(valExpr, combinedRow, ctx))
|
values.add(evalExpr(valExpr, combinedRow, ctx))
|
||||||
else:
|
else:
|
||||||
values.add("")
|
values.add("\\N")
|
||||||
if fields.len > 0:
|
if fields.len > 0:
|
||||||
var row = initTable[string, string]()
|
var row = initTable[string, string]()
|
||||||
for i, f in fields:
|
for i, f in fields:
|
||||||
@@ -3724,7 +3725,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
if col in row:
|
if col in row:
|
||||||
colVals.add(row[col])
|
colVals.add(row[col])
|
||||||
else:
|
else:
|
||||||
colVals.add("")
|
colVals.add("\\N")
|
||||||
let idxVal = colVals.join("|")
|
let idxVal = colVals.join("|")
|
||||||
if idxVal.len > 0 and not isNull(idxVal):
|
if idxVal.len > 0 and not isNull(idxVal):
|
||||||
let lsmKey = if "$key" in row: stmt.ciTarget & "." & row["$key"] else: ""
|
let lsmKey = if "$key" in row: stmt.ciTarget & "." & row["$key"] else: ""
|
||||||
|
|||||||
@@ -481,6 +481,16 @@ proc readString(l: var Lexer, quote: char): string =
|
|||||||
if l.pos < l.input.len:
|
if l.pos < l.input.len:
|
||||||
discard l.advance() # skip closing quote
|
discard l.advance() # skip closing quote
|
||||||
|
|
||||||
|
proc readBacktickIdent(l: var Lexer, startLine, startCol: int): Token =
|
||||||
|
var ident = ""
|
||||||
|
discard l.advance() # skip opening backtick
|
||||||
|
while l.pos < l.input.len and l.input[l.pos] != '`':
|
||||||
|
ident.add(l.input[l.pos])
|
||||||
|
discard l.advance()
|
||||||
|
if l.pos < l.input.len:
|
||||||
|
discard l.advance() # skip closing backtick
|
||||||
|
return Token(kind: tkIdent, value: ident, line: startLine, col: startCol)
|
||||||
|
|
||||||
proc readNumber(l: var Lexer, startLine, startCol: int): Token =
|
proc readNumber(l: var Lexer, startLine, startCol: int): Token =
|
||||||
var numStr = ""
|
var numStr = ""
|
||||||
var isFloat = false
|
var isFloat = false
|
||||||
@@ -692,6 +702,8 @@ proc nextToken*(l: var Lexer): Token =
|
|||||||
of '#':
|
of '#':
|
||||||
l.skipLineComment()
|
l.skipLineComment()
|
||||||
return l.nextToken()
|
return l.nextToken()
|
||||||
|
of '`':
|
||||||
|
return l.readBacktickIdent(startLine, startCol)
|
||||||
else:
|
else:
|
||||||
if ch in Digits:
|
if ch in Digits:
|
||||||
return l.readNumber(startLine, startCol)
|
return l.readNumber(startLine, startCol)
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ suite "JOIN execution":
|
|||||||
check r.rows[0]["name"] == "Alice"
|
check r.rows[0]["name"] == "Alice"
|
||||||
check r.rows[1]["name"] == "Alice"
|
check r.rows[1]["name"] == "Alice"
|
||||||
check r.rows[2]["name"] == "Bob"
|
check r.rows[2]["name"] == "Bob"
|
||||||
check r.rows[2]["total"] == "" # NULL represented as empty string
|
check r.rows[2]["total"] == "\\N" # NULL represented as \N
|
||||||
|
|
||||||
test "RIGHT JOIN keeps unmatched right rows":
|
test "RIGHT JOIN keeps unmatched right rows":
|
||||||
let r = execSql(ctx, "SELECT * FROM users u RIGHT JOIN orders o ON u.id = o.user_id")
|
let r = execSql(ctx, "SELECT * FROM users u RIGHT JOIN orders o ON u.id = o.user_id")
|
||||||
check r.rows.len == 3
|
check r.rows.len == 3
|
||||||
check r.rows[2]["id"] == "30"
|
check r.rows[2]["id"] == "30"
|
||||||
check r.rows[2]["name"] == "" # NULL
|
check r.rows[2]["name"] == "\\N" # NULL
|
||||||
check r.rows[2]["total"] == "150.0"
|
check r.rows[2]["total"] == "150.0"
|
||||||
|
|
||||||
test "FULL JOIN keeps all rows":
|
test "FULL JOIN keeps all rows":
|
||||||
@@ -100,7 +100,7 @@ suite "JOIN execution":
|
|||||||
var foundBob = false
|
var foundBob = false
|
||||||
for row in r.rows:
|
for row in r.rows:
|
||||||
if row["u.name"] == "Bob":
|
if row["u.name"] == "Bob":
|
||||||
check row["x.total"] == ""
|
check row["x.total"] == "\\N"
|
||||||
foundBob = true
|
foundBob = true
|
||||||
check foundBob
|
check foundBob
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user