Standard library · Database

db/sql

Read, check and type SQL without a database: a dialect-aware parser, a schema catalog and typed placeholders.

import db.sql

A query is a string, so a typo in it is found by the database, at run time, on the one code path nobody tested. This package reads the string instead: it lexes and parses SQL for one of four dialects, and when you hand it your schema it also resolves every table and column, names the ones that do not exist, and tells you the type of every placeholder and every column that comes back. Nothing here connects to a database; it is all static, so it belongs in a test or in a build step.

It is the same idea as sqlc in Go, with the schema taken from the SQL you already write: the CREATE TABLE statements in your migrations are the catalog, so there is no second file to keep in step.

Things that catch people out:

  • The dialect is not advice. ? is a placeholder in SQLite and MySQL and a plain operator in PostgreSQL, which numbers its own as $1; "name" is a quoted name everywhere except MySQL, where it is a string; AUTOINCREMENT, AUTO_INCREMENT and GENERATED ... AS IDENTITY belong to one engine each. Pass the dialect you will really run against.
  • Check only resolves names when the statement parses. A report with a syntax error in it says nothing about the columns, so fix the syntax first and run it again.
  • A table whose columns are not known, from CREATE TABLE ... AS SELECT or a view the package could not read, is left open: columns on it are never reported as missing. No news is not always good news.
  • Apply reads the DDL in order, so a view has to come after the tables it selects from, exactly as it does in the database.
  • Diagnostic messages are English in both language modes; the codes are the same, so a Persian program can match on code.
  • Placeholders hands back a Vector<str> you own: free the vector, and the strings in it, when you are done.
  • Salam lets a name contain spaces; SQL does not. SELECT family name FROM t is a column and an alias, not one name, and it is accepted without complaint - which is worth knowing when the names are Persian, since those are often two words. Quote them: SELECT "family name" FROM t.

Start here: one walk from a schema to checked queries

The six sections after this one take the package apart piece by piece. This one puts it together, in the order you would actually meet it on a project that already has a database and a pile of query strings. The program below is the whole arc, and you can run it as it stands - no server, no connection, nothing to set up.

db-sql-tutorial.salam
import db.sql
import str

mut g_schema := sql.Catalog {}

func str_of(n: int): str:
    ret str.FromInt(n as i64)
end

func step(n: int, title: str):
    println ""
    println "step " + str_of(n) + ": " + title
end

func try_query(q: str):
    mut r := sql.Check(q, g_schema)
    if r.ok and r.warnings == 0:
        println "  ok   " + q
    else:
        println "  bad  " + q
        mut i := 0
        until i < r.items.len():
            d := r.items.get(i)
            println "       " + d.code + " " + d.message
            if not str.IsEmpty(d.help):
                println "       " + d.help
            end
            i = i + 1
        end
    end
    sql.FreeReport(r)
end

func main:
    step(1, "the schema you already have")
    g_schema = sql.NewCatalog(sql.DialectOf(sql.SQLITE))
    defer sql.FreeCatalog(g_schema)
    mut ddl := sql.Apply(g_schema, """
        CREATE TABLE authors (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            country TEXT
        );
        CREATE TABLE books (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            author_id INTEGER NOT NULL,
            title TEXT NOT NULL,
            year INTEGER,
            price REAL
        );
    """)
    println "  loaded " + str_of(sql.TableCount(g_schema)) + " tables, " + str_of(sql.ColumnCount(g_schema, "books")) + " columns on books"
    sql.FreeReport(ddl)

    step(2, "the queries you already have")
    try_query("SELECT id, titles FROM books WHERE author_id = ?")
    try_query("SELECT b.title, a.nationality FROM books b JOIN authors a ON a.id = b.author_id")
    try_query("INSERT INTO books (title) VALUES (?)")
    try_query("SELECT id FROM books WHERE year = 'recent'")
    try_query("DELETE FROM books")

    step(3, "the same five, put right")
    try_query("SELECT id, title FROM books WHERE author_id = ?")
    try_query("SELECT b.title, a.country FROM books b JOIN authors a ON a.id = b.author_id")
    try_query("INSERT INTO books (author_id, title) VALUES (?, ?)")
    try_query("SELECT id FROM books WHERE year = 2024")
    try_query("DELETE FROM books WHERE id = ?")

    step(4, "what a query takes and gives back")
    mut a := sql.Analyze("SELECT books.title, books.price, authors.country FROM books JOIN authors ON authors.id = books.author_id WHERE books.year > ?", g_schema)
    println "  it is a " + sql.StmtKindName(a.kind) + " on " + a.table
    p := a.params.get(0)
    println "  takes " + p.column + " as " + sql.SalamType(p.kind, p.not_null)
    mut i := 0
    until i < a.results.len():
        c := a.results.get(i)
        println "  gives " + c.name + " as " + sql.SalamType(c.kind, not c.nullable)
        i = i + 1
    end
    sql.FreeAnalysis(a)

    step(5, "the values go in safely")
    mut args := sql.NewArgs()
    sql.AddText(args, "O'Brien")
    sql.AddInt(args, 1988 as i64)
    mut rep := sql.Report {}
    filled := sql.Fill("INSERT INTO books (title, year) VALUES (?, ?)", args, sql.DialectOf(sql.SQLITE), rep)
    println "  " + filled
    sql.FreeReport(rep)
    sql.FreeArgs(args)

    step(6, "or let it write the code for you")
    mut gen := sql.Report {}
    mut queries := sql.ParseQueries("""
-- name: BooksByAuthor :many
SELECT id, title, year FROM books WHERE author_id = ?;
""", gen)
    code := sql.Generate("store", queries, g_schema, gen)
    println code
    sql.FreeQueries(queries)
    sql.FreeReport(gen)
    ret 0
end
Output step 1: the schema you already have loaded 2 tables, 5 columns on books step 2: the queries you already have bad SELECT id, titles FROM books WHERE author_id = ? SQL311 no column named "titles" in this statement did you mean "title"? bad SELECT b.title, a.nationality FROM books b JOIN authors a ON a.id = b.author_id SQL311 "a" has no column "nationality" bad INSERT INTO books (title) VALUES (?) SQL315 "author_id" is NOT NULL with no default and this INSERT leaves it out add it to the column list and to every row bad SELECT id FROM books WHERE year = 'recent' SQL330 "year" is int and this compares it with text a number goes here without quotes bad DELETE FROM books SQL320 this DELETE has no WHERE clause and removes every row add a WHERE clause, or use TRUNCATE when that is what you mean step 3: the same five, put right ok SELECT id, title FROM books WHERE author_id = ? ok SELECT b.title, a.country FROM books b JOIN authors a ON a.id = b.author_id ok INSERT INTO books (author_id, title) VALUES (?, ?) ok SELECT id FROM books WHERE year = 2024 ok DELETE FROM books WHERE id = ? step 4: what a query takes and gives back it is a select on books takes year as option.Option<i64> gives title as str gives price as option.Option<f64> gives country as option.Option<str> step 5: the values go in safely INSERT INTO books (title, year) VALUES ('O''Brien', 1988) step 6: or let it write the code for you package store import db import db.sql import mem import option struct BooksByAuthorRow: pub id: i64 = 0 as i64 pub title: str = "" pub year: option.Option<i64> = option.None() as option.Option<i64> end func fill_query(c: dyn db.Connection, raw: str, a: sql.Args): str: mut rep := sql.Report {} out := sql.Fill(raw, a, sql.DialectNamed(c.Kind()), rep) sql.FreeReport(rep) ret out end pub func BooksByAuthor(c: dyn db.Connection, author_id: i64): Vector<BooksByAuthorRow>: mut a := sql.NewArgs() sql.AddInt(a, author_id) q := fill_query(c, "SELECT id, title, year FROM books WHERE author_id = ?", a) sql.FreeArgs(a) mut rows := Vector {} as Vector<BooksByAuthorRow> r := c.Query(q) if r.Ok(): until r.Next(): mut out := BooksByAuthorRow {} out.id = r.Int64(0) out.title = r.Text(1) if r.IsNull(2): out.year = option.None() as option.Option<i64> else: out.year = option.Some(r.Int64(2)) end rows.push(out) end r.Close() end mem.Free(q as void*) ret rows end

Step 1 is the only setup there is. You do not write a schema description for this package; you hand it the CREATE TABLE statements you already have. Point Apply at your migration files in order and the catalog is the schema your program really runs against.

Step 2 is why you came. Five queries of the kind that live in every codebase, and five different ways of being wrong: a column name remembered in the plural, a column that belongs to the other table in the join, an INSERT that leaves out a NOT NULL column with no default, a number compared against a quoted word, and a DELETE with no WHERE. Each one is found without a database, and each one names what to do about it. The last is a warning rather than an error, because sometimes clearing a table is the intent - the point is that you decide, in a review, rather than at two in the morning.

Step 3 is the same five, put right, and it is worth running your own queries through this pair of steps once before reading any further. The quickest way is the command: salam sql check src --schema=db reads the SQL out of your .salam files as well as your .sql ones.

Step 4 is where it stops being a linter. Once the names resolve, the types follow: the placeholder is an option.Option<i64> because year can be null, title is a plain str because the column is NOT NULL. That is the signature your code should have, derived from the schema instead of remembered.

Step 5 is the one that prevents the oldest bug in the book. Fill writes the values into the query with the quoting the engine wants - note what happens to the apostrophe in O'Brien - and because the query was lexed first, a ? that lives inside a string is left alone.

Step 6 is the part you can stop writing by hand. Annotate a query with a name and a kind and the generated function has the parameters and the row struct that step 4 worked out. Run it with salam sql generate queries --schema=db --output=gen/store.salam.

Two things to carry away. The schema is the DDL you already have, so nothing can drift out of step with it. And every one of these checks happens before the program runs, which is the only time a query mistake is cheap.

Reading a query without a database

Lint parses one statement, or a whole script of them separated by ;, and returns a Report. r.ok is false as soon as there is an error; ReportText renders the whole list the way a compiler would, with the line, the column, the message and a help line. Most of the dialect rules are in here: the same string is fine for SQLite and wrong for PostgreSQL, and the report says which part of it to change.

db-sql-guide-lint.salam
import db.sql

func report(title: str, query: str, d: sql.Dialect):
    mut r := sql.Lint(query, d)
    if r.ok:
        println title, "ok"
    else:
        println title, sql.ReportText(r)
    end
    sql.FreeReport(r)
end

func main:
    sqlite := sql.DialectOf(sql.SQLITE)
    mysql := sql.DialectOf(sql.MYSQL)
    postgres := sql.DialectOf(sql.POSTGRES)

    report("plain select: ", "SELECT id, name FROM users WHERE id = ?", sqlite)
    report("typo:         ", "SELECT id FROM users WHERE", sqlite)
    report("wrong marker: ", "SELECT id FROM users WHERE id = ?", postgres)
    report("mysql only:   ", "SELECT id FROM users WHERE name ILIKE 'a%'", mysql)
    report("sqlite only:  ", "CREATE TABLE t (id INT AUTO_INCREMENT)", sqlite)

    println "placeholders: ", sql.ParamCount("UPDATE users SET name = ?, age = ? WHERE id = ?", sqlite)
    ret 0
end
Outputplain select: ok typo: error[SQL105] 1:27: expected a value, found the end of the statement help: a column name, a literal, a function call or a placeholder goes here wrong marker: error[SQL201] 1:33: '?' is not a placeholder in postgres help: postgres numbers its placeholders: $1, $2 mysql only: error[SQL205] 1:33: ILIKE is postgres only help: use LOWER(x) LIKE LOWER(y), or LIKE on a case-insensitive column sqlite only: error[SQL211] 1:24: AUTO_INCREMENT is a mysql keyword help: sqlite spells it AUTOINCREMENT placeholders: 3

The compiler ships the same checks as a command, so a whole project can be checked without writing a program for it: salam sql check src --schema=db reads the .sql files under src, and the SQL string literals out of its .salam files, and reports them with the position in the Salam source. Without --dialect each Salam file is read with the engine it imports, so a tree that mixes db/sqlite and db/mysql is checked correctly with no flags at all. A query assembled from pieces - glued with + or .concat, or any literal whose only fault is that it stops in the middle - is a fragment rather than a statement, so it is counted and skipped instead of being reported as a syntax error. The count is in the summary, so nothing is checked silently; a fragment is also the reason to bind values as placeholders instead of pasting them in.

Every diagnostic carries a code. SQL0xx is lexical, SQL1xx is syntax, SQL2xx is a dialect rule, SQL3xx is a name or a count that does not match the schema. Match on the code when you want to allow one particular thing; never match on the message.

Your migrations are the schema

Apply runs DDL into a Catalog: CREATE TABLE adds a table with its columns, their types, their NOT NULL and PRIMARY KEY, ALTER TABLE adds, drops and renames, DROP removes, and CREATE VIEW records the view's columns. Feed it the same strings you give db.AddMigration and the catalog is the schema the program really has.

db-sql-guide-catalog.salam
import db.sql

mut g_schema := sql.Catalog {}

func check(query: str):
    mut r := sql.Check(query, g_schema)
    if r.ok and r.warnings == 0:
        println "ok   ", query
    else:
        println "bad  ", query
        println sql.ReportText(r)
    end
    sql.FreeReport(r)
end

func main:
    g_schema = sql.NewCatalog(sql.DialectOf(sql.SQLITE))
    defer sql.FreeCatalog(g_schema)

    mut ddl := sql.Apply(g_schema, """
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT NOT NULL,
            name TEXT
        );
        CREATE TABLE orders (
            id INTEGER PRIMARY KEY,
            user_id INTEGER NOT NULL,
            total REAL NOT NULL
        );
    """)
    println "schema loaded:", ddl.ok, " tables:", sql.TableCount(g_schema)
    sql.FreeReport(ddl)

    check("SELECT u.email, o.total FROM users u JOIN orders o ON o.user_id = u.id WHERE u.id = ?")
    check("SELECT id, mail FROM users")
    check("SELECT id FROM user")
    check("INSERT INTO users (name) VALUES (?)")
    check("DELETE FROM orders")
    ret 0
end
Outputschema loaded: true tables: 2 ok SELECT u.email, o.total FROM users u JOIN orders o ON o.user_id = u.id WHERE u.id = ? bad SELECT id, mail FROM users error[SQL311] 1:12: no column named "mail" in this statement help: did you mean "email"? bad SELECT id FROM user error[SQL317] 1:16: no table named "user" help: did you mean "users"? bad INSERT INTO users (name) VALUES (?) error[SQL315] 1:1: "email" is NOT NULL with no default and this INSERT leaves it out help: add it to the column list and to every row bad DELETE FROM orders warning[SQL320] 1:1: this DELETE has no WHERE clause and removes every row help: add a WHERE clause, or use TRUNCATE when that is what you mean

With a catalog, Check resolves names: an unknown column or table is an error with the closest real name offered, a column that two joined tables both have is an error until you qualify it, an INSERT is checked against the column list and against the columns that are NOT NULL with no default, and an UPDATE or DELETE with no WHERE is a warning, because that is almost never what was meant.

It also reads the types it has: comparing a numeric column with a quoted value that is not a number is a warning on SQLite and MySQL, where it silently never matches or silently becomes zero, and an error on PostgreSQL, which refuses it; a column in the result list of a GROUP BY query that is neither grouped nor aggregated is an error on PostgreSQL and a warning elsewhere, with the same exception those engines make - grouping by a table's whole primary key determines the rest of that table's columns, so they are allowed unqualified; and USING (col) is checked on both sides of the join. A subquery that returns the wrong number of columns for the comparison it sits in, an ORDER BY 3 when the statement returns two columns, a LIMIT that is not a number, an aggregate in WHERE rather than HAVING, the same column assigned twice in one SET, and two result columns given the same alias are all reported too.

Putting the values in safely

The three SQL drivers do not all offer prepared statements - only db/sqlite has Prepare and Bind* - so code that has to run on MySQL or PostgreSQL ends up gluing values into the SQL by hand, which is where quoting mistakes become injections. Fill does that one job properly: it lexes the query, so it knows which ? is a placeholder and which one is a character inside a string, checks that the number of values matches, and writes each value the way the dialect wants it.

db-sql-guide-fill.salam
import db.sql

func show(query: str, a: sql.Args, d: sql.Dialect):
    mut r := sql.Report {}
    out := sql.Fill(query, a, d, r)
    if r.ok:
        println out
    else:
        println sql.ReportText(r)
    end
    sql.FreeReport(r)
end

func main:
    sqlite := sql.DialectOf(sql.SQLITE)
    postgres := sql.DialectOf(sql.POSTGRES)

    mut row := sql.NewArgs()
    sql.AddText(row, "O'Brien")
    sql.AddInt(row, 41 as i64)
    sql.AddBool(row, true)
    sql.AddNull(row)
    show("INSERT INTO users (name, age, paid, note) VALUES (?, ?, ?, ?)", row, sqlite)
    show("INSERT INTO users (name, age, paid, note) VALUES ($1, $2, $3, $4)", row, postgres)
    sql.FreeArgs(row)

    mut attack := sql.NewArgs()
    sql.AddText(attack, "x' OR '1'='1")
    show("SELECT id FROM users WHERE name = ?", attack, sqlite)
    sql.FreeArgs(attack)

    mut one := sql.NewArgs()
    sql.AddInt(one, 7 as i64)
    show("SELECT id FROM notes WHERE body = '?' AND id = ?", one, sqlite)
    show("SELECT id FROM users WHERE id = ? AND org = ?", one, sqlite)
    sql.FreeArgs(one)
    ret 0
end
OutputINSERT INTO users (name, age, paid, note) VALUES ('O''Brien', 41, 1, NULL) INSERT INTO users (name, age, paid, note) VALUES ('O''Brien', 41, TRUE, NULL) SELECT id FROM users WHERE name = 'x'' OR ''1''=''1' SELECT id FROM notes WHERE body = '?' AND id = 7 error[SQL400] 1:1: this query takes 2 values but got 1 help: add or remove an argument so the two agree

A text value is wrapped in single quotes with every quote doubled, and on MySQL a backslash is doubled as well, because MySQL reads backslashes as escapes and the other two do not. A boolean is 1 or 0 on SQLite and MySQL and TRUE or FALSE on PostgreSQL; a null is NULL; numbers are written plainly. The '?' in the fourth query stays as it was, which a search-and-replace over the string would have got wrong.

This is substitution, not a prepared statement: the database still parses one complete statement, and there is no plan reuse. Where the driver can prepare and bind, as sqlite can, that is still the better tool. Where it cannot, this is the safe way to get the values in, and it is one audited function rather than a quoting rule repeated at every call site.

The type of every placeholder

Analyze does everything Check does and also hands back what the query takes and what it returns. Each Param says which column it was compared with, in which table, and the type that column has; each ResultCol says the name the column comes back under, its type, and whether it can be null. SalamType turns that into the Salam type to read it into, so a LEFT JOIN gives you option.Option<i64> where an inner join gives you i64.

db-sql-guide-analyze.salam
import db.sql

func describe(query: str, schema: sql.Catalog):
    mut a := sql.Analyze(query, schema)
    println query
    println "  statement:", sql.StmtKindName(a.kind), "on", a.table
    mut i := 0
    until i < a.params.len():
        p := a.params.get(i)
        println "  param", p.ordinal, "is", p.column, "of type", sql.SalamType(p.kind, p.not_null)
        i = i + 1
    end
    mut j := 0
    until j < a.results.len():
        c := a.results.get(j)
        println "  column", c.name, "comes back as", sql.SalamType(c.kind, not c.nullable)
        j = j + 1
    end
    sql.FreeAnalysis(a)
end

func main:
    mut schema := sql.NewCatalog(sql.DialectOf(sql.SQLITE))
    defer sql.FreeCatalog(schema)
    mut ddl := sql.Apply(schema, """
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT NOT NULL,
            name TEXT,
            age INTEGER
        );
    """)
    sql.FreeReport(ddl)

    describe("SELECT id, name FROM users WHERE email = ? AND age > ?", schema)
    describe("INSERT INTO users (email, name) VALUES (?, ?) RETURNING id", schema)
    ret 0
end
OutputSELECT id, name FROM users WHERE email = ? AND age > ? statement: select on users param 1 is email of type str param 2 is age of type option.Option<i64> column id comes back as i64 column name comes back as option.Option<str> INSERT INTO users (email, name) VALUES (?, ?) RETURNING id statement: insert on users param 1 is email of type str param 2 is name of type option.Option<str> column id comes back as i64

That is enough to generate the binding code for a query, or to assert in a test that a query still takes and returns what the code around it believes. Placeholders are numbered in the order they appear, which is the order the driver binds them; for PostgreSQL the ordinal is the number in the $n itself, and a gap in those numbers is an error of its own.

JobUse
Is this SQL well formed for my engine?Lint
How many values does it bind?ParamCount, Placeholders
Load the schemaNewCatalog, Apply
Do the tables and columns exist?Check
What does it take and return?Analyze
Which Salam type reads a column?SalamType
Put the values into the SQL safelyNewArgs, Add*, Fill
Look around the schemaTableCount, TableName, ColumnCount, ColumnAt, HasTable, HasColumn

Generating the query functions

With the schema and the analysis in place, the last step is mechanical, so the package does it: annotate a .sql file the way sqlc does and Generate writes the Salam for it - a row struct per query, a typed function per query, each one checked against the schema as it is generated.

db-sql-guide-generate.salam
import db.sql

func main:
    mut schema := sql.NewCatalog(sql.DialectOf(sql.SQLITE))
    defer sql.FreeCatalog(schema)
    mut ddl := sql.Apply(schema, """
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT NOT NULL,
            name TEXT
        );
    """)
    sql.FreeReport(ddl)

    queries := """
-- name: GetUser :one
SELECT id, email, name FROM users WHERE email = ?;

-- name: CreateUser :exec
INSERT INTO users (email, name) VALUES (?, ?);
"""
    mut r := sql.Report {}
    mut parsed := sql.ParseQueries(queries, r)
    println "queries:", parsed.len()
    code := sql.Generate("store", parsed, schema, r)
    if r.ok:
        println code
    else:
        println sql.ReportText(r)
    end
    sql.FreeQueries(parsed)
    sql.FreeReport(r)
    ret 0
end
Outputqueries: 2 package store import db import db.sql import mem import option struct GetUserRow: pub id: i64 = 0 as i64 pub email: str = "" pub name: option.Option<str> = option.None() as option.Option<str> end func fill_query(c: dyn db.Connection, raw: str, a: sql.Args): str: mut rep := sql.Report {} out := sql.Fill(raw, a, sql.DialectNamed(c.Kind()), rep) sql.FreeReport(rep) ret out end pub func GetUser(c: dyn db.Connection, email: str, ok &: bool): GetUserRow: mut a := sql.NewArgs() sql.AddText(a, email) q := fill_query(c, "SELECT id, email, name FROM users WHERE email = ?", a) sql.FreeArgs(a) mut out := GetUserRow {} ok = false r := c.Query(q) if r.Ok(): if r.Next(): out.id = r.Int64(0) out.email = r.Text(1) if r.IsNull(2): out.name = option.None() as option.Option<str> else: out.name = option.Some(r.Text(2)) end ok = true end r.Close() end mem.Free(q as void*) ret out end pub func CreateUser(c: dyn db.Connection, email: str, name: str): bool: mut a := sql.NewArgs() sql.AddText(a, email) sql.AddText(a, name) q := fill_query(c, "INSERT INTO users (email, name) VALUES (?, ?)", a) sql.FreeArgs(a) done := c.Exec(q) == 0 mem.Free(q as void*) ret done end

The kinds are :one (the first row, with an ok &: bool out parameter), :many (a Vector of rows), :exec (a bool) and :execrows (the number of rows changed). Parameters are named after the column each placeholder is compared with and typed from it; a column that can be null comes back as option.Option<T> and is read through IsNull. A query that does not resolve against the schema stops the generation with the query's name in the message, so a typo never reaches the generated file.

The command is salam sql generate queries --schema=db --output=gen/store.salam --package=store. The generated file takes a dyn db.Connection, so one set of functions serves SQLite, MySQL and PostgreSQL, and it picks each dialect's quoting from c.Kind() at run time.

Types

Param

struct Param
One placeholder, and the column it is bound against.
NameTypeDescription
markerstr = ""The placeholder as written: ?, $2, :name.
ordinalint = 0Its position, counting from 1; for $n it is the number itself.
tablestr = ""The table of the column it was compared with, when there was one.
columnstr = ""The column it was compared with, when there was one.
kindint = 7That column's TYPE_* kind, or TYPE_UNKNOWN.
not_nullbool = falseTrue when that column cannot be null.
db-sql-Methods.salam
import db.sql

func main:
    sqlite := sql.DialectOf(sql.SQLITE)
    println sqlite.name, sql.DialectNamed("postgresql").name, sql.DialectOf(sql.MARIADB).name

    mut bad := sql.Lint("SELECT id FROM t WHERE", sqlite)
    println bad.ok, bad.errors, bad.warnings, bad.statements
    println sql.DiagText(bad.items.get(0))
    sql.FreeReport(bad)

    println sql.ParamCount("SELECT a FROM t WHERE b = ? AND c = ?", sqlite)
    mut marks := sql.Placeholders("SELECT a FROM t WHERE b = ? AND c = ?", sqlite)
    println marks.len(), marks.get(0)
    marks.free()

    mut cat := sql.NewCatalog(sqlite)
    mut ddl := sql.Apply(cat, """
        CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL, age INTEGER);
    """)
    println "schema:", ddl.ok, sql.TableCount(cat), sql.TableName(cat, 0)
    sql.FreeReport(ddl)
    println sql.HasTable(cat, "users"), sql.HasColumn(cat, "users", "email"), sql.ColumnCount(cat, "users")
    col := sql.ColumnAt(cat, "users", 1)
    println col.name, col.type_name, sql.KindName(col.kind), col.not_null, col.primary

    mut miss := sql.Check("SELECT nope FROM users", cat)
    println sql.ReportText(miss)
    sql.FreeReport(miss)

    mut a := sql.Analyze("SELECT id, age FROM users WHERE email = ?", cat)
    println sql.StmtKindName(a.kind), a.table, a.params.len(), a.results.len()
    p := a.params.get(0)
    println p.marker, p.ordinal, p.table + "." + p.column, sql.SalamType(p.kind, p.not_null)
    r := a.results.get(1)
    println r.name, sql.KindName(r.kind), r.nullable
    sql.FreeAnalysis(a)

    mut args := sql.NewArgs()
    sql.AddInt(args, 3 as i64)
    sql.AddText(args, "a'b")
    sql.AddBool(args, false)
    sql.AddFloat(args, 2.5)
    sql.AddNull(args)
    println sql.ArgCount(args)
    mut err := sql.Report {}
    filled := sql.Fill("INSERT INTO t (a, b, c, d, e) VALUES (?, ?, ?, ?, ?)", args, sqlite, err)
    println filled
    sql.FreeReport(err)
    sql.FreeArgs(args)

    sql.FreeCatalog(cat)
    ret 0
end
Outputsqlite postgres mariadb false 1 0 0 error[SQL105] 1:23: expected a value, found the end of the statement help: a column name, a literal, a function call or a placeholder goes here 2 2 ? schema: true 1 users true true 3 email TEXT text true false error[SQL311] 1:8: no column named "nope" in this statement select users 1 2 ? 1 users.email str age int true 5 INSERT INTO t (a, b, c, d, e) VALUES (3, 'a''b', 0, 2.5, NULL)

ResultCol

struct ResultCol
One column of what the statement returns.
NameTypeDescription
namestr = ""The name it comes back under: the alias, or the column's own name.
tablestr = ""The table it came from, when it is a plain column.
kindint = 7Its TYPE_* kind.
nullablebool = trueTrue when it can come back null, which an outer join makes true for the whole of its side.

The example for this item is shown with the ones above: db-sql-Methods.salam

Analysis

struct Analysis
What Analyze works out about one statement.
NameTypeDescription
okbool = trueTrue when nothing is an error.
kindint = 0Which STMT_* statement it is.
tablestr = ""The table it reads from or writes to.
paramsVectorIts placeholders, in binding order.
resultsVectorThe columns it returns.
reportReportThe diagnostics, as Check would give them.
ownedVectorThe strings the analysis owns. FreeAnalysis releases them; nothing else should read it.

The example for this item is shown with the ones above: db-sql-Methods.salam

Args

struct Args
The values a query binds, in order. Build one with NewArgs and the Add* functions, and release it with FreeArgs.
NameTypeDescription
kindsVectorWhich kind each value is; one entry per value.
intsVectorThe integer values, by the same index.
floatsVectorThe floating point values, by the same index.
textsVectorThe text values, by the same index. AddText copies the string, so the caller keeps its own.
boolsVectorThe boolean values, by the same index.

The example for this item is shown with the ones above: db-sql-Methods.salam

Dialect

struct Dialect
Everything that differs between the engines: how names and strings are quoted, which placeholders exist, which clauses are supported. Build one with DialectOf or DialectNamed.
NameTypeDescription
idint = 0Which engine: SQLITE, MYSQL, MARIADB or POSTGRES.
namestr = "sqlite"The engine's name, as it appears in diagnostics.
double_quoted_stringbool = falseTrue when "x" is a string instead of a name (MySQL).
backtick_identbool = falseTrue when `x` is a name (MySQL, SQLite).
bracket_identbool = falseTrue when [x] is a name (SQLite).
hash_commentbool = falseTrue when # starts a comment (MySQL).
dollar_stringbool = falseTrue when $tag$ ... $tag$ is a string (PostgreSQL).
backslash_escapebool = falseTrue when a backslash escapes inside a string (MySQL).
qmark_parambool = falseTrue when ? is a placeholder.
numbered_parambool = falseTrue when $1 is a placeholder.
named_parambool = falseTrue when :name and @name are placeholders.
returningbool = falseTrue when RETURNING is supported.
on_conflictbool = falseTrue when ON CONFLICT is supported.
on_duplicatebool = falseTrue when ON DUPLICATE KEY UPDATE is supported.
limit_commabool = falseTrue when LIMIT start, count is supported.
ilikebool = falseTrue when ILIKE is supported.
colon_castbool = falseTrue when x::type is a cast.
autoincrement_wordstr = ""The engine's own word for an auto-incrementing key.

The example for this item is shown with the ones above: db-sql-Methods.salam

Diag

struct Diag
One diagnostic: what is wrong, where, and what to write instead.
NameTypeDescription
severityint = 0ERROR, WARNING or NOTE.
codestr = ""The code, for example SQL311. Match on this, not on the message.
messagestr = ""What is wrong, in one line.
helpstr = ""What to write instead; empty when there is nothing to suggest.
lineint = 1The line it starts on, counting from 1.
colint = 1The column it starts at, counting from 1.
offsetint = 0The byte offset into the SQL it starts at.
lengthint = 0How many bytes it covers.

The example for this item is shown with the ones above: db-sql-Methods.salam

Report

struct Report
The diagnostics of one run, in the order they were found.
NameTypeDescription
okbool = trueTrue when nothing is an error. Warnings do not clear it.
errorsint = 0How many errors there are.
warningsint = 0How many warnings there are.
statementsint = 0How many statements parsed, which is how a caller counts what it checked.
itemsVectorThe diagnostics themselves.

The example for this item is shown with the ones above: db-sql-Methods.salam

Column

struct Column
One column of the schema.
NameTypeDescription
namestr = ""Its name, as the schema spells it.
type_namestr = ""Its type, as the schema spells it.
kindint = 7Which of the TYPE_* kinds that type is.
not_nullbool = falseTrue when it cannot be null.
has_defaultbool = falseTrue when it has a default, which is what lets an INSERT leave it out.
primarybool = falseTrue when it is part of the primary key.
autobool = falseTrue when the engine fills it in.
generatedbool = falseTrue when it is computed and cannot be written.

The example for this item is shown with the ones above: db-sql-Methods.salam

Table

struct Table
One table or view of the schema.
NameTypeDescription
namestr = ""Its name.
is_viewbool = falseTrue when it is a view.
openbool = falseTrue when its columns are not known, which turns off column checking for it.
columnsVectorIts columns, in declaration order.

The example for this item is shown with the ones above: db-sql-Methods.salam

Catalog

struct Catalog
The schema a query is checked against. Build one with NewCatalog and fill it with Apply.
NameTypeDescription
dDialectThe dialect the DDL and the queries are read with.
tablesVector
The tables and views it holds.

The example for this item is shown with the ones above: db-sql-Methods.salam

Query

struct Query
One annotated query read out of a .sql file.
NameTypeDescription
namestr = ""The name from the -- name: header, used for the generated function and its row struct.
kindint = 0One of the Q_* kinds.
textstr = ""The SQL itself, with any trailing ; removed.
lineint = 1The line its header is on, so a diagnostic can point back at the file.

The example for this item is shown with the ones above: db-sql-Methods.salam

Constants and variables

STMT_UNKNOWN

const STMT_UNKNOWN: i32 = 0
A statement this package does not model.

The example for this item is shown with the ones above: db-sql-Methods.salam

STMT_SELECT

const STMT_SELECT: i32 = 1
A SELECT, a VALUES or a set operation.

The example for this item is shown with the ones above: db-sql-Methods.salam

STMT_INSERT

const STMT_INSERT: i32 = 2
An INSERT or a REPLACE.

The example for this item is shown with the ones above: db-sql-Methods.salam

STMT_UPDATE

const STMT_UPDATE: i32 = 3
An UPDATE.

The example for this item is shown with the ones above: db-sql-Methods.salam

STMT_DELETE

const STMT_DELETE: i32 = 4
A DELETE.

The example for this item is shown with the ones above: db-sql-Methods.salam

STMT_DDL

const STMT_DDL: i32 = 5
A statement that changes the schema.

The example for this item is shown with the ones above: db-sql-Methods.salam

SQLITE

const SQLITE: i32 = 0
The SQLite dialect.

The example for this item is shown with the ones above: db-sql-Methods.salam

MYSQL

const MYSQL: i32 = 1
The MySQL dialect.

The example for this item is shown with the ones above: db-sql-Methods.salam

MARIADB

const MARIADB: i32 = 2
The MariaDB dialect: MySQL plus RETURNING.

The example for this item is shown with the ones above: db-sql-Methods.salam

POSTGRES

const POSTGRES: i32 = 3
The PostgreSQL dialect.

The example for this item is shown with the ones above: db-sql-Methods.salam

ERROR

const ERROR: i32 = 0
The severity of a diagnostic that makes the statement wrong.

The example for this item is shown with the ones above: db-sql-Methods.salam

WARNING

const WARNING: i32 = 1
The severity of a diagnostic that is worth reading but does not make the statement wrong.

The example for this item is shown with the ones above: db-sql-Methods.salam

NOTE

const NOTE: i32 = 2
The severity of a remark.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_NULL

const TYPE_NULL: i32 = 0
The type of an SQL NULL. Same numbering as db's column types.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_INT

const TYPE_INT: i32 = 1
An integer column.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_FLOAT

const TYPE_FLOAT: i32 = 2
A floating point or decimal column.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_TEXT

const TYPE_TEXT: i32 = 3
A text column.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_BLOB

const TYPE_BLOB: i32 = 4
A binary column.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_BOOL

const TYPE_BOOL: i32 = 5
A boolean column.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_TIME

const TYPE_TIME: i32 = 6
A date, time or timestamp column.

The example for this item is shown with the ones above: db-sql-Methods.salam

TYPE_UNKNOWN

const TYPE_UNKNOWN: i32 = 7
A type the package could not work out.

The example for this item is shown with the ones above: db-sql-Methods.salam

Q_ONE

const Q_ONE: i32 = 0
The query returns at most one row; the generated function takes an ok &: bool and returns the row struct.

The example for this item is shown with the ones above: db-sql-Methods.salam

Q_MANY

const Q_MANY: i32 = 1
The query returns any number of rows; the generated function returns a Vector of them.

The example for this item is shown with the ones above: db-sql-Methods.salam

Q_EXEC

const Q_EXEC: i32 = 2
The query changes data and the generated function returns whether it worked.

The example for this item is shown with the ones above: db-sql-Methods.salam

Q_EXECROWS

const Q_EXECROWS: i32 = 3
Like Q_EXEC, but the generated function returns how many rows changed.

The example for this item is shown with the ones above: db-sql-Methods.salam

Functions

Check

func Check(sql: str, c: Catalog): Report
Parse the SQL and resolve it against a catalog: unknown tables and columns, ambiguous columns, INSERT arity and required columns, and the writes that have no WHERE.

The example for this item is shown with the ones above: db-sql-Methods.salam

Analyze

func Analyze(sql: str, c: Catalog): Analysis
Everything Check does, plus the type of every placeholder and of every column returned. Release it with FreeAnalysis.

The example for this item is shown with the ones above: db-sql-Methods.salam

FreeAnalysis

func FreeAnalysis(a &: Analysis)
Release an analysis and the strings in it.

The example for this item is shown with the ones above: db-sql-Methods.salam

StmtKindName

func StmtKindName(k: int): str
The name of a STMT_* kind: "select", "insert", and so on.

The example for this item is shown with the ones above: db-sql-Methods.salam

NewArgs

func NewArgs(): Args
An empty value list.

The example for this item is shown with the ones above: db-sql-Methods.salam

AddInt

func AddInt(a &: Args, v: i64)
Add an integer value.

The example for this item is shown with the ones above: db-sql-Methods.salam

AddFloat

func AddFloat(a &: Args, v: f64)
Add a floating point value.

The example for this item is shown with the ones above: db-sql-Methods.salam

AddText

func AddText(a &: Args, v: str)
Add a text value. The string is copied.

The example for this item is shown with the ones above: db-sql-Methods.salam

AddBool

func AddBool(a &: Args, v: bool)
Add a boolean value.

The example for this item is shown with the ones above: db-sql-Methods.salam

AddNull

func AddNull(a &: Args)
Add an SQL NULL.

The example for this item is shown with the ones above: db-sql-Methods.salam

ArgCount

func ArgCount(a: Args): int
How many values the list holds.

The example for this item is shown with the ones above: db-sql-Methods.salam

FreeArgs

func FreeArgs(a &: Args)
Release a value list and the text it copied.

The example for this item is shown with the ones above: db-sql-Methods.salam

Fill

func Fill(query: str, a: Args, d: Dialect, err &: Report): str
Write the values into the query and return the finished SQL. The query is lexed first, so a ? inside a string literal is left alone, the count has to match, and each value is written the way the dialect wants it: quotes doubled, backslashes doubled on MySQL, TRUE and FALSE on PostgreSQL. It is substitution, not a prepared statement, and the result is yours to free.

The example for this item is shown with the ones above: db-sql-Methods.salam

DialectOf

func DialectOf(id: int): Dialect
The dialect of one of SQLITE, MYSQL, MARIADB or POSTGRES.

The example for this item is shown with the ones above: db-sql-Methods.salam

DialectNamed

func DialectNamed(name: str): Dialect
The dialect of a name such as "sqlite", "mysql", "mariadb" or "postgres", which is what db.SQLITE and c.Kind() give you. An unknown name gives SQLite.

The example for this item is shown with the ones above: db-sql-Methods.salam

FreeReport

func FreeReport(r &: Report)
Release a report.

The example for this item is shown with the ones above: db-sql-Methods.salam

DiagText

func DiagText(d: Diag): str
One diagnostic as a line of text, with its help line under it.

The example for this item is shown with the ones above: db-sql-Methods.salam

ReportText

func ReportText(r: Report): str
A whole report as text, one diagnostic per block.

The example for this item is shown with the ones above: db-sql-Methods.salam

Lint

func Lint(sql: str, d: Dialect): Report
Parse the SQL and report what is wrong with it, without a schema. Lexical, syntax and dialect problems only.

The example for this item is shown with the ones above: db-sql-Methods.salam

ParamCount

func ParamCount(sql: str, d: Dialect): int
How many placeholders the SQL binds.

The example for this item is shown with the ones above: db-sql-Methods.salam

Placeholders

func Placeholders(sql: str, d: Dialect): Vector<str>
Every placeholder as written, in order. You own the vector and the strings in it.

The example for this item is shown with the ones above: db-sql-Methods.salam

NewCatalog

func NewCatalog(d: Dialect): Catalog
An empty catalog that reads DDL and queries with this dialect.

The example for this item is shown with the ones above: db-sql-Methods.salam

Apply

func Apply(c &: Catalog, ddl: str): Report
Run DDL into the catalog and report what was wrong with it. Call it once per migration, in order.

The example for this item is shown with the ones above: db-sql-Methods.salam

FreeCatalog

func FreeCatalog(c &: Catalog)
Release a catalog and every name in it.

The example for this item is shown with the ones above: db-sql-Methods.salam

TableCount

func TableCount(c: Catalog): int
How many tables and views the catalog holds.

The example for this item is shown with the ones above: db-sql-Methods.salam

TableName

func TableName(c: Catalog, i: int): str
The name of the table at this position, or "".

The example for this item is shown with the ones above: db-sql-Methods.salam

HasTable

func HasTable(c: Catalog, name: str): bool
Whether the catalog has a table or view of this name, ignoring case.

The example for this item is shown with the ones above: db-sql-Methods.salam

ColumnCount

func ColumnCount(c: Catalog, table: str): int
How many columns a table has, or 0 when there is no such table.

The example for this item is shown with the ones above: db-sql-Methods.salam

ColumnAt

func ColumnAt(c: Catalog, table: str, i: int): Column
The column at this position of a table, or an empty Column.

The example for this item is shown with the ones above: db-sql-Methods.salam

HasColumn

func HasColumn(c: Catalog, table: str, column: str): bool
Whether a table has a column of this name, ignoring case.

The example for this item is shown with the ones above: db-sql-Methods.salam

KindName

func KindName(k: int): str
The name of a TYPE_* kind: "int", "text", and so on.

The example for this item is shown with the ones above: db-sql-Methods.salam

SalamType

func SalamType(k: int, not_null: bool): str
The Salam type that reads a column of this kind: i64, f64, bool or str, wrapped in option.Option when the value can be null.

The example for this item is shown with the ones above: db-sql-Methods.salam

ParseQueries

func ParseQueries(src: str, err &: Report): Vector<Query>
Read the -- name: Thing :one headers and their SQL out of a query file. A bad name or an unknown kind is reported rather than guessed.

The example for this item is shown with the ones above: db-sql-Methods.salam

FreeQueries

func FreeQueries(v &: Vector<Query>)
Release the queries and the text they own.

The example for this item is shown with the ones above: db-sql-Methods.salam

Generate

func Generate(pkg: str, queries: Vector<Query>, c: Catalog, err &: Report): str
Write the Salam source for a whole query file: one row struct and one function per query, each analysed against the catalog first. A query that does not resolve is reported with its name and nothing is generated for it.

The example for this item is shown with the ones above: db-sql-Methods.salam