Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ where 1 = 1
order by id
```

(A template may instead spell its binds the way sqlc does, giving up this property for
values in exchange for having them typed by an analyzer; see [Bind syntax](#bind-syntax).)

When the text is executed verbatim in a SQL client, `/*%if*/`, `/*%end*/`, and `/*name*/` are
interpreted as comments, and the trailing literal `'Alice'` remains in place; the client
therefore evaluates `name = 'Alice'`. When the same text is processed by bisql, the `/*%if*/`
Expand Down Expand Up @@ -632,12 +635,105 @@ a `/*%for*/` directive is the iterable expression verbatim — including any col
`a ? b : c`, a slice `x[1:2]`, a map `{k: v}`). The evaluator is replaceable through
`WithEvaluator`.

## Bind syntax

A bind is written as `/*expr*/literal` by default, and that shape is what makes a template
runnable as-is: the comment is ignored and the sample literal takes its place. The cost
appears when the template is also meant to be read by a static analyzer such as
[sqlc](https://sqlc.dev). Being runnable requires a **literal** at the bind site; being
recognized as a parameter requires a **marker**. No single text is both, so an analyzer
reading a two-way template sees constants where the binds are — it can check the SQL, the
catalog, and the result columns, but it can say nothing about the arguments.

`WithBindSyntax(bindsyntax.SqlcNamed)` trades the runnable-as-is property, for values only,
to get that back:

```go
tmpl, err := bisql.Parse(src,
bisql.WithBindSyntax(bindsyntax.SqlcNamed),
bisql.WithDialect(dialect.PostgreSQL))
```

| Form | Binds | Notes |
| ---- | ----- | ----- |
| `sqlc.arg(name)` | one parameter | the name may also be quoted, and only then may it contain dots (`'c.name'`), for a value reached through a field |
| `sqlc.narg(name)` | one parameter | identical at build time; the distinction is for the analyzer |
| `sqlc.slice(name)` | a placeholder list | the parentheses stay in the template, as `in (sqlc.slice(ids))` |
| `@name` | one parameter | a shortcut for `sqlc.arg(name)`, **except under MySQL** (see below); `@a.b` is an error, not a dotted name |

```sql
select id from users
where 1 = 1
/*%if activeOnly*/ and status = @status /*%end*/
/*%if minAge != null*/ and age >= @min_age /*%end*/
/*%for kw in keywords*/ and name like @kw /*%end*/
and id in (sqlc.slice('ids'))
```

**Only the bind spelling changes.** Every block directive is a SQL comment under either
syntax, so `/*%if*/`, `/*%elseif*/`, `/*%else*/`, `/*%for*/` and `@include` behave
identically, and placeholder numbering is the same single renderer-global counter — binds in
branches that did not render are still never counted.

The two forms that depend on a test literal have no meaning without one, so `Parse` rejects
them rather than reinterpreting them:

- **The two-way bind directive.** `/*status*/'active'` would otherwise be read as a plain
comment followed by a literal, producing a query that runs while ignoring a value.
- **`/*^ */` literal interpolation.** The value is inlined as text rather than bound, so an
analyzer sees a constant there and can check nothing about it. Bind the value as a
parameter, or use a whitelisted `/*%if*/` toggle for an identifier or a sort direction.

A prefix that could only have been meant as a marker but cannot be one is rejected for the
same reason, since nothing downstream parses the SQL to catch it:

```sql
where name = @c.name -- error: a dotted name must be sqlc.arg('c.name')
where name = sqlc.arg(c.name) -- error: a dotted name has to be quoted
```

`@c.name` would otherwise bind only `c` and render as `$1.name`. sqlc makes the same reading
and then rejects the edited query for being invalid SQL; bisql never parses the SQL, so it
has to reject the spelling up front instead.

The two syntaxes are not mirror images of each other. A named marker is opaque under the
default syntax — `@status` is a plain word there — but a two-way directive under `SqlcNamed`
is an error rather than text, because reading it as a comment followed by a literal would
give a query that runs while ignoring a value.

### The `@name` shortcut and MySQL

**`@name` is a bind under every dialect except MySQL**, where it stays ordinary text. This
mirrors sqlc, which does not support the shortcut for MySQL because `@name` there is a user
variable — and mirroring it is the point: a spelling one of them binds and the other does
not is precisely the divergence this arrangement exists to avoid.

```sql
-- sqlc-named, MySQL: a user variable, left alone
select @row_number := @row_number + 1
-- sqlc-named, PostgreSQL: two binds
select @row_number := @row_number + 1 -- renders as: select $1 := $2 + 1
```

Recognizing `@name` also requires that what follows the `@` can start an identifier, so `@>`
and `@@version` are names under no dialect. The call forms are available everywhere, so a
template that has to read the same way under both engines should use them.

### What is not a bind

`sqlc.embed(table)` is a result-column construct — it selects a whole table into a nested
struct — so it is not a bind and passes through untouched. Note that sqlc expands it into an
explicit column list in the SQL it hands back, so a tool rendering that SQL never sees the
call; a tool rendering the *original* template would, and would send it to the database.

## Package layout

```text
bisql Public API: NewParser, Parser, Parse, ParseFile, Expand, ExpandFile,
Template, Statement, Option, and fragment loaders (Loader, RegistryLoader,
FSLoader, LoaderFunc, StackedLoader, ErrNotFound, WithLoader, WithStackedLoader).
bindsyntax/ How a bind is written: TwoWay (the default /*expr*/literal form) and
SqlcNamed (@name / sqlc.arg('name'), not implemented yet).
dialect/ Dialect definitions: placeholder generation and literal formatting
(MySQL, SQLite, PostgreSQL, Oracle, SQL Server).
expr/ Evaluator interface and Scope (for custom evaluators).
Expand Down
Loading
Loading