SQL Builder¶
Builder generates SQL strings for common SQLite DDL operations. It is a
convenience layer, not an ORM: inspect the generated SQL and execute it with
db.execute() or use it directly in a migration.
from scriptdb import Builder
sql = Builder.create_table("users").primary_key("id", int).add_field("name", str)
print(sql) # __str__ delegates to done()
print(sql.done())
Supported Python types are int, str, float, bytes, bool, date, and
datetime. Date and datetime values are stored as SQLite TEXT declarations.
Security: identifiers are quoted, but raw SQL options such as
checkexpressions are not sanitized. Never pass untrusted input to table names, column names, or raw constraint expressions.
Builder.create_table¶
Start a CREATE TABLE statement:
sql = (
Builder.create_table("users", if_not_exists=True)
.primary_key("id", int)
.add_field("email", str, not_null=True, unique=True)
.add_field("active", bool, default=True)
.unique("email")
.check("length(email) > 3")
.done()
)
Options:
if_not_exists=TrueaddsIF NOT EXISTS;without_rowid=TrueaddsWITHOUT ROWID;primary_key(name, py_type, auto_increment=..., not_null=True)adds a primary key. Integer keys auto-increment by default;add_fieldand its aliasadd_columnadd typed columns;remove_column,remove_field, andremove_filterremove a column already added to the builder;unique(*columns)adds a table-level unique constraint;check(expression)adds a table-level check constraint;done()orstr(builder)renders SQL.
primary_key(name, py_type, *, auto_increment=..., not_null=True)¶
name is the column name and py_type is one of the supported Python types.
With the default auto_increment=..., integer primary keys get SQLite
AUTOINCREMENT; pass False to disable it. auto_increment=True is only
valid for an integer primary key. not_null is ignored when auto-increment is
enabled because SQLite implies NOT NULL:
Builder.create_table("users").primary_key(
"id", int, auto_increment=False, not_null=True
).done()
add_field(name, py_type, *, not_null=False, unique=False, default=None, check=None, references=None)¶
Add a typed column. default is rendered as a SQLite literal, check is a
raw SQLite expression, and references is (table, column) or (table, None):
Builder.create_table("users").add_field(
"email",
str,
not_null=True,
unique=True,
default="unknown@example.com",
check="length(email) > 3",
references=("accounts", "email"),
).done()
add_column(name, ...) is an exact alias for add_field.
remove_column(name), remove_field(name), and remove_filter(name)¶
Remove a column already queued in a CREATE TABLE builder. The three methods
are aliases. A missing name raises ValueError:
table = Builder.create_table("users").add_field("temporary", str)
table.remove_column("temporary")
unique(*cols) and check(expr)¶
Add table-level constraints. unique needs at least one column. check takes
raw SQL and is not sanitized:
Builder.create_table("users").unique("email").check("age >= 0").done()
Example with references and defaults:
sql = (
Builder.create_table("posts")
.primary_key("id", int)
.add_field("author_id", int, references=("users", "id"))
.add_field("published", bool, default=False, not_null=True)
.done()
)
Builder.create_table_from_dict¶
Infer columns from a flat representative dictionary:
sql = Builder.create_table_from_dict(
"users",
{"id": 1, "name": "Alice", "active": True},
).done()
An id value of type int or str becomes a primary key. Nested mappings,
lists, tuples, and sets are rejected because inference only supports flat
values. An empty dictionary is invalid.
Builder.alter_table¶
Queue one or more ALTER TABLE actions:
sql = (
Builder.alter_table("users")
.add_column("age", int, default=0)
.rename_column("name", "display_name")
.done()
)
Methods:
add_column(name, py_type, not_null=False, unique=False, default=None, check=None, references=None);add_field(...)is an alias foradd_column;drop_column(name);remove_column(name),remove_field(name), andremove_filter(name)are aliases fordrop_column;rename_to(new_table_name);rename_column(old_name, new_name);done()renders all queued statements separated by newlines.
add_column uses the same parameters and constraints as the create-table
builder. Each call queues an action; the SQL is not executed until you pass
the result to db.execute() or use it in a migration. rename_to changes the
table name used by subsequent queued actions.
An ALTER TABLE builder with no actions raises ValueError.
Drop table and indexes¶
drop_table = Builder.drop_table("old_users", if_exists=True).done()
create_index = Builder.create_index(
"users",
on=["last_name", "first_name"],
unique=False,
name="idx_users_name",
).done()
drop_index = Builder.drop_index(
"users",
on=["last_name", "first_name"],
name="idx_users_name",
).done()
create_index(table, on, unique=False, if_not_exists=True, name=None) accepts
a column string or list. When name is omitted it is generated as
{table}_{columns}_idx.
drop_table and drop_index accept if_exists. drop_index can infer the
same generated name from table and on; pass name for a custom index.
Builders in migrations¶
Builders can be stored directly in sql or inside sqls:
class AppDB(SyncBaseDB):
def migrations(self):
return [{
"name": "create_users",
"sql": (
Builder.create_table("users")
.primary_key("id", int)
.add_field("name", str, not_null=True)
),
}]