duckql

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2025 License: BSD-3-Clause-Clear Imports: 16 Imported by: 0

README

platypus

DuckQL

Go

If it looks like a table, and quacks like SQL...

TL;DR: Allow LLMs to interface (safely) with arbitrary data via SQLite syntax

Background

LLMs are great at writing code, including SQL, to accomplish tasks. Using SQL as a data description and querying format allows a perfect blend of flexibility to service novel requests, while still being able to enforce "bounds" on the LLM.

There are problems with opening up an existing API or app to this kind of interface though:

  1. LLMs are susceptible to prompt injection, so if you're allowing 1:1 access to a database, you could run into a data-loss or data-injection scenario.
  2. You may need to restrict access to a slice of data (accounts matching some ID, for example)
  3. You may not have direct access to a database, but perhaps you have some rest apis, and want the power of SQL

This project is an attempt to solve the above generally in a way that is easy to "glue in" to existing APIs or data layers.

How it works

DuckQL operates as the glue between a BackingStore interface and raw SQLite queries. Depending on the kind of data you are accessing, you might choose to use a different type of backing store. Below, we can demonstrate the concept with a SliceFilter, which allows querying a slice of structs as if it were a SQLite database.

First, point duckql at your "schema" structs, and export the resulting DDL to your llm of choice:

type User struct {
    ID           string
    Email        string
    Name         string `ddl:"comment='First and last name of the user (space separated)'"`
    PasswordHash []byte `ddl:"-"`
}

...

s := duckql.Initialize(&User{})
s.SetPermissions(duckql.AllowSelectStatements)

ddl := s.DDL()
// Should result in:
//
// CREATE TABLE users
// (
//   id TEXT,
//   email TEXT,
//   name TEXT, -- First and last name of the user (space separated)
// )

In the above snippet, we call SetPermissions() to restrict which types of SQL queries are allowed. Fields which should not be accessible by an LLM can be marked as private with a 'ddl' tag value of "-". Comments can be set via the comment field. These are purely to help the LLM understand the schema.

Construct your prompt with the DDL to explain to the model how it can query information, and have it write a query. The next step is running the query against your data. We initialize our SliceFilter backing store with some hardcoded data, which we can then execute against:

...

s.SetBacking(duckql.NewSliceFilter(
    s, []any{
        &User{
            ID: "a",
            Email: "a@gmail.com",
            Name: "Cindy Lou",
        },
        &User{
            ID: "b",
            Email: "b@aol.com",
            Name: "Bob Bert",
        },
    }
))

result, err := s.Execute("select name from users where email like '%aol.com")
if err != nil {
    panic(err)
}

fmt.Println(result.String())

// Bob Bert

In the above, duckql will do the following:

  1. Check that users is a valid table that it knows about
  2. Check that the "name" field is valid on the users table
  3. Match the where clause
  4. Only copy selected fields into return value

Installing

go get github.com/dburkart/duckql

Examples

You can find more examples in the ./examples directory. Examples so far:

  • ./examples/slice: Uses a SliceFilter backing store to demonstrate using DuckQL to "emulate" a SQLite database using preset data.
  • ./examples/sqlite: Uses the SQLite backing store to demonstrate how to use DuckQL as a "permission" layer on top of a SQLite database.

Documentation

Index

Constants

View Source
const (
	AllowSelectStatements = 1 << iota
	AllowInsertStatements
	AllowUpdateStatements
	AllowDeleteStatements
)

Variables

This section is empty.

Functions

func SheetColumnToIndex

func SheetColumnToIndex(col string) int

SheetColumnToIndex converts a Google Sheets column name into its index ('A' -> 0, 'AA' -> 26)

Types

type AggregateFunction

type AggregateFunction func(*AggregateFunctionColumn, ResultRows) ResultRows

type AggregateFunctionColumn

type AggregateFunctionColumn struct {
	UnderlyingColumn string
	ResultPosition   int
	Function         AggregateFunction
}

func (*AggregateFunctionColumn) Call

type BackingStore

type BackingStore interface {
	sql.Visitor
	Rows() ResultRows
}

type ColumnMapping

type ColumnMapping struct {
	Name       string
	GoField    string
	SQLType    string
	SQLComment string
	Tag        reflect.StructTag
	Type       reflect.Type
}

type IntermediateTable

type IntermediateTable struct {
	Source  *Table
	Aliases map[string]string
	Columns []string
	Rows    ResultRows
}

func NewIntermediateTable

func NewIntermediateTable() *IntermediateTable

func (*IntermediateTable) Filter

type IntermediateVisitor

type IntermediateVisitor interface {
	sql.Visitor
	Result() *IntermediateTable
}

type JoinVisitor

type JoinVisitor struct {
	F          *QueryExecutor
	JoinResult *IntermediateTable
	Sources    []*IntermediateTable
	Previous   IntermediateVisitor
}

func (*JoinVisitor) Result

func (j *JoinVisitor) Result() *IntermediateTable

func (*JoinVisitor) Visit

func (j *JoinVisitor) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

func (*JoinVisitor) VisitEnd

func (j *JoinVisitor) VisitEnd(n sql.Node) (sql.Node, error)

type QualifiedTableVisitor

type QualifiedTableVisitor struct {
	F     *QueryExecutor
	Table *IntermediateTable
}

func (*QualifiedTableVisitor) Result

func (*QualifiedTableVisitor) Visit

func (*QualifiedTableVisitor) VisitEnd

func (i *QualifiedTableVisitor) VisitEnd(n sql.Node) (sql.Node, error)

type QueryExecutor

type QueryExecutor struct {
	FillIntermediate func(table *IntermediateTable)
	// contains filtered or unexported fields
}

func NewQueryExecutor

func NewQueryExecutor(s *SQLizer, f func(table *IntermediateTable)) *QueryExecutor

func (*QueryExecutor) Filter

func (q *QueryExecutor) Filter() sql.Node

func (*QueryExecutor) Rows

func (q *QueryExecutor) Rows() ResultRows

func (*QueryExecutor) Visit

func (q *QueryExecutor) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

func (*QueryExecutor) VisitEnd

func (q *QueryExecutor) VisitEnd(n sql.Node) (sql.Node, error)

type RESTBacking

type RESTBacking struct {
	// contains filtered or unexported fields
}

func NewRESTBacking

func NewRESTBacking(s *SQLizer) *RESTBacking

func (*RESTBacking) FillIntermediate

func (r *RESTBacking) FillIntermediate(intermediate *IntermediateTable)

func (*RESTBacking) Get

func (r *RESTBacking) Get(s any, options RESTOptions, handler func(*http.Response) (any, error)) error

func (*RESTBacking) Rows

func (r *RESTBacking) Rows() ResultRows

func (*RESTBacking) Visit

func (r *RESTBacking) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

func (*RESTBacking) VisitEnd

func (r *RESTBacking) VisitEnd(n sql.Node) (sql.Node, error)

type RESTOptions

type RESTOptions struct {
	Url    string
	Header http.Header
}

type ResultRow

type ResultRow []ResultValue

func (*ResultRow) String

func (r *ResultRow) String() string

type ResultRows

type ResultRows []ResultRow

func (*ResultRows) String

func (r *ResultRows) String() string

type ResultValue

type ResultValue struct {
	Name  string
	Value reflect.Value
}

type SQLiteBacking

type SQLiteBacking struct {
	// contains filtered or unexported fields
}

func NewSQLiteBacking

func NewSQLiteBacking(db *gosql.DB, s *SQLizer) *SQLiteBacking

New creates a new SQLiteBacking with the given SQLite database connection

func (*SQLiteBacking) Error

func (s *SQLiteBacking) Error() error

Error returns the last error encountered during query execution

func (*SQLiteBacking) Rows

func (s *SQLiteBacking) Rows() ResultRows

Rows implements duckql.BackingStore

func (*SQLiteBacking) Visit

func (s *SQLiteBacking) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

Visit implements sql.Visitor

func (*SQLiteBacking) VisitEnd

func (s *SQLiteBacking) VisitEnd(n sql.Node) (sql.Node, error)

VisitEnd implements sql.Visitor

type SQLizer

type SQLizer struct {
	Tables      map[string]*Table
	Permissions uint
	Backing     BackingStore
}

func Initialize

func Initialize(structs ...any) *SQLizer

func (*SQLizer) DDL

func (s *SQLizer) DDL() string

func (*SQLizer) Execute

func (s *SQLizer) Execute(statement string) (ResultRows, error)

func (*SQLizer) Matches

func (s *SQLizer) Matches(filter sql.Node, data any) bool

func (*SQLizer) SetBacking

func (s *SQLizer) SetBacking(backing BackingStore)

func (*SQLizer) SetPermissions

func (s *SQLizer) SetPermissions(permissions uint)

func (*SQLizer) TableForData

func (s *SQLizer) TableForData(data any) *Table

func (*SQLizer) TypeForData

func (s *SQLizer) TypeForData(data any) reflect.Type

type SheetsBacking

type SheetsBacking struct {
	// contains filtered or unexported fields
}

func NewSheetsBacking

func NewSheetsBacking(s *SQLizer, options *SheetsOptions) *SheetsBacking

func (*SheetsBacking) ComputeRangeString

func (s *SheetsBacking) ComputeRangeString(colStart string, rowStart int, colEnd string, endRow int) string

func (*SheetsBacking) FillIntermediate

func (s *SheetsBacking) FillIntermediate(intermediate *IntermediateTable)

func (*SheetsBacking) Rows

func (s *SheetsBacking) Rows() ResultRows

func (*SheetsBacking) Visit

func (s *SheetsBacking) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

func (*SheetsBacking) VisitEnd

func (s *SheetsBacking) VisitEnd(n sql.Node) (sql.Node, error)

type SheetsOptions

type SheetsOptions struct {
	Service      *sheets.Service
	SheetId      string
	SheetName    *string
	IDColumn     string
	DataRowStart int
}

type SliceFilter

type SliceFilter struct {
	// contains filtered or unexported fields
}

func NewSliceFilter

func NewSliceFilter(s *SQLizer, data []any) *SliceFilter

func (*SliceFilter) FillIntermediate

func (f *SliceFilter) FillIntermediate(table *IntermediateTable)

func (*SliceFilter) Rows

func (f *SliceFilter) Rows() ResultRows

func (*SliceFilter) Visit

func (f *SliceFilter) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

func (*SliceFilter) VisitEnd

func (f *SliceFilter) VisitEnd(n sql.Node) (sql.Node, error)

type Table

type Table struct {
	Name           string
	StructName     string
	Columns        []string
	ColumnMappings map[string]ColumnMapping
	ForeignKeys    map[string]*Table
}

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

func (*Validator) Visit

func (v *Validator) Visit(n sql.Node) (sql.Visitor, sql.Node, error)

func (*Validator) VisitEnd

func (v *Validator) VisitEnd(n sql.Node) (sql.Node, error)

Directories

Path Synopsis
examples
sheets command
slice command
sqlite command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL