src/sydra/query/translator.zig
Purpose
Provides a string-based SQL → sydraQL translation layer for the PostgreSQL compatibility surface.
This is not a full SQL parser; it uses case-insensitive substring searches and simple parenthesis matching.
The main consumer is the pgwire server (src/sydra/compat/wire/server.zig), which translates SQL from Postgres clients into sydraQL before running the normal sydraQL pipeline.
See also
- pgwire server
- Compatibility: SQLSTATE and Compatibility: translation log
- Compatibility: translator fixtures loader
- Query pipeline overview
Definition index (public)
pub const Result = union(enum)
success: Successfailure: Failure
pub const Success
sydraql: []const u8– allocator-owned string; callers must free it.
pub const Failure
sqlstate: []const u8– borrowed SQLSTATE string (fromcompat.sqlstate)message: []const u8– borrowed default message for that SQLSTATE (fromcompat.sqlstate)
pub fn translate(alloc: std.mem.Allocator, sql: []const u8) !Result
Memory / ownership:
- On
.success,result.success.sydraqlis allocated withallocand must be freed by the caller. - On
.failure, no allocation is returned (the SQLSTATE/message are borrowed constants).
Metrics/logging:
- The translator measures
duration_nswithstd.time.nanoTimestamp(). - It always calls
compat.clog.global().record(...):- success:
translated = sydraql,fell_back = false - fallback:
translated = "",fell_back = true
- success:
used_cacheis alwaysfalse(there is no cache layer in this module today).
Supported patterns (as implemented)
The translator operates on trimmed = trimRight(trim(sql, " \\t\\r\\n"), " \\t\\r\\n;") and is intentionally conservative: if it can’t confidently translate a shape, it returns feature_not_supported.
SELECT
Special case:
SELECT 1(case-insensitive exact match) →select 1- Trailing semicolons are ignored (
SELECT 1;→select 1)
General shape:
SELECT <cols> FROM <table> [WHERE <cond>]
Rules:
- Requires the substring
" FROM "(case-insensitive search). Joins, subqueries, etc. are not recognized. - Column list is split on commas and trimmed; empty column entries are skipped.
- Requires at least one non-empty column.
- Trailing semicolons are trimmed from the tail (
trim(..., " \\t\\r\\n;")).
Output shape:
select <col1>,<col2>,... from <table> [where <cond>]
Note: commas are emitted without a following space.
INSERT
Shape:
INSERT INTO <table> [(<columns>)] VALUES (<values>)
Rules:
<table>is scanned until whitespace or(.- Optional
(<columns>)is captured using a raw parenthesis match (seefindMatchingParen). - Requires
VALUESkeyword and a parenthesized values list. RETURNINGis not supported yet; anyRETURNINGclause returnsfeature_not_supported.
Output shape:
insert into <table> [(<columns>)] values (<values>)
UPDATE
UPDATE is not supported by the translator yet; any UPDATE returns feature_not_supported (SQLSTATE 0A000).
DELETE
Shape:
DELETE FROM <table> [WHERE <cond>]
Rules:
RETURNINGis not supported; if present, the translator returnsfeature_not_supported.- Optional
WHEREis split on" WHERE "(case-insensitive).
Output shape:
delete from <table> [where <cond>]
Fallback behavior
If no rule matches, translate returns:
Result.failurewith payload fromcompat.sqlstate.buildPayload(.feature_not_supported, null, null, null)- records the fallback via
compat.clog.global().record(trimmed, "", false, true, duration_ns)
Important internal helpers (non-public)
These helpers are not pub, but they define what “supported” means:
startsWithCaseInsensitive(text, prefix) bool– ASCII-only case-insensitive prefix matchfindCaseInsensitive(haystack, needle) ?usize– first occurrence, ASCII-onlyfindLastCaseInsensitive(haystack, needle) ?usize– last occurrence, ASCII-onlyfindMatchingParen(text, open_index) ?usize– balances(/)with a depth counter- does not account for quotes/strings, so parentheses in string literals can confuse it
Tests
The inline test test "translator fixtures" loads JSONL fixtures from tests/translator/cases.jsonl and asserts:
- expected translation strings for
.successcases - expected SQLSTATE codes for
.failurecases - global compat stats counters match fixture expectations
Code excerpt
pub fn translate(alloc: std.mem.Allocator, sql: []const u8) !Result {
const trimmed_input = std.mem.trim(u8, sql, " \t\r\n");
const trimmed = std.mem.trimRight(u8, trimmed_input, " \t\r\n;");
const start = std.time.nanoTimestamp();
if (std.ascii.eqlIgnoreCase(trimmed, "SELECT 1")) {
const out = try alloc.dupe(u8, "select 1");
const duration = std.time.nanoTimestamp() - start;
const duration_ns: u64 = @intCast(@max(duration, @as(i128, 0)));
compat.clog.global().record(trimmed, out, false, false, duration_ns);
return Result{ .success = .{ .sydraql = out } };
}
if (startsWithCaseInsensitive(trimmed, "SELECT ")) {
if (findCaseInsensitive(trimmed, " FROM ")) |from_idx| {
const cols_raw = std.mem.trim(u8, trimmed["SELECT ".len..from_idx], " \t\r\n");
const remainder = std.mem.trim(u8, trimmed[from_idx + " FROM ".len ..], " \t\r\n;");
if (cols_raw.len != 0 and remainder.len != 0) {
var table_part = remainder;
var where_part: ?[]const u8 = null;
if (findCaseInsensitive(remainder, " WHERE ")) |where_idx| {
table_part = std.mem.trim(u8, remainder[0..where_idx], " \t\r\n");
const cond_slice = std.mem.trim(u8, remainder[where_idx + " WHERE ".len ..], " \t\r\n;");
if (cond_slice.len != 0) where_part = cond_slice;
}
if (table_part.len != 0) {
var builder = std.array_list.Managed(u8).init(alloc);
defer builder.deinit();
try builder.appendSlice("select ");
var col_iter = std.mem.splitScalar(u8, cols_raw, ',');
var first = true;
while (col_iter.next()) |raw| {
const trimmed_col = std.mem.trim(u8, raw, " \t\r\n");
if (trimmed_col.len == 0) continue;
if (!first) try builder.appendSlice(",");
first = false;
try builder.appendSlice(trimmed_col);
}
if (!first) {
try builder.appendSlice(" from ");
try builder.appendSlice(table_part);
if (where_part) |cond| {
try builder.appendSlice(" where ");
try builder.appendSlice(cond);
}
const sydra_str = try builder.toOwnedSlice();
const duration = std.time.nanoTimestamp() - start;
const duration_ns: u64 = @intCast(@max(duration, @as(i128, 0)));
compat.clog.global().record(trimmed, sydra_str, false, false, duration_ns);
return Result{ .success = .{ .sydraql = sydra_str } };
}
}
}
}
}
// ... INSERT / UPDATE / DELETE cases
const payload = compat.sqlstate.buildPayload(.feature_not_supported, null, null, null);
const duration = std.time.nanoTimestamp() - start;
const duration_ns: u64 = @intCast(@max(duration, @as(i128, 0)));
compat.clog.global().record(trimmed, "", false, true, duration_ns);
return Result{ .failure = .{ .sqlstate = payload.sqlstate, .message = payload.message } };
}