Getting Started topic

Getting Started

This walks through the full migration-first workflow: configure → migrate → generate schema → write models → generate mappers → query. It mirrors the runnable example/ app in this repository.

1. Dependencies

The packages are unpublished, so depend on them via path: (within this repo) or git::

dependencies:
  basalt:
  basalt_sqlite:
dev_dependencies:
  basalt_cli:       # the `basalt` CLI
  basalt_codegen:   # build_runner derives
  build_runner: ^2.4.0

Dart SDK: >=3.5.0 <4.0.0.

2. Configure the CLI

Create basalt.yaml in the directory you'll run the CLI from:

backend: basalt_sqlite      # required — the backend package (no default)
database:
  path: app.db              # SQLite: a file path, or ':memory:'
migrations_dir: migrations
# native_types: true        # opt into the backend's native type presets (see §4)
# types:                    # optional — customize generated column types (see §4)

The CLI itself has no dependency on any database backend. backend: is required — naming the backend package is an explicit decision, there is no default. On first run the CLI generates a small entrypoint under .dart_tool/basalt/ that imports that package's adapter and re-runs itself through it (the build_runner model), so the backend must be in your dev_dependencies. It is regenerated automatically when the backend or the package graph changes. Run the CLI from the project root (where .dart_tool/package_config.json lives).

The keys under database: are adapter-specific:

# SQLite (backend: basalt_sqlite)
database:
  path: app.db          # or ':memory:'

# Postgres (backend: basalt_postgres) — a URL...
database:
  url: postgres://user:secret@localhost:5432/shop?sslmode=disable

# ...or manual keys
database:
  host: localhost       # default: localhost
  port: 5432            # default: 5432
  database: shop        # required
  username: postgres    # default: postgres
  password: secret      # default: empty
  ssl: false            # default: true

If DATABASE_URL is set in the environment it overrides database.url — handy for CI with Postgres. Writing a new backend? See basalt CLI Adapters & Tooling.

3. Create and apply a migration

dart run basalt_cli:basalt migration generate create_users

This scaffolds migrations/<version>_create_users/{up,down}.sql. Fill them in:

-- up.sql
CREATE TABLE users (
  id         INTEGER PRIMARY KEY,
  name       TEXT    NOT NULL,
  age        INTEGER NOT NULL,
  active     INTEGER NOT NULL,
  manager_id INTEGER REFERENCES users(id)
);
-- down.sql
DROP TABLE users;

Apply pending migrations (each runs in a transaction, tracked in __basalt_schema_migrations):

dart run basalt_cli:basalt migration run

See Migrations in the sidebar for the full command set.

4. Generate the typed schema

dart run basalt_cli:basalt generate-schema

generate-schema introspects the live database, so run it after applying migrations. It writes table marker classes to schema_output in basalt.yaml (default lib/schema.dart):

// GENERATED by `basalt generate-schema`. Do not edit by hand.
final class Users extends TableRef<Users> {
  const Users._() : super('users');

  static const table = Users._();

  static const id = PrimaryKey<int, Users>(
    table,
    'id',
    IntSqlType(),
  );
  static const name = ValueColumn<String, Users>(
    table,
    'name',
    StringSqlType(),
  );
  static const age = ValueColumn<int, Users>(
    table,
    'age',
    IntSqlType(),
  );
  static const managerId = Ref<int?, Users, Users>(
    table,
    'manager_id',
    NullableSqlType(IntSqlType()),
    references: Users.id,
  );

  @override
  List<TableColumn<Object?, Object?>> get columns =>
      const [id, name, age, managerId];
}

Because the generated class extends TableRef, a column whose camelCase field name collides with an inherited member (table, tableName, alias, columns, col, aliased, or an Object member like to_string) cannot be emitted — generate-schema fails with a clear error asking you to rename the column.

SQLite has no native boolean or timestamp, so a column declared bare INTEGER comes back as int. Columns declared BOOLEAN/DATETIME are restored to bool/DateTime by the SQLite adapter's built-in preset — see basalt_sqlite Type Mapping for the details, and basalt Types for the codec model.

Customizing column types

By default each column maps to a built-in SqlType, adjusted by the backend adapter's type presets: a portable tier that always applies (e.g. SQLite's declared-BOOLEAN/DATETIME fix), and a backend-native tier that applies only with native_types: true (e.g. Postgres json/jsonbMap<String, Object?> via PostgresJsonbSqlType — note the generated schema then imports the backend package and is no longer backend-portable).

To emit your own custom SqlType (e.g. an enum codec, or a JSON type) instead of editing the generated file by hand, add an optional types: block to basalt.yaml — it always wins over the adapter's presets. Overrides are matched per column with the precedence specific column > native type > canonical type, each falling back to the presets and then the built-in mapping:

types:
  # By canonical type — keys are ColumnType names
  # (integer, text, real, boolean, blob, dateTime).
  canonical:
    dateTime:
      dart_type: "DateTime"
      sql_type: "UtcDateTimeSqlType()"
      import: "package:my_app/db/utc_date_time_sql_type.dart"

  # By native/backend type — matched against the introspected column type
  # (trimmed, lower-cased; a `varchar` key also matches `VARCHAR(255)`).
  native:
    jsonb:
      dart_type: "Map<String, Object?>"
      sql_type: "JsonMapSqlType()"
      import: "package:my_app/db/json_map_sql_type.dart"
      nullable:                          # used for NULLable columns
        dart_type: "Map<String, Object?>?"
        sql_type: "NullableSqlType(JsonMapSqlType())"

  # By a single column — key is "table.column" (highest precedence).
  columns:
    users.metadata:
      dart_type: "UserMeta"
      sql_type: "UserMetaSqlType()"
      import: "package:my_app/models/user_meta.dart"

Each entry needs dart_type and sql_type (both written verbatimdart_type must equal the SqlType's T), an optional import the generated file needs for a custom symbol (omit it for built-in types), and an optional nullable: variant used when the column allows NULL. The generator collects and de-duplicates the imports at the top of the file. Overrides apply to ValueColumn, PrimaryKey and Ref alike; a foreign key and the primary key it references must resolve to the same base type (override both or neither), otherwise generate-schema fails with a clear error.

Postgres reports enum/array columns as USER-DEFINED/ARRAY (the specific name isn't captured), so target those with a per-column override rather than a native: entry.

5. Write data classes and generate mappers

Annotate plain Dart classes against the schema, then run build_runner. See basalt Annotations & Codegen for the full derive reference.

// lib/user.dart
import 'package:basalt/basalt.dart';
import 'schema.dart';

part 'user.g.dart';

@Queryable(Users.table)
@Insertable(Users.table)
@AsChangeset(Users.table)
class User {
  final int id;
  final String name;
  final int age;
  final int active;
  final int? managerId;

  @Relation(Users.managerId)
  final User? manager;

  const User(this.id, this.name, this.age, this.active, {this.managerId, this.manager});
}
dart run build_runner build

See basalt_codegen Getting Started for wiring build.yaml.

6. Query and write

import 'package:basalt/basalt.dart';
import 'package:basalt_sqlite/basalt_sqlite.dart';
import 'schema.dart';
import 'user.dart';

Future<void> main() async {
  final db = SqliteConnection.open('app.db');

  await db.execute(const User(1, 'Bob', 30, 1).toInsert());

  final adults = await db.fetch(
    from(Users.table).where(Users.age > 18).orderBy(Users.name.asc()).mapWith(UserQuery.mapper),
  );
  print(adults);

  await db.close();
}

Next: basalt Query Builder for the full DSL, and example/ for joins, relations, transactions, and raw SQL.

Libraries

basalt_cli Getting Started
CLI library for the Basalt Dart ORM. The basalt executable is a thin bootstrapper (Bootstrapper) that generates and runs an entrypoint calling runBasalt with the configured backend adapter; the migration engine (MigrationRunner) lives in package:basalt/migration.dart and is re-exported here for convenience, as is the adapter contract from package:basalt/tooling.dart.

Classes

BasaltConfig Getting Started
CLI configuration, loaded from basalt.yaml.
CliRunner Getting Started
Builds the basalt command tree around the backend adapter the bootstrap entrypoint was generated with.

Functions

runBasalt(List<String> args, {required BasaltAdapter adapter}) Future<int> Getting Started
Runs the basalt CLI against the given backend adapter and returns the process exit code.