README
¶
Sqltom
Sqltom is a command line tool to inspect SQL database, stores its visible schema in an editable manifest, and renders Go packages from that manifest.
Features:
- Supports PostgreSQL, MySQL, SQLite, and SQL Server.
- Inspects tables and views.
- Generates Go models from an editable, backend-neutral manifest.
- Generates an optional
queryhelper for composing SQL with named bindings and lists. - Supports custom data type mappings through the manifest.
- Supports custom column, table, package, and file names through the manifest.
Installation:
Sqltom and its generated packages require Go 1.27 or newer. Install the latest version with:
go install github.com/catamat/sqltom/cmd/sqltom@latest
Then verify the installation:
sqltom -version
Dialects:
- PostgreSQL 12 or newer via
pgx - MySQL 8.0 or newer via
go-sql-driver/mysql - SQLite through the bundled pure-Go
modernc.org/sqliteengine - SQL Server 2016 or newer via
go-mssqldb
Inspection verifies these minimum server versions before accepting catalog metadata. MariaDB is not treated as MySQL because its catalog compatibility is not part of the supported contract. The bundled SQLite engine must provide SQLite 3.37 or newer, which introduced the catalog primitives used by Sqltom.
Server-dialect DSNs are passed unchanged to the selected driver. SQLite inspection preserves explicit in-memory DSNs and forces file DSNs to read-only mode. Typical forms are:
sqlserver://user:password@host:1433?database=MainDB
postgres://user:password@host:5432/MainDB?sslmode=disable
user:password@tcp(host:3306)/MainDB?parseTime=true
./MainDB.sqlite
For SQLite, a file database uses its filename as DatabaseName; an in-memory
database uses main. Tables in main or an attached database retain the SQLite
schema alias in TableSchema. File databases are opened read-only during
inspection, so a missing or misspelled path fails instead of creating an empty
database. Explicit in-memory DSNs remain supported.
Commands:
Print the installed version with -version.
Every database operation accepts an optional -tables filter. Its value is a
CSV list of exact, case-sensitive database table or view selectors. A selector
may be name, schema.name, or catalog.schema.name:
-tables="dbo.Vehicle,dbo.Customer"
Omitting the flag selects every object available to the operation;
passing an explicitly empty -tables="" is a usage error. Physical
names with leading or trailing whitespace cannot be selected because that
whitespace is treated as CSV syntax.
Inspect
sqltom -inspect \
-dialect="sqlserver" \
-dsn="sqlserver://user:password@host:1433?database=MainDB" \
Inspection reads visible user tables and views, the current database name,
catalog/schema identity, identity/computed/generated/rowversion/default flags,
and ordered primary-key metadata. It writes sqltom_MainDB.json in the current
directory. System and backend-specific unsupported objects are excluded.
If the target manifest exists, its DatabaseName must match the live
connection. The source dialect is deliberately not persisted: each inspector
normalizes native metadata into the common manifest vocabulary. Sqltom
refreshes database facts while preserving:
- global
TypeMappings; - table
IsManagedand, only while managed,FileName,PackageName, andGoName; - column
GoName,JSONName,GoType, andGoImport.
With -tables, only selected objects are refreshed. Previously saved objects
outside the selection retain their metadata, overrides, and IsManaged setting.
An inspection without -tables refreshes the whole visible catalog and removes
objects that are no longer present. Statistics and the models produced by
-generate refer only to the selected subset. A filtered render still replaces
the entire managed output directory with that subset.
Render
sqltom -render \
-dialect="sqlserver" \
-manifest="./sqltom_MainDB.json" \
-output="./models" \
-dialect selects the target backend, not the provenance of the manifest. The
same manifest can therefore be rendered by any implemented backend.
Render structurally validates the manifest for the target backend, and writes and formats
every selected model; files and model directories from previous renders are removed.
A missing or empty output directory is accepted. A non-empty directory is
replaced only if it contains the exact .sqltom-output marker created by a
prior successful Sqltom render; unrelated directories are refused.
An output path nested inside another Sqltom-managed output is also refused:
parent and child replacement cannot be made independently safe.
This marker is intentionally fail-closed. A model directory produced by an older Sqltom without the marker must be moved or emptied deliberately once, or you can render to a new path. Sqltom never adopts a non-empty unmarked directory automatically.
Generate
sqltom -generate \
-dialect="sqlserver" \
-dsn="sqlserver://user:password@host:1433?database=MainDB" \
-output="./models" \
Generate is Inspect followed by Render.
Manifest v1:
The manifest is deliberately editable and backend-neutral. DatabaseName is
mandatory and is its only database-level identity.
{
"Version": 1,
"DatabaseName": "MainDB",
"TypeMappings": {
"uuid": {
"GoType": "uuid.UUID",
"GoImport": "github.com/google/uuid",
"NullableGoType": "null.UUID",
"NullableGoImport": "github.com/catamat/null"
},
"string": {
"GoType": "string",
"NullableGoType": "null.String",
"NullableGoImport": "github.com/catamat/null"
}
},
"Tables": [
{
"TableCatalog": "MainDB",
"TableSchema": "dbo",
"TableName": "VEHICLE",
"TableType": "BASE TABLE",
"IsManaged": true,
"FileName": "vehicle",
"PackageName": "vehicle",
"GoName": "Vehicle",
"Columns": [
{
"ColumnName": "VEHICLE_ID",
"OrdinalPosition": 1,
"IsNullable": false,
"DataType": "uuid",
"IsIdentity": false,
"IsPrimaryKey": true,
"IsComputed": false,
"IsGeneratedAlways": false,
"IsRowVersion": false,
"HasDefault": true,
"PrimaryKeyOrdinal": 1,
"GoName": "ID",
"JSONName": "id",
"GoType": "",
"GoImport": ""
}
]
}
]
}
Table management
Every inspected table is written with "IsManaged": true. A missing
IsManaged key is also interpreted as true, but the next save writes it
explicitly.
Setting IsManaged to false excludes that table from rendering. On the next
inspect or generate, Sqltom retains only TableCatalog, TableSchema,
TableName, TableType, and IsManaged; it clears FileName,
PackageName, and GoName, and writes "Columns": []. The identity stub
remains in the manifest and can still be addressed by -tables.
To re-enable a table, set IsManaged to true and run inspect or generate.
Fresh inspection metadata repopulates Columns; discarded naming and column
overrides are not restored. Rendering a re-enabled stub directly, before a new
inspection, fails because its managed table has no columns.
Type precedence
For every column, the effective type is selected in this order:
- column
GoTypeandGoImport; - a matching
TypeMappingsentry; - a dialect-specific default, when provided by the selected renderer;
- the backend-neutral built-in mapping for the canonical
DataType; - an error naming the unknown logical or custom type.
TypeMappings is partial: omitted canonical types use the renderer defaults
and then the shared built-ins. Dialect defaults are applied to an in-memory
rendering view; they are not written into the manifest or merged over overrides.
If a selected mapping has no NullableGoType, a nullable column uses *GoType
and reuses GoImport.
Mapping lookup first tries the exact trimmed DataType, preserving case. A
case-insensitive fallback is used only when it identifies one entry; if multiple
case variants exist without an exact match, render fails with an ambiguity
error.
The package qualifier is taken from GoType, not inferred from the last segment
of GoImport; versioned module paths such as github.com/guregu/null/v6 are
therefore valid.
The canonical vocabulary is any, bigint, binary, boolean, date,
datetime, datetimeoffset, decimal, double, integer, json, money,
real, rowversion, smallint, string, time, tinyint, unsignedbigint,
unsignedinteger, unsignedsmallint, unsignedtinyint, uuid, variant, and
xml. Inspectors translate native database types into these values; renderers
never need to know which database originally produced the manifest. json maps
to encoding/json.RawMessage; variant maps to any for both nullable and
non-nullable columns because each value can have a different concrete Go type.
time maps to string (*string when nullable), preserving SQL time text and
fractional precision. This also represents MySQL's negative durations and hours
above 24 without inventing a date. SQL Server's driver time values are formatted
as time-of-day text. Explicit custom time mappings retain their own conversion
contract; existing time.Time overrides are not silently migrated.
Generated scan adapters accept both text and bytes for JSON, and binary BIT(1)
values for booleans. JSON writes bind text while preserving SQL NULL.
The remaining conversion uses database/sql, including custom Scanner and
driver.Valuer types; generated packages need no Sqltom runtime dependency.
IsIdentity is the normalized identity-like flag used for a value generated by
the database and omitted from ordinary inserts. Backends map their equivalent
mechanisms to it, such as identity, auto-increment, or a SQLite rowid alias.
Computed, generated-always, and rowversion columns remain available in models
and reads but are omitted from ordinary inserts and updates. HasDefault alone
does not make a column read-only, so the current API can still provide an
explicit value.
Column-level GoType is already the final type, including nullability:
"GoType": "sql.NullString",
"GoImport": "database/sql"
All four dialects render logical uuid (or guid) columns as string by
default, or *string when nullable. Generated reads return lowercase UUID text;
Insert and Update also lowercase string UUID parameters, including keys,
without modifying the caller's struct. SQL NULL remains nil, distinct from
the zero UUID (00000000-0000-0000-0000-000000000000) and empty text.
SQL Server uses UNIQUEIDENTIFIER and PostgreSQL uses native UUID. For MySQL
CHAR(36) and SQLite TEXT, declare "DataType": "uuid" on the relevant
manifest columns: generic text columns are not automatically treated as UUIDs.
Store UUID text in canonical, lowercase, hyphenated form, including external
writes. The string mapping normalizes case but does not validate or repair UUID
syntax; native UUID columns still enforce their database's validation rules.
Binary UUID storage such as MySQL BINARY(16) requires a suitable custom mapping.
Arguments supplied to query suffixes, Query or the builder are passed unchanged;
use lowercase UUID strings when comparing against textual UUID columns.
SQL Server's generated scanner decodes the driver's native UUID bytes without
SQL casts, using github.com/microsoft/go-mssqldb internally. Keep the driver's
guid conversion option at its default false to avoid decoding the byte order
twice. This requirement also applies to custom UUID mappings.
You can use github.com/google/uuid in the public model by adding this entry to
TypeMappings; the generated read methods work without casts in your queries:
"uuid": {
"GoType": "uuid.UUID",
"GoImport": "github.com/google/uuid"
}
Nullable columns then use *uuid.UUID. To use uuid.NullUUID instead, add
"NullableGoType": "uuid.NullUUID" and
"NullableGoImport": "github.com/google/uuid". Column-level GoType/GoImport
still take precedence over this global mapping. Inserts, updates and query
parameters retain the chosen type's Valuer behavior.
Regenerate models to adopt the new default. Existing explicit UUID mappings are
preserved: remove them (including column overrides), or change them to string
and *string, if you want those models to use strings too. An explicit
mssql.UniqueIdentifier mapping remains available for SQL Server applications.
Naming fields
- table
IsManaged: whether Sqltom renders the table; - table
FileName: output directory and.gofilename base; - table
PackageName: package declaration; - table
GoName: model type; - column
GoName: struct field; - column
JSONName: JSON field name, defaulting to the original database name; - column
GoType/GoImport: granular type override.
Explicit invalid overrides fail validation. Defaults retain case but remove
characters invalid in Go identifiers or package import paths. File defaults also
avoid _test, implicit GOOS/GOARCH suffixes, vendor, testdata, leading
dot/underscore, Windows device names, and overlong components. Output-name
collisions are checked with Unicode case folding for case-insensitive
filesystems.
Because defaults do not change case, a lowercase database name produces a
lowercase, unexported Go identifier. The CLI emits a warning for every effective
table or column GoName that is not exported; set it explicitly in the
manifest when the model or field must be visible outside its package.
Generated model API
Every generated model package declares a DBTX interface implemented by both
*sql.DB and *sql.Tx. SelectAll, SelectCols, Insert, Update, Delete, Exists, and
Query accept that interface, so the same generated operations can run either
directly or inside a transaction without wrappers.
The collection also provides:
Rows.First() (*Model, error): returns the first loaded row, orsql.ErrNoRowswhen the collection is empty.Rows.Last() (*Model, error): returns the last loaded row, orsql.ErrNoRowswhen the collection is empty.Rows.One() (*Model, error): returns the only loaded row. Empty collections returnsql.ErrNoRows; multiple rows return an error wrappingErrMultipleRows, recognizable witherrors.Is.At(index int) (*Model, error): zero-based access. Negative indices and indices greater than or equal tolen(rows)return an error wrapping the package'sErrIndexOutOfRange, recognizable witherrors.Is. This also applies to empty collections. It returns the existing pointer for a valid index.IsEmpty() bool: reports whetherlen(rows) == 0, including nil slices.Filter(predicate func(*Model) bool) Rows: returns a new slice with the existing pointers that satisfy the predicate, preserving order and duplicates. No matches (or an empty input) produces a non-nil emptyRows.
Generated query helper
Each render also creates <output>/query, alongside the table and view packages.
Import it as "your/module/models/query" when your output folder is models.
It uses only the standard library, has no runtime dependency on Sqltom, and
already knows the rendering dialect. Existing model functions keep their signatures;
using the helper is optional.
q := query.New()
q.Write("WHERE 1 = 1")
if namePrefix != "" {
q.Write("AND Name LIKE :name_prefix").
Bind("name_prefix", namePrefix+"%")
}
if len(ids) > 0 {
q.Write("AND ID IN (:ids)").Bind("ids", ids)
}
q.Write("ORDER BY ID")
stmt, args, err := q.Build()
if err != nil {
return nil, err
}
rows, err := vehicle.SelectAll(db, stmt, args...)
Write appends each fragment verbatim, separated by a newline. It neither adds
clauses nor checks their order or validity. Identifiers, quoting, operators,
functions, and parameter type context are your SQL dialect's responsibility.
For example, PostgreSQL's ILIKE is not translated for other databases. Use
trusted SQL fragments and bind dynamic values; bindings cannot replace identifiers.
Bind accepts case-sensitive names matching [A-Za-z_][A-Za-z0-9_]*, without the
colon. Bindings may precede or follow their SQL fragments. Arguments are ordered
by appearance in SQL, independently of the order of Bind calls. Repeated uses
of a name are supported; calling Bind twice for that name is an error, even
when the values are equal.
Scalar prevents expansion and passes the original value as one
argument. It does not serialize or convert the value; the driver must support
its type. For example, with PostgreSQL and pgx:
q := query.New().Write("WHERE id = ANY(:ids)").
Bind("ids", query.Scalar([]int64{7, 9}))
Build works with SelectAll, SelectCols, Exists,
and Delete when constructing a statement suffix. It also works with Query
and database/sql directly when constructing a complete statement.
It does not execute SQL or call driver.Valuer.Value.
Preview uses the same compilation and returns SQL plus
numbered arguments with their Go types, without interpolating them into SQL:
preview, err := q.Preview()
if err != nil {
return nil, err
}
fmt.Println(preview)
Gotchas & tips:
- The project is designed for local, sequential use. Concurrent use in distributed or automated environments may cause issues and is currently outside the project's scope.
- The CLI currently accepts configuration only through command-line arguments, not environment variables or file descriptors.
- Databases with the same
DatabaseNameare treated as the same logical source, regardless of host or instance. - SQL Server: leave
guid conversionunset orfalse(the driver default). Generated models already decode GUID byte order; enabling it would convert twice and return incorrect UUIDs.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
sqltom
command
|
|
|
internal
|
|
|
output
Package output installs generated directory trees without overwriting output that is not owned by sqltom.
|
Package output installs generated directory trees without overwriting output that is not owned by sqltom. |
|
render/queryruntime
Package query composes SQL fragments and binds named values for its generated dialect.
|
Package query composes SQL fragments and binds named values for its generated dialect. |