Chapter 04 · SQLite’s Type System, Affinity, STRICT Tables, and Value Representation
Dates, Times, Booleans, Money, UUIDs, and Other Logical Types
Choose explicit SQLite representations for common logical types that do not have dedicated storage classes, based on query behavior, exactness, portability, and application contracts.
Learning outcomes
A logical type is the application meaning of a value: timestamp, Boolean flag, money amount, UUID, status code, and so on. SQLite does not need a separate storage class for every logical type. Instead, you choose one of its physical representations and define the conventions that make the value meaningful.
Choose among ISO-style TEXT, Unix-time INTEGER, and Julian-day REAL representations for dates/times based on workload needs.
Model Boolean values without inventing a nonexistent Boolean storage class.
Avoid using floating-point REAL as an automatic choice for exact money and design an explicit fixed-scale strategy.
Compare UUID-as-TEXT and UUID-as-16-byte-BLOB tradeoffs.
Write a representation contract that includes units, timezone, scale, validation, query behavior, and driver expectations.
Physical representation and logical meaning are different layers
The five storage classes tell SQLite how a runtime value is represented. They do not tell your application whether INTEGER 1 means “true,” one millisecond, one cent, one retry, or one enum member. A robust schema therefore documents both layers.
| Logical concept | Possible SQLite representation | Missing information you must define |
|---|---|---|
| Timestamp | TEXT / INTEGER / REAL | Timezone convention, precision, epoch/format, accepted range. |
| Boolean | INTEGER | Allowed values—normally 0 and 1—and NULL policy. |
| Money | INTEGER or carefully controlled TEXT | Currency, scale/minor unit, rounding rules, range. |
| UUID | TEXT or 16-byte BLOB | Canonical text form or byte order/serialization convention. |
| Status enum | TEXT or INTEGER | Allowed members and forward-compatibility policy. |
The design question is not “which SQLite type is the date type?” There is no dedicated date storage class. The question is “which representation best supports our comparisons, sorting, arithmetic, portability, debugging, and application drivers?”
Dates and times have three conventional SQLite representations
SQLite’s built-in date/time functions understand documented time-value formats rather than a dedicated DATETIME storage class. The three principal representations are ISO-8601-style TEXT, Julian-day REAL, and Unix-time INTEGER (or numeric Unix timestamps when functions are used with the appropriate modifier).
| Representation | Example | Strengths | Tradeoffs |
|---|---|---|---|
| ISO-style TEXT | 2026-08-12 05:00:00 or a documented UTC form | Human-readable; lexicographic order works when one canonical big-endian format is used; easy interchange. | Consumes more bytes; application must enforce one format/timezone/precision convention. |
| Unix-time INTEGER | 1786510800 seconds since Unix epoch | Compact; simple interval arithmetic; natural for systems already using epoch timestamps. | Unreadable by inspection; unit/precision and timezone interpretation must be documented. |
| Julian-day REAL | Approximately 2461264.7083 for the sample moment | Works naturally with SQLite Julian-day calculations and fractional days. | Floating-point representation; less common at application/API boundaries. |
SELECT date('2026-08-12') AS day_text, datetime('2026-08-12 05:00:00') AS date_time_text, julianday('2026-08-12 05:00:00') AS julian_value, unixepoch('2026-08-12 05:00:00') AS unix_seconds;On the documented Gregorian/UTC-oriented date-function model, the fixed example yields Unix seconds 1786510800 and a Julian-day value near 2461264.7083333335. Do not make tests depend on 'now' when teaching representation; fixed inputs are reproducible.
Choose a date contract before choosing convenience functions
For FieldNotes, suppose maintenance timestamps are exchanged with APIs, read by humans, and sorted frequently, while sub-second arithmetic is not a dominant requirement. Canonical UTC TEXT can be a reasonable choice—but only if the contract fixes the format.
CREATE TABLE IF NOT EXISTS maintenance_event ( event_id INTEGER PRIMARY KEY, device_code TEXT NOT NULL, occurred_at TEXT NOT NULL);INSERT INTO maintenance_event(device_code, occurred_at) VALUES ('PUMP-007', '2026-08-12T02:00:00Z'), ('PUMP-007', '2026-08-12T05:30:00Z');SELECT device_code, occurred_atFROM maintenance_eventORDER BY occurred_at;The ordering property depends on a consistent year-to-fractional-second layout and one timezone convention. Mixing local times, offsets, missing seconds, and different precision can defeat simple lexical reasoning even if each string looks date-like. If your workload centers on arithmetic or an external system already mandates epoch values, INTEGER may be the better contract.
A declared type of DATE in an ordinary table merely receives NUMERIC affinity. Even a STRICT TEXT column only enforces TEXT representation—not that the text is a valid timestamp. Format/domain validation needs explicit constraints or application validation designed for the accepted date grammar.
Boolean meaning is convention plus validation
SQLite has no separate Boolean storage class. The SQL keywords TRUE and FALSE normally act as aliases for integer 1 and 0. They improve readability, but they do not create a new runtime class.
SELECT TRUE, typeof(TRUE), FALSE, typeof(FALSE);DROP TABLE IF EXISTS device_flag;CREATE TABLE device_flag ( device_code TEXT PRIMARY KEY, is_active INTEGER NOT NULL CHECK (is_active IN (0,1))) STRICT;INSERT INTO device_flag VALUES ('PUMP-007', TRUE);INSERT INTO device_flag VALUES ('FAN-014', FALSE);SELECT device_code, is_active, typeof(is_active)FROM device_flagORDER BY device_code;The stored class is INTEGER. STRICT ensures the field is integer-compatible; the CHECK constraint narrows the domain to 0 or 1. Without that CHECK, INTEGER 2 would still be type-correct but would violate the intended Boolean contract.
Exact money needs an explicit exactness strategy
SQLite REAL values are floating point. Floating point is excellent for many measurements, but binary floating point cannot represent every decimal fraction exactly. If a currency amount must be exact to a defined minor unit, storing the smallest unit as INTEGER is often simpler and safer.
SELECT (0.1 + 0.2) = 0.3 AS exact_float_equality;DROP TABLE IF EXISTS service_charge;CREATE TABLE service_charge ( charge_id INTEGER PRIMARY KEY, currency TEXT NOT NULL, amount_minor INTEGER NOT NULL CHECK (amount_minor >= 0)) STRICT;-- 47.49 in a currency whose contract defines 100 minor units per major unit:INSERT INTO service_charge(currency, amount_minor)VALUES ('USD', 4749);SELECT currency, amount_minorFROM service_charge;The first query demonstrates why floating-point equality is not an exact-decimal contract: the expression evaluates false on ordinary IEEE representation. The integer strategy is only correct if you define the currency and scale. Some currencies do not use two decimal minor units, and some domains need more precision than currency cash units.
For domains requiring arbitrary or variable decimal precision, a canonical decimal TEXT representation plus an exact-decimal library in the application can be appropriate. Document normalization, scale, rounding, comparison, and arithmetic rules. A declaration such as DECIMAL(10,2) alone does not create exact decimal storage in ordinary SQLite.
UUIDs can be readable TEXT or compact 16-byte BLOBs
A UUID is logically a 128-bit identifier. SQLite has no UUID storage class, so a common choice is canonical 36-character text with hyphens or the corresponding 16 raw bytes. Neither choice is universally superior.
SELECT length('550e8400-e29b-41d4-a716-446655440000') AS text_chars, typeof('550e8400-e29b-41d4-a716-446655440000') AS text_type, length(x'550e8400e29b41d4a716446655440000') AS blob_bytes, typeof(x'550e8400e29b41d4a716446655440000') AS blob_type, lower(hex(x'550e8400e29b41d4a716446655440000')) AS blob_hex;The canonical text form is easy to log, copy, inspect, and interchange. A 16-byte BLOB is smaller and avoids textual punctuation, but every application/driver must agree on byte serialization and convert for display. If external APIs already use canonical UUID strings and database size is not dominated by UUID storage, TEXT can reduce integration friction.
| Question | TEXT UUID | 16-byte BLOB UUID |
|---|---|---|
| Human inspection | Excellent | Needs conversion such as hex() plus formatting. |
| Storage size of payload | Larger | 16 bytes for the raw identifier. |
| API interchange | Often direct | Requires encode/decode step. |
| Driver handling | String binding | Byte-array/BLOB binding. |
| Ordering semantics | Lexical order of chosen canonical text | Byte order of serialization; must be documented. |
Other logical types need the same design discipline
The pattern generalizes. A status can be TEXT with a CHECK list, or INTEGER with an explicit code map. An IP address can be canonical TEXT for readability or binary for compact network-aware processing. Durations can be INTEGER milliseconds, INTEGER nanoseconds, or REAL seconds depending on precision and arithmetic requirements. JSON receives dedicated treatment in Chapter 13 because JSON validity and query behavior add another layer.
| Logical type | Possible contract | Questions before choosing |
|---|---|---|
| Status | TEXT such as online/offline/maintenance | Will labels evolve? Do external systems share the same names? |
| Duration | INTEGER milliseconds | What precision and maximum range are required? |
| Temperature | REAL plus documented unit | Is approximate floating arithmetic acceptable? Celsius or Kelvin? |
| Version | TEXT | Is lexical sorting meaningful, or is semantic-version parsing required in the app? |
| Opaque token | BLOB or TEXT | Is it binary by definition, or encoded for transport/logging? |
Lab: define FieldNotes logical-type contracts
Build a small STRICT table whose physical types are simple but whose logical contract is explicit in column names and constraints. Then verify the runtime classes.
DROP TABLE IF EXISTS field_measurement;CREATE TABLE field_measurement ( measurement_id INTEGER PRIMARY KEY, device_code TEXT NOT NULL, observed_at TEXT NOT NULL, is_valid INTEGER NOT NULL CHECK (is_valid IN (0,1)), reading_value REAL, service_minor INTEGER CHECK (service_minor IS NULL OR service_minor >= 0), currency TEXT, trace_uuid TEXT NOT NULL) STRICT;INSERT INTO field_measurement( device_code, observed_at, is_valid, reading_value, service_minor, currency, trace_uuid) VALUES ( 'SENS-003', '2026-08-12T05:45:00Z', TRUE, 21.75, 1299, 'USD', '550e8400-e29b-41d4-a716-446655440000');SELECT typeof(observed_at), typeof(is_valid), typeof(reading_value), typeof(service_minor), typeof(trace_uuid)FROM field_measurement;Expected classes are TEXT, INTEGER, REAL, INTEGER, and TEXT. Then write the missing application contract beside the DDL: observed_at is canonical UTC ISO text; is_valid is 0/1; reading_value uses the unit defined for the device; service_minor is in the minor units of currency; and trace_uuid uses canonical lowercase-or-case-insensitive UUID text according to the API contract.
Logical-type checkpoint
Choose representations by requirements, not by habit.
- Why does a column declared DATE in an ordinary table not create date storage?
- What do TRUE and FALSE evaluate to in current SQLite?
- Why can INTEGER minor units be preferable to REAL for exact currency amounts?
- What integration benefit can UUID TEXT provide over a 16-byte BLOB?
- What additional information must accompany any numeric timestamp representation?
Review the answers
DATE is only a declared type name that derives affinity; TRUE/FALSE normally map to INTEGER 1/0; integer minor units avoid binary floating-point approximation for a fixed scale; UUID text is directly readable/interchangeable with many APIs; and numeric timestamps need a defined epoch, unit/precision, timezone interpretation, and range.
Failure patterns and safe corrections
| Failure | Diagnosis | Safe correction |
|---|---|---|
| Mixed timestamp formats sort strangely. | The application stored multiple textual layouts/timezone conventions. | Choose one canonical representation at the write boundary and migrate/validate legacy forms. |
| STRICT Boolean column stores 2. | INTEGER is type-correct; Boolean domain was never constrained. | Add CHECK (flag IN (0,1)) or equivalent domain validation. |
| Money totals differ by tiny fractions. | REAL was used as an exact decimal contract. | Use integer fixed-scale units or a documented exact-decimal strategy. |
| UUID BLOBs look different across services. | Byte serialization was not standardized. | Document canonical byte order/encoding or use canonical text at the interchange boundary. |
| A designer chooses a representation only because it is compact. | Query/debug/integration requirements were ignored. | Evaluate representation across storage, queries, drivers, portability, observability, and failure recovery. |
Summary and bridge to cross-language boundaries
SQLite gives you a small set of physical value classes and expects the application/schema contract to define higher-level meaning. Dates can be canonical TEXT, Unix-time INTEGER, or Julian-day REAL; Booleans are commonly 0/1 INTEGER values; exact money needs an explicit exactness/scale strategy; UUIDs can be readable TEXT or compact BLOBs. Lesson 5 follows these values across the final boundary: NULL, bytes, Unicode text, collations, and host-language adapters.