Feat v0.5.1 chat core library + unicode security gate + cluster registry design
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m31s
CI/CD / test-sqlite (pull_request) Successful in 2m45s
CI/CD / build-and-deploy (pull_request) Successful in 1m34s
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m31s
CI/CD / test-sqlite (pull_request) Successful in 2m45s
CI/CD / build-and-deploy (pull_request) Successful in 1m34s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
//
|
||||
// Starlark API:
|
||||
//
|
||||
// rows = db.query("logs", filters={"user_id": "abc"}, order="created_at", limit=50)
|
||||
// rows = db.query("logs", filters={"user_id": "abc"}, order="created_at", limit=50, before={"created_at": "2026-01-01"}, after={"count": 5})
|
||||
// row = db.insert("logs", {"message": "hello"})
|
||||
// ok = db.update("logs", row_id, {"message": "updated"})
|
||||
// ok = db.delete("logs", row_id)
|
||||
@@ -221,19 +221,64 @@ func goToStarlark(v any) (starlark.Value, error) {
|
||||
|
||||
// ── Builtins ─────────────────────────────────
|
||||
|
||||
// dbQuery implements db.query(table, filters=None, order=None, limit=100).
|
||||
// starlarkRangeToSQL converts a Starlark dict of {col: val} into range
|
||||
// comparison clauses (e.g. col < $N or col > $N). op must be "<" or ">".
|
||||
func (cfg DBModuleConfig) starlarkRangeToSQL(rangeVal starlark.Value, op string, startIdx int) ([]string, []any, error) {
|
||||
if rangeVal == starlark.None || rangeVal == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
d, ok := rangeVal.(*starlark.Dict)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("db: range param must be a dict, got %s", rangeVal.Type())
|
||||
}
|
||||
if d.Len() == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var parts []string
|
||||
var args []any
|
||||
idx := startIdx
|
||||
|
||||
for _, item := range d.Items() {
|
||||
col, ok := item[0].(starlark.String)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("db: range key must be a string, got %s", item[0].Type())
|
||||
}
|
||||
colStr := string(col)
|
||||
if strings.ContainsAny(colStr, " \t\n\"';-") {
|
||||
return nil, nil, fmt.Errorf("db: invalid column name %q", colStr)
|
||||
}
|
||||
|
||||
val, err := starlarkToGoValue(item[1])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("db: range value for %q: %w", colStr, err)
|
||||
}
|
||||
|
||||
parts = append(parts, fmt.Sprintf("%s %s %s", colStr, op, cfg.ph(idx)))
|
||||
args = append(args, val)
|
||||
idx++
|
||||
}
|
||||
|
||||
return parts, args, nil
|
||||
}
|
||||
|
||||
// dbQuery implements db.query(table, filters=None, order=None, limit=100, before=None, after=None).
|
||||
func dbQuery(ctx context.Context, cfg DBModuleConfig) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
|
||||
return func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
var table string
|
||||
var filters starlark.Value = starlark.None
|
||||
var order starlark.Value = starlark.None
|
||||
var limit starlark.Int = starlark.MakeInt(100)
|
||||
var before starlark.Value = starlark.None
|
||||
var after starlark.Value = starlark.None
|
||||
|
||||
if err := starlark.UnpackArgs(b.Name(), args, kwargs,
|
||||
"table", &table,
|
||||
"filters?", &filters,
|
||||
"order?", &order,
|
||||
"limit?", &limit,
|
||||
"before?", &before,
|
||||
"after?", &after,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -248,14 +293,41 @@ func dbQuery(ctx context.Context, cfg DBModuleConfig) func(*starlark.Thread, *st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build range clauses (before → <, after → >)
|
||||
beforeParts, beforeArgs, err := cfg.starlarkRangeToSQL(before, "<", len(whereArgs)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
afterParts, afterArgs, err := cfg.starlarkRangeToSQL(after, ">", len(whereArgs)+len(beforeArgs)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Merge all WHERE conditions
|
||||
var allParts []string
|
||||
var allArgs []any
|
||||
|
||||
// Extract equality parts from whereClause
|
||||
if whereClause != "" {
|
||||
// whereClause is "WHERE x = $1 AND y = $2"; strip the "WHERE " prefix
|
||||
allParts = append(allParts, strings.TrimPrefix(whereClause, "WHERE "))
|
||||
allArgs = append(allArgs, whereArgs...)
|
||||
} else {
|
||||
allArgs = append(allArgs, whereArgs...)
|
||||
}
|
||||
allParts = append(allParts, beforeParts...)
|
||||
allArgs = append(allArgs, beforeArgs...)
|
||||
allParts = append(allParts, afterParts...)
|
||||
allArgs = append(allArgs, afterArgs...)
|
||||
|
||||
lim, ok := limit.Int64()
|
||||
if !ok || lim < 1 || lim > 1000 {
|
||||
lim = 100
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT * FROM %s", physTable)
|
||||
if whereClause != "" {
|
||||
query += " " + whereClause
|
||||
if len(allParts) > 0 {
|
||||
query += " WHERE " + strings.Join(allParts, " AND ")
|
||||
}
|
||||
|
||||
if order != starlark.None {
|
||||
@@ -275,11 +347,11 @@ func dbQuery(ctx context.Context, cfg DBModuleConfig) func(*starlark.Thread, *st
|
||||
query += fmt.Sprintf(" ORDER BY %s %s", colStr, dir)
|
||||
}
|
||||
|
||||
limitPH := cfg.ph(len(whereArgs) + 1)
|
||||
limitPH := cfg.ph(len(allArgs) + 1)
|
||||
query += " LIMIT " + limitPH
|
||||
whereArgs = append(whereArgs, lim)
|
||||
allArgs = append(allArgs, lim)
|
||||
|
||||
rows, err := cfg.DB.QueryContext(ctx, query, whereArgs...)
|
||||
rows, err := cfg.DB.QueryContext(ctx, query, allArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db.query: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user