feat: JSON/JSONB validation, multi-column indexes, CTE execution, wire protocol column types
- Add fkJson to wire protocol with serialization/deserialization - Validate JSON/JSONB on INSERT/UPDATE via std/json - Send real column type metadata in wire protocol responses - Update all 4 clients (Nim, Python, JS, Rust) for JSON and columnTypes - Implement multi-column index parser (CREATE INDEX idx ON t(a, b)) - Add ciColumns to AST nkCreateIndex - Build composite index keys as table.col1.col2 with val1|val2 - Support multi-column exact match in SELECT for AND chains - Implement non-recursive CTE execution via ctx.cteTables materialization - Add tkRecursive token and parse WITH RECURSIVE - Fix test isolation: use temp dirs for JOIN, migration, and RLS tests - All tests passing (0 failures)
This commit is contained in:
@@ -46,6 +46,7 @@ const FieldKind = {
|
||||
ARRAY: 0x0A,
|
||||
OBJECT: 0x0B,
|
||||
VECTOR: 0x0C,
|
||||
JSON: 0x0D,
|
||||
};
|
||||
|
||||
const ResultFormat = {
|
||||
@@ -57,6 +58,7 @@ const ResultFormat = {
|
||||
class QueryResult {
|
||||
constructor() {
|
||||
this.columns = [];
|
||||
this.columnTypes = [];
|
||||
this.rows = [];
|
||||
this.rowCount = 0;
|
||||
this.affectedRows = 0;
|
||||
@@ -169,6 +171,12 @@ class Client {
|
||||
pos += sLen;
|
||||
}
|
||||
result.columns = cols;
|
||||
const colTypes = [];
|
||||
for (let i = 0; i < colCount; i++) {
|
||||
colTypes.push(payload[pos]);
|
||||
pos++;
|
||||
}
|
||||
result.columnTypes = colTypes;
|
||||
const rowCount = new DataView(payload.buffer).getUint32(pos, false);
|
||||
pos += 4;
|
||||
for (let r = 0; r < rowCount; r++) {
|
||||
@@ -252,6 +260,13 @@ class Client {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FieldKind.JSON: {
|
||||
const len = new DataView(payload.buffer).getUint32(pos, false);
|
||||
pos += 4;
|
||||
result = new TextDecoder().decode(payload.slice(pos, pos + len));
|
||||
pos += len;
|
||||
break;
|
||||
}
|
||||
default: result = null;
|
||||
}
|
||||
this._lastReadPos = pos;
|
||||
|
||||
@@ -27,6 +27,7 @@ type
|
||||
fkArray = 0x0A
|
||||
fkObject = 0x0B
|
||||
fkVector = 0x0C
|
||||
fkJson = 0x0D
|
||||
|
||||
MsgKind* = enum
|
||||
mkClientHandshake = 0x01
|
||||
@@ -61,6 +62,7 @@ type
|
||||
of fkArray: arrayVal*: seq[WireValue]
|
||||
of fkObject: objVal*: seq[(string, WireValue)]
|
||||
of fkVector: vecVal*: seq[float32]
|
||||
of fkJson: jsonVal*: string
|
||||
|
||||
proc writeUint32(buf: var seq[byte], val: uint32) =
|
||||
var bytes: array[4, byte]
|
||||
@@ -198,6 +200,8 @@ proc deserializeValue*(buf: openArray[byte], pos: var int): WireValue =
|
||||
vec.add(fv)
|
||||
pos += 4
|
||||
result = WireValue(kind: fkVector, vecVal: vec)
|
||||
of fkJson:
|
||||
result = WireValue(kind: fkJson, jsonVal: readString(buf, pos))
|
||||
else:
|
||||
result = WireValue(kind: fkNull)
|
||||
|
||||
@@ -228,6 +232,7 @@ type
|
||||
|
||||
QueryResult* = object
|
||||
columns*: seq[string]
|
||||
columnTypes*: seq[string]
|
||||
rows*: seq[seq[string]]
|
||||
rowCount*: int
|
||||
affectedRows*: int
|
||||
@@ -277,6 +282,7 @@ proc wireValueToString*(wv: WireValue): string =
|
||||
of fkArray: return "<array:" & $wv.arrayVal.len & ">"
|
||||
of fkObject: return "<object:" & $wv.objVal.len & ">"
|
||||
of fkVector: return "<vector:" & $wv.vecVal.len & ">"
|
||||
of fkJson: return wv.jsonVal
|
||||
|
||||
proc readQueryResponse(client: BaraClient): Future[QueryResult] {.async.} =
|
||||
let headerData = await client.socket.recv(12)
|
||||
@@ -308,6 +314,11 @@ proc readQueryResponse(client: BaraClient): Future[QueryResult] {.async.} =
|
||||
for i in 0..<colCount:
|
||||
cols.add(readString(payload, dpos))
|
||||
result.columns = cols
|
||||
var colTypes: seq[string] = @[]
|
||||
for i in 0..<colCount:
|
||||
colTypes.add($FieldKind(payload[dpos]))
|
||||
inc dpos
|
||||
result.columnTypes = colTypes
|
||||
let rowCount = int(readUint32(payload, dpos))
|
||||
for r in 0..<rowCount:
|
||||
var row: seq[string] = @[]
|
||||
|
||||
@@ -37,6 +37,7 @@ class FieldKind:
|
||||
ARRAY = 0x0A
|
||||
OBJECT = 0x0B
|
||||
VECTOR = 0x0C
|
||||
JSON = 0x0D
|
||||
|
||||
|
||||
class MsgKind:
|
||||
@@ -88,6 +89,7 @@ class WireValue:
|
||||
class QueryResult:
|
||||
def __init__(self):
|
||||
self.columns: list[str] = []
|
||||
self.column_types: list[str] = []
|
||||
self.rows: list[list[Any]] = []
|
||||
self.row_count: int = 0
|
||||
self.affected_rows: int = 0
|
||||
@@ -176,7 +178,12 @@ class Client:
|
||||
val = self._deserialize_value(data, pos)
|
||||
row.append(val)
|
||||
rows.append(row)
|
||||
col_types = []
|
||||
for _ in range(col_count):
|
||||
col_types.append(hex(data[pos[0]]))
|
||||
pos[0] += 1
|
||||
result.columns = cols
|
||||
result.column_types = col_types
|
||||
result.rows = rows
|
||||
result.row_count = row_count
|
||||
# Read following COMPLETE message
|
||||
@@ -281,6 +288,8 @@ class Client:
|
||||
vec.append(struct.unpack(">f", data[pos[0]:pos[0]+4])[0])
|
||||
pos[0] += 4
|
||||
return vec
|
||||
elif kind == FieldKind.JSON:
|
||||
return self._read_string(data, pos)
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
+15
-3
@@ -39,6 +39,7 @@ const FK_BYTES: u8 = 0x09;
|
||||
const FK_ARRAY: u8 = 0x0A;
|
||||
const FK_OBJECT: u8 = 0x0B;
|
||||
const FK_VECTOR: u8 = 0x0C;
|
||||
const FK_JSON: u8 = 0x0D;
|
||||
|
||||
/// Connection configuration
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -66,6 +67,7 @@ impl Default for Config {
|
||||
#[derive(Debug)]
|
||||
pub struct QueryResult {
|
||||
columns: Vec<String>,
|
||||
column_types: Vec<String>,
|
||||
rows: Vec<HashMap<String, String>>,
|
||||
affected_rows: usize,
|
||||
}
|
||||
@@ -75,6 +77,10 @@ impl QueryResult {
|
||||
&self.columns
|
||||
}
|
||||
|
||||
pub fn column_types(&self) -> &[String] {
|
||||
&self.column_types
|
||||
}
|
||||
|
||||
pub fn rows(&self) -> &[HashMap<String, String>] {
|
||||
&self.rows
|
||||
}
|
||||
@@ -158,7 +164,7 @@ impl Client {
|
||||
}
|
||||
|
||||
match kind {
|
||||
MK_READY => Ok(QueryResult { columns: vec![], rows: vec![], affected_rows: 0 }),
|
||||
MK_READY => Ok(QueryResult { columns: vec![], column_types: vec![], rows: vec![], affected_rows: 0 }),
|
||||
MK_DATA => {
|
||||
let mut pos = 0usize;
|
||||
let col_count = read_u32(&payload, &mut pos) as usize;
|
||||
@@ -166,6 +172,11 @@ impl Client {
|
||||
for _ in 0..col_count {
|
||||
columns.push(read_string(&payload, &mut pos));
|
||||
}
|
||||
let mut col_types = Vec::with_capacity(col_count);
|
||||
for _ in 0..col_count {
|
||||
col_types.push(format!("{}", payload[pos]));
|
||||
pos += 1;
|
||||
}
|
||||
let row_count = read_u32(&payload, &mut pos) as usize;
|
||||
let mut rows = Vec::with_capacity(row_count);
|
||||
for _ in 0..row_count {
|
||||
@@ -188,13 +199,13 @@ impl Client {
|
||||
let affected = if comp_kind == MK_COMPLETE && comp_payload.len() >= 4 {
|
||||
u32::from_be_bytes([comp_payload[0], comp_payload[1], comp_payload[2], comp_payload[3]]) as usize
|
||||
} else { 0 };
|
||||
Ok(QueryResult { columns, rows, affected_rows: affected })
|
||||
Ok(QueryResult { columns, column_types: col_types, rows, affected_rows: affected })
|
||||
}
|
||||
MK_COMPLETE => {
|
||||
let affected = if payload.len() >= 4 {
|
||||
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize
|
||||
} else { 0 };
|
||||
Ok(QueryResult { columns: vec![], rows: vec![], affected_rows: affected })
|
||||
Ok(QueryResult { columns: vec![], column_types: vec![], rows: vec![], affected_rows: affected })
|
||||
}
|
||||
MK_ERROR => Err("Query error".into()),
|
||||
_ => Err(format!("Unknown response kind: {}", kind).into()),
|
||||
@@ -433,6 +444,7 @@ fn read_value(data: &[u8], pos: &mut usize) -> String {
|
||||
*pos += (dim * 4) as usize;
|
||||
format!("<vector:{}>", dim)
|
||||
}
|
||||
FK_JSON => read_string(data, pos),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user