Chapter 12 · Views, Triggers, ATTACH, Multiple Databases, and Schema-Level Automation

Application IDs, user_version, schema_version, and Database-as-File-Format Design

Treat a SQLite database as a versioned application file format by validating application_id, user_version, schema_version, and required schema objects before handing the file to migrations or application code.

Beginner110–130 minutesFile-identity labSQLite 3.53.4 baselineApplication-owned version metadataLast reviewed: August 2026

Learning outcomes

SQLite's stable file format makes a database attractive as an application document: one portable file can contain relational data, indexes, constraints, and metadata. But “this is a valid SQLite file” is not the same as “this is a compatible FieldNotes file.” Applications need their own identity and schema-version contract.

01

Use application_id as an application-owned file-type marker.

02

Use user_version as an application-owned schema/migration version integer.

03

Distinguish user_version from SQLite-managed schema_version/schema cookie behavior.

04

Validate identity, version, and required schema before enabling writes.

05

Design practical forward/backward compatibility rules for application database files.

06

Build a read-only file-identification script that hands compatible files to later migration logic.

Three “version-ish” values with different owners

MetadataOwnerMeaning
PRAGMA application_idYour application/file formatA 32-bit integer used to identify the expected application database type.
PRAGMA user_versionYour application/migrationsA 32-bit integer SQLite stores but does not interpret for you.
PRAGMA schema_versionSQLite internalsThe schema cookie used to detect schema changes; applications should not treat manual writes as migrations.

Keeping ownership clear prevents one of the most damaging migration anti-patterns: manually adjusting internal metadata to make an incompatible schema “look current.”

Assign an application ID intentionally

For the course lab we use decimal 1179537236, whose bytes correspond to the memorable marker FNOT. A real project should choose and document its identifier according to its file-format governance.

sql · identify the FieldNotes file type
PRAGMA application_id = 1179537236;PRAGMA application_id;

This marker does not provide cryptographic authenticity. It is a fast format-identification field. A malicious or corrupted file can contain the same number, so still validate schema/integrity and trust boundaries appropriate to your application.

user_version is a migration contract you define

sql · mark schema generation 12
PRAGMA user_version = 12;PRAGMA user_version;

SQLite will happily store the integer but does not know that “12” means Chapter 12 or migration 12. Your migration code must define transitions such as 10→11 and 11→12, test them, and decide which older/newer versions can be opened read-only, migrated, or rejected.

schema_version is not your application migration number

sql · observe, do not manage as an app version
PRAGMA schema_version;CREATE TABLE IF NOT EXISTS schema_cookie_probe(id INTEGER PRIMARY KEY);PRAGMA schema_version;DROP TABLE schema_cookie_probe;PRAGMA schema_version;

Schema changes alter SQLite's schema cookie so prepared statements can detect that their compiled schema assumptions may be stale. The current PRAGMA documentation warns that direct manipulation can cause incorrect behavior. Use DDL to change schema and user_version to record your migration level.

Never fix a migration by bumping schema_version

If the tables/indexes/views/triggers are wrong, change those schema objects through a tested migration. Internal schema metadata is not a substitute for the migration itself.

Unknown file workflow: validate before writes

open candidate file READ-ONLY
          |
          v
is it readable SQLite? ---- no ---> reject / report
          |
          yes
          v
application_id matches? --- no ---> wrong application file
          |
          yes
          v
user_version supported? --- newer ---> reject or read-only compatibility mode
          |
       older/current
          v
required schema objects + columns present?
          |
          +--- no ---> reject / repair / migration failure
          |
          yes
          v
integrity / foreign-key checks as policy requires
          |
          v
handoff to migration or normal application open

Validate the schema, not only the version integer

sql · SQL-side identity checks
PRAGMA application_id;PRAGMA user_version;PRAGMA schema_version;SELECT name,typeFROM sqlite_schemaWHERE name IN ('site','device','maintenance_note','device_summary')ORDER BY type,name;PRAGMA table_xinfo('device');PRAGMA foreign_key_check;PRAGMA integrity_check;

A stale/corrupted/manually edited file can claim user_version=12 while missing required objects. Version metadata should select an expected schema; validation should confirm the expectation when opening untrusted or operationally important files.

Forward and backward compatibility are product decisions

SituationPossible policy
File version equals app versionOpen normally after identity/health checks.
File is older but has a tested migration pathBack up, migrate transactionally, verify, then update user_version.
File is newer than the application understandsUsually refuse writable open; consider explicit read-only compatibility only if designed/tested.
application_id does not matchTreat as a different file type even if SQLite can parse it.
Required schema mismatches claimed versionFail closed; do not blindly bump metadata.

Python file-identification script

python · inspect before handing off to migrations
from __future__ import annotationsimport sqlite3from pathlib import PathEXPECTED_APP_ID = 1179537236MAX_SUPPORTED_USER_VERSION = 12REQUIRED_TABLES = {"site", "device", "maintenance_note"}def inspect_fieldnotes(path: str) -> dict:    p = Path(path)    uri = p.resolve().as_uri() + "?mode=ro&cache=private"    con = sqlite3.connect(uri, uri=True)    try:        app_id = con.execute("PRAGMA application_id").fetchone()[0]        user_version = con.execute("PRAGMA user_version").fetchone()[0]        schema_version = con.execute("PRAGMA schema_version").fetchone()[0]        tables = {            row[0] for row in con.execute(                "SELECT name FROM sqlite_schema WHERE type='table'"            )        }        integrity = con.execute("PRAGMA integrity_check").fetchone()[0]        return {            "application_id": app_id,            "user_version": user_version,            "schema_version": schema_version,            "identity_ok": app_id == EXPECTED_APP_ID,            "version_supported": user_version <= MAX_SUPPORTED_USER_VERSION,            "required_tables_ok": REQUIRED_TABLES <= tables,            "integrity": integrity,        }    finally:        con.close()print(inspect_fieldnotes("fieldnotes.db"))

This script deliberately opens the candidate read-only. A later migration layer can decide whether to create a backup, acquire a write connection, migrate, and update user_version.

Lab: create one valid and one misleading file

sql · prove metadata alone is insufficient
-- Valid lab file metadata after creating the FieldNotes schema:PRAGMA application_id = 1179537236;PRAGMA user_version = 12;-- In a separate disposable file, create no FieldNotes tables but set the same metadata.PRAGMA application_id = 1179537236;PRAGMA user_version = 12;-- A robust inspector must still detect the missing required schema objects.

The point is the data contract: identity + supported version + expected schema + health checks. Any one field by itself is too weak.

Designing SQLite as an application document format

A well-designed SQLite-backed document format gains transactions, constraints, indexing, queryability, and portability. It also creates responsibilities: migration policy, backward compatibility, backup/recovery, concurrent access rules, extension requirements, and opening untrusted files safely.

Document-format decisionQuestion to answer
IdentityHow does the application distinguish its SQLite file from someone else's?
VersioningWhat does user_version mean, and which transitions exist?
CompatibilityCan old/new readers safely open the file? Read-only or writable?
ExtensionsDoes the file depend on FTS/JSON/custom collations/functions not present in every build?
IntegrityWhat checks run before/after migration or import?
RecoveryWhat backup/recovery process exists before destructive upgrades?

Verification checkpoint

Database-as-file-format checkpoint

Separate SQLite internal metadata from your application contract.

  1. What does application_id identify?
  2. Who interprets user_version?
  3. Why should schema_version not be your migration version?
  4. Why validate required schema even when user_version matches?
  5. What should an old application do with a newer file version?
  6. Why open an unknown candidate read-only first?
Review the answers

application_id is an application-managed file-type marker. user_version is stored by SQLite but interpreted entirely by your migration policy. schema_version is SQLite's internal schema cookie. Matching metadata does not prove schema integrity. Newer files should normally be rejected for writable use unless explicit compatibility exists. Read-only inspection reduces the chance of modifying the wrong/incompatible file before identity and compatibility checks finish.

Production judgment and Chapter 13 bridge

Chapter 12 closes the schema-level automation layer: stable read interfaces, controlled triggers, multi-file workflows, connection-open policy, and explicit file identity/versioning. Chapter 13 moves into semi-structured data. The same discipline will apply there: JSON is a representation choice inside SQLite, not an excuse to abandon relational constraints or application contracts.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.