Initial vibecoded proof of concept
This commit is contained in:
parent
74812459af
commit
461318a656
61 changed files with 13306 additions and 0 deletions
412
lua/notex/query/parser.lua
Normal file
412
lua/notex/query/parser.lua
Normal file
|
@ -0,0 +1,412 @@
|
|||
-- Query syntax parser module
|
||||
local M = {}
|
||||
|
||||
local utils = require('notex.utils')
|
||||
|
||||
-- Parse query string into structured object
|
||||
function M.parse(query_string)
|
||||
local result = {
|
||||
filters = {},
|
||||
conditions = nil,
|
||||
order_by = nil,
|
||||
group_by = nil,
|
||||
limit = nil,
|
||||
raw_query = query_string,
|
||||
parse_errors = {}
|
||||
}
|
||||
|
||||
if not query_string or query_string == "" then
|
||||
table.insert(result.parse_errors, "Empty query string")
|
||||
return result
|
||||
end
|
||||
|
||||
-- Extract query block from markdown
|
||||
local query_content = M.extract_query_block(query_string)
|
||||
if not query_content then
|
||||
table.insert(result.parse_errors, "No valid query block found")
|
||||
return result
|
||||
end
|
||||
|
||||
-- Parse query lines
|
||||
local lines = M.split_lines(query_content)
|
||||
local current_section = "filters" -- filters, where, order_by, group_by, limit
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
line = line:trim()
|
||||
if line == "" or line:match("^%s*%-%-%-") then
|
||||
-- Skip empty lines and comments
|
||||
continue
|
||||
end
|
||||
|
||||
-- Detect section changes
|
||||
local section = M.detect_section(line)
|
||||
if section then
|
||||
current_section = section
|
||||
continue
|
||||
end
|
||||
|
||||
-- Parse based on current section
|
||||
if current_section == "filters" then
|
||||
M.parse_filter_line(line, result)
|
||||
elseif current_section == "where" then
|
||||
M.parse_condition_line(line, result)
|
||||
elseif current_section == "order_by" then
|
||||
M.parse_order_by_line(line, result)
|
||||
elseif current_section == "group_by" then
|
||||
M.parse_group_by_line(line, result)
|
||||
elseif current_section == "limit" then
|
||||
M.parse_limit_line(line, result)
|
||||
end
|
||||
end
|
||||
|
||||
-- Validate parsed query
|
||||
M.validate_parsed_query(result)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Extract query block from markdown content
|
||||
function M.extract_query_block(content)
|
||||
-- Match ```notex-query blocks
|
||||
local start_pos, end_pos, query_content = content:match("```notex%-query%s*\n(.*)\n```")
|
||||
if query_content then
|
||||
return query_content
|
||||
end
|
||||
|
||||
-- Match inline query format
|
||||
local inline_query = content:match("```notex%-query%s*\n(.*)")
|
||||
if inline_query then
|
||||
return inline_query
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Split content into lines
|
||||
function M.split_lines(content)
|
||||
local lines = {}
|
||||
for line in content:gmatch("[^\r\n]+") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
-- Detect query section
|
||||
function M.detect_section(line)
|
||||
local upper_line = line:upper()
|
||||
|
||||
if upper_line:match("^FROM%s+") then
|
||||
return "filters"
|
||||
elseif upper_line:match("^WHERE%s+") then
|
||||
return "where"
|
||||
elseif upper_line:match("^ORDER%s+BY%s+") then
|
||||
return "order_by"
|
||||
elseif upper_line:match("^GROUP%s+BY%s+") then
|
||||
return "group_by"
|
||||
elseif upper_line:match("^LIMIT%s+") then
|
||||
return "limit"
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse filter line (FROM clause or direct property filters)
|
||||
function M.parse_filter_line(line, result)
|
||||
-- Handle FROM clause
|
||||
local from_match = line:match("^FROM%s+(.+)")
|
||||
if from_match then
|
||||
M.parse_property_filters(from_match, result)
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle direct property filters
|
||||
M.parse_property_filters(line, result)
|
||||
end
|
||||
|
||||
-- Parse property filters
|
||||
function M.parse_property_filters(filter_string, result)
|
||||
-- Parse YAML-style property filters
|
||||
local yaml_filters = M.parse_yaml_filters(filter_string)
|
||||
for key, value in pairs(yaml_filters) do
|
||||
result.filters[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse YAML-style filters
|
||||
function M.parse_yaml_filters(yaml_string)
|
||||
local filters = {}
|
||||
|
||||
-- Handle key: value pairs
|
||||
for key, value in yaml_string:gmatch("(%w+)%s*:%s*(.+)") do
|
||||
-- Parse quoted values
|
||||
local quoted_value = value:match('^"(.*)"$')
|
||||
if quoted_value then
|
||||
filters[key] = quoted_value
|
||||
else
|
||||
-- Parse array values
|
||||
local array_match = value:match("^%[(.*)%]$")
|
||||
if array_match then
|
||||
local array_values = {}
|
||||
for item in array_match:gsub("%s", ""):gmatch("[^,]+") do
|
||||
table.insert(array_values, item:gsub("^['\"](.*)['\"]$", "%1"))
|
||||
end
|
||||
filters[key] = array_values
|
||||
else
|
||||
filters[key] = value:trim()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return filters
|
||||
end
|
||||
|
||||
-- Parse WHERE condition line
|
||||
function M.parse_condition_line(line, result)
|
||||
local condition_string = line:match("^WHERE%s+(.+)")
|
||||
if not condition_string then
|
||||
table.insert(result.parse_errors, "Invalid WHERE clause: " .. line)
|
||||
return
|
||||
end
|
||||
|
||||
result.conditions = M.parse_conditions(condition_string)
|
||||
end
|
||||
|
||||
-- Parse conditions with logical operators
|
||||
function M.parse_conditions(condition_string)
|
||||
local conditions = {
|
||||
type = "AND",
|
||||
clauses = {}
|
||||
}
|
||||
|
||||
-- Split by AND/OR operators
|
||||
local and_parts = M.split_logical_operators(condition_string, "AND")
|
||||
local or_parts = {}
|
||||
|
||||
-- Check if this is an OR condition
|
||||
for _, part in ipairs(and_parts) do
|
||||
if part:match("OR") then
|
||||
local or_split = M.split_logical_operators(part, "OR")
|
||||
if #or_split > 1 then
|
||||
conditions.type = "OR"
|
||||
for _, or_part in ipairs(or_split) do
|
||||
table.insert(conditions.clauses, M.parse_single_condition(or_part))
|
||||
end
|
||||
return conditions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse AND conditions
|
||||
for _, and_part in ipairs(and_parts) do
|
||||
table.insert(conditions.clauses, M.parse_single_condition(and_part))
|
||||
end
|
||||
|
||||
return conditions
|
||||
end
|
||||
|
||||
-- Split by logical operator
|
||||
function M.split_logical_operators(string, operator)
|
||||
local parts = {}
|
||||
local pattern = "%s+" .. operator .. "%s+"
|
||||
|
||||
for part in string:gmatch("([^" .. pattern .. "]+)") do
|
||||
table.insert(parts, part:trim())
|
||||
end
|
||||
|
||||
return parts
|
||||
end
|
||||
|
||||
-- Parse single condition
|
||||
function M.parse_single_condition(condition_string)
|
||||
condition_string = condition_string:trim()
|
||||
|
||||
-- Handle NOT operator
|
||||
local negated = false
|
||||
if condition_string:match("^NOT%s+") then
|
||||
negated = true
|
||||
condition_string = condition_string:sub(4):trim()
|
||||
end
|
||||
|
||||
-- Parse comparison operators
|
||||
local operators = {
|
||||
{ pattern = ">=%s*", type = ">=" },
|
||||
{ pattern = "<=%s*", type = "<=" },
|
||||
{ pattern = "!=%s*", type = "!=" },
|
||||
{ pattern = ">%s*", type = ">" },
|
||||
{ pattern = "<%s*", type = "<" },
|
||||
{ pattern = "=%s*", type = "=" },
|
||||
{ pattern = "%s+CONTAINS%s+", type = "CONTAINS" },
|
||||
{ pattern = "%s+STARTS_WITH%s+", type = "STARTS_WITH" },
|
||||
{ pattern = "%s+ENDS_WITH%s+", type = "ENDS_WITH" },
|
||||
{ pattern = "%s+INCLUDES%s+", type = "INCLUDES" },
|
||||
{ pattern = "%s+BEFORE%s+", type = "BEFORE" },
|
||||
{ pattern = "%s+AFTER%s+", type = "AFTER" },
|
||||
{ pattern = "%s+WITHIN%s+", type = "WITHIN" }
|
||||
}
|
||||
|
||||
for _, op in ipairs(operators) do
|
||||
local field, value = condition_string:match("^(.-)" .. op.pattern .. "(.+)$")
|
||||
if field and value then
|
||||
return {
|
||||
type = "comparison",
|
||||
field = field:trim(),
|
||||
operator = op.type,
|
||||
value = M.parse_value(value:trim()),
|
||||
negated = negated
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- If no operator found, treat as existence check
|
||||
return {
|
||||
type = "existence",
|
||||
field = condition_string,
|
||||
negated = negated
|
||||
}
|
||||
end
|
||||
|
||||
-- Parse value (handle quotes, numbers, booleans)
|
||||
function M.parse_value(value_string)
|
||||
-- Handle quoted strings
|
||||
local quoted = value_string:match('^"(.*)"$')
|
||||
if quoted then
|
||||
return quoted
|
||||
end
|
||||
|
||||
-- Handle single quoted strings
|
||||
quoted = value_string:match("^'(.*)'$")
|
||||
if quoted then
|
||||
return quoted
|
||||
end
|
||||
|
||||
-- Handle numbers
|
||||
local number = tonumber(value_string)
|
||||
if number then
|
||||
return number
|
||||
end
|
||||
|
||||
-- Handle booleans
|
||||
local lower = value_string:lower()
|
||||
if lower == "true" then
|
||||
return true
|
||||
elseif lower == "false" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Handle date/time relative values (e.g., "7d" for 7 days)
|
||||
local time_match = value_string:match("^(%d+)([hdwmy])$")
|
||||
if time_match then
|
||||
local amount = tonumber(time_match[1])
|
||||
local unit = time_match[2]
|
||||
return { type = "relative_time", amount = amount, unit = unit }
|
||||
end
|
||||
|
||||
-- Default to string
|
||||
return value_string
|
||||
end
|
||||
|
||||
-- Parse ORDER BY line
|
||||
function M.parse_order_by_line(line, result)
|
||||
local order_string = line:match("^ORDER%s+BY%s+(.+)")
|
||||
if not order_string then
|
||||
table.insert(result.parse_errors, "Invalid ORDER BY clause: " .. line)
|
||||
return
|
||||
end
|
||||
|
||||
local field, direction = order_string:match("^(%w+)%s*(ASC|DESC)?$")
|
||||
if not field then
|
||||
table.insert(result.parse_errors, "Invalid ORDER BY format: " .. order_string)
|
||||
return
|
||||
end
|
||||
|
||||
result.order_by = {
|
||||
field = field,
|
||||
direction = direction and direction:upper() or "ASC"
|
||||
}
|
||||
end
|
||||
|
||||
-- Parse GROUP BY line
|
||||
function M.parse_group_by_line(line, result)
|
||||
local group_string = line:match("^GROUP%s+BY%s+(.+)")
|
||||
if not group_string then
|
||||
table.insert(result.parse_errors, "Invalid GROUP BY clause: " .. line)
|
||||
return
|
||||
end
|
||||
|
||||
result.group_by = group_string:trim()
|
||||
end
|
||||
|
||||
-- Parse LIMIT line
|
||||
function M.parse_limit_line(line, result)
|
||||
local limit_string = line:match("^LIMIT%s+(.+)")
|
||||
if not limit_string then
|
||||
table.insert(result.parse_errors, "Invalid LIMIT clause: " .. line)
|
||||
return
|
||||
end
|
||||
|
||||
local limit = tonumber(limit_string)
|
||||
if not limit or limit <= 0 then
|
||||
table.insert(result.parse_errors, "Invalid LIMIT value: " .. limit_string)
|
||||
return
|
||||
end
|
||||
|
||||
result.limit = limit
|
||||
end
|
||||
|
||||
-- Validate parsed query
|
||||
function M.validate_parsed_query(query)
|
||||
-- Check if we have any filters or conditions
|
||||
if next(query.filters) == nil and not query.conditions then
|
||||
table.insert(query.parse_errors, "Query must have at least one filter or condition")
|
||||
end
|
||||
|
||||
-- Validate field names (basic check)
|
||||
local valid_fields = {} -- Could be populated from schema
|
||||
for field, _ in pairs(query.filters) do
|
||||
if not M.is_valid_field_name(field) then
|
||||
table.insert(query.parse_errors, "Invalid field name: " .. field)
|
||||
end
|
||||
end
|
||||
|
||||
-- Validate conditions
|
||||
if query.conditions then
|
||||
M.validate_conditions(query.conditions, query.parse_errors)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if field name is valid
|
||||
function M.is_valid_field_name(field)
|
||||
return field:match("^[%w_%-%.]+$") ~= nil
|
||||
end
|
||||
|
||||
-- Validate conditions recursively
|
||||
function M.validate_conditions(conditions, errors)
|
||||
if conditions.type == "comparison" then
|
||||
if not M.is_valid_field_name(conditions.field) then
|
||||
table.insert(errors, "Invalid field name in condition: " .. conditions.field)
|
||||
end
|
||||
elseif conditions.type == "existence" then
|
||||
if not M.is_valid_field_name(conditions.field) then
|
||||
table.insert(errors, "Invalid field name in existence check: " .. conditions.field)
|
||||
end
|
||||
elseif conditions.clauses then
|
||||
for _, clause in ipairs(conditions.clauses) do
|
||||
M.validate_conditions(clause, errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Generate query hash for caching
|
||||
function M.generate_query_hash(query)
|
||||
local hash_input = vim.json.encode({
|
||||
filters = query.filters,
|
||||
conditions = query.conditions,
|
||||
order_by = query.order_by,
|
||||
group_by = query.group_by,
|
||||
limit = query.limit
|
||||
})
|
||||
|
||||
return utils.sha256(hash_input)
|
||||
end
|
||||
|
||||
return M
|
Loading…
Add table
Add a link
Reference in a new issue