# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.6.1] - 2026-05-17

### Fixed

- **`Transaction.commit` skipped param normalisation, crashing on
  `surql.RecordID` bound params.** `DatabaseClient.execute` routes its
  incoming `params` dict through `_denormalize_params`, which converts
  `surql.types.record_id.RecordID` (surql-py's Pydantic wrapper) to
  `surrealdb.RecordID` (the SDK's native CBOR-encodable class) before
  the encoder sees them. `Transaction.commit` flushed buffered
  statements via `Transaction._raw_query` → `query_raw` (or the
  fallback `DatabaseClient.execute`) and passed `self._params` through
  unchanged — skipping the normalisation step entirely. The asymmetry
  meant callers who placed `surql.RecordID` values into bound params
  via `txn.execute(sql, {'foo': RecordID(...)})` crashed at commit
  with `('no encoder for type ', <class 'surql.types.record_id.RecordID'>)`,
  defeating the bound-param ergonomics inside transactions.

  Fix: route the `Transaction` param dict through the existing
  `_denormalize_params` helper inside `Transaction._raw_query` before
  invoking `query_raw` or the fallback `execute`. No change to
  `_denormalize_params` itself or to `DatabaseClient.execute`; the
  only behavioural change is plumbing the existing normalisation into
  the transaction path. The fallback `execute` branch double-normalises
  (`execute` runs `_denormalize_params` again internally), which is a
  no-op — `_denormalize_params` is idempotent on already-converted
  `surrealdb.RecordID` values (they are neither a `SurqlRecordID`,
  nor a regex-matching string, nor a dict/list, so they fall through
  to the final `return value`). Module docstring on
  `Transaction._raw_query` documents the symmetry with
  `DatabaseClient.execute` and the idempotency rationale.

  Regression coverage:
  `tests/test_connection.py::TestTransaction::test_commit_normalizes_surql_record_id_params`
  and `test_commit_normalizes_mixed_param_types` (4 new tests across
  two anyio backends; mixed-type test guards against an over-eager
  conversion damaging primitives, strings, lists, or nested dicts that
  share a params dict with a `RecordID`).

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2605 passed**
  (was 2601 in 1.6.0; +4 new regression tests covering the fix above).

## [1.6.0] - 2026-05-17

### Added

- **`conditions=` parameter on graph traversal helpers.** Six graph
  helpers — `traverse`, `traverse_with_depth`, `get_related_records`,
  `get_outgoing_edges`, `get_incoming_edges`, and `shortest_path` —
  now accept an optional `conditions: list[str | Operator] | None`
  keyword. Each entry is rendered through `Query.where(...)`, matching
  the exact shape that `query_records` already accepts; multiple
  conditions combine with AND. Pre-1.6.0 these helpers emitted bare
  `SELECT ... FETCH ...` with no WHERE hook, forcing any caller that
  needed row-level isolation (multi-tenant filtering, archived-flag
  exclusion, etc.) to roll a custom walker. The parameter is purely
  additive — defaulting `None` leaves the emitted SurrealQL byte-for-byte
  identical to 1.5.15. As part of the refactor, the four helpers that
  previously emitted hand-rolled f-string SQL now route through the
  `Query` builder so the WHERE composition is identical to the typed
  CRUD helpers' path. Regression coverage:
  `tests/test_query_graph.py::TestTraverseConditions` plus per-helper
  sibling classes (46 new tests).

### Fixed

- **Transaction commits silently swallowed mid-batch failures.** Prior
  to 1.6.0 `Transaction.commit()` flushed buffered statements as a
  single `BEGIN TRANSACTION; ...; COMMIT TRANSACTION;` request through
  the SDK's `query` method. SurrealDB v3 collapses that response to
  `None` regardless of whether the batch succeeded OR was rolled back
  server-side — verified against `surrealdb/surrealdb:v3.0.5` with
  `surrealdb==2.0.0a1`. The commit helper read `None` as success, so a
  single bad statement that triggered a rollback (type mismatch on a
  SCHEMAFULL field, assertion failure, FK violation) was
  indistinguishable from a clean commit. Callers saw "transaction
  committed", looked for their writes, and found nothing.

  Fix: inject a sentinel `RETURN '__txn_ok__';` statement immediately
  before the `COMMIT TRANSACTION` line, then route the commit RPC
  through the SDK's `query_raw` method instead of `query`. `query_raw`
  preserves the per-statement `{status, result}` envelope; the sentinel
  surfaces as `{'result': '__txn_ok__', 'status': 'OK'}` on success and
  is absent on rollback (the COMMIT entry is replaced with
  `status: 'ERR'`, `details.kind: 'Cancelled'`). The commit method now
  inspects the envelope and raises `TransactionError` with the
  server's per-statement explanation when any statement has
  `status == 'ERR'` OR the sentinel is missing. State machine
  transitions to `CANCELLED` (not `COMMITTED`) on this path.

  Defensive fallback: when the underlying SDK does not expose
  `query_raw` (older alpha versions, vendored forks), the commit falls
  through to the prior `execute` path and logs a warning — silent
  swallow is preserved only on SDK versions we cannot probe. The
  module docstring and this entry document the limitation; the
  warning string names the cause so operators can correlate it with
  an SDK pin.

  Regression coverage:
  `tests/test_connection.py::TestTransaction::test_commit_returns_user_statement_results`,
  `test_commit_raises_on_mid_batch_failure`,
  `test_commit_raises_when_sentinel_absent`, and
  `test_commit_falls_back_when_query_raw_unavailable` (8 new tests
  across two anyio backends).

### Changed

- **Typed-vs-untyped CRUD documentation clarified.** Investigation
  revealed the typed (`create_typed`, `update_typed`, `upsert_typed`,
  `get_typed`, `query_typed`) and untyped (`create_record`,
  `update_record`, `upsert_record`, `get_record`, `query_records`)
  surfaces are partial duplicates with deliberate asymmetries, not
  full aliases:

  - `create_typed` / `update_typed` / `upsert_typed` differ from their
    untyped siblings in return type only — untyped returns
    `dict[str, Any]`, typed revalidates the response into the model
    instance's class and returns `T`.
  - `get_typed` is a thin alias of `get_record` (the untyped helper
    already returns `T | None` when given a `model: type[T]`).
  - `query_typed` is NOT a typed variant of `query_records`: the
    former runs hand-written SurrealQL, the latter uses the `Query`
    builder. They serve different use cases.

  Module-level docstrings in both `src/surql/query/crud.py` and
  `src/surql/query/typed.py` now carry a "when to use which" section
  pointing at the canonical sibling and naming the asymmetries
  explicitly. Per-function docstrings cross-reference the alternative
  helper. No code change — both surfaces remain supported.

### Verified

- **Sentinel-probe assumption verified against live SurrealDB v3.0.5.**
  Direct HTTP `/sql` shows the sentinel surfaces as
  `{'result': '__txn_ok__', 'status': 'OK'}` on success and the
  statement list contains `status: 'ERR'` rows on rollback. The Python
  SDK's `query` method collapses both shapes to `None`; the SDK's
  `query_raw` method preserves the full envelope. The commit path
  routes through `query_raw` and is therefore observationally
  equivalent to direct HTTP for the purposes of mid-batch error
  detection.
- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues, 81 source files.
- `uv run pytest --no-cov --ignore=tests/integration` — **2601 passed,
  9 skipped, 2 xfailed** (was 2547 in 1.5.15; +54 new regression tests
  covering the three items above).

## [1.5.15] - 2026-05-17

### Fixed

- **Migration generator emitted a wall-clock timestamp in the file docstring (#93).**
  `generate_initial_migration` and `create_blank_migration` wrote
  `Generated: {datetime.now(UTC).isoformat()}` into the docstring header of
  every generated file. Sub-second precision meant regenerating an unchanged
  schema produced a byte-different file every time — any "regen + diff against
  the checked-in migration" CI gate (the standard reproducibility check)
  reported spurious diffs even when no schema had changed.

  Fix: drop the `Generated:` line entirely. The information was redundant —
  the version-derived filename (`YYYYMMDD_HHMMSS_*.py`) already encodes when
  the migration was generated, and `_migration_history.applied_at` records
  when it was actually applied. The squash code path (`generate_squashed_migration`)
  keeps its `Generated:` line; it's a single-purpose op whose timestamp is
  genuinely meaningful audit data, not background CI noise.

  Regression coverage: `test_generate_initial_migration_is_deterministic`.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2547 passed, 9 skipped, 2 xfailed**.

## [1.5.14] - 2026-05-17

### Added

- **Typed `record<target>` field emission for cross-table introspection (#92).**
  Until now, `field('author', FieldType.RECORD, value='type::record("user", $value)')`
  emitted `DEFINE FIELD author ... TYPE record VALUE type::record("user", $value)`
  — bare `TYPE record` plus a VALUE coercion. SurrealDB stored the link correctly,
  but the type system carried no signal of WHICH table the field links to, so
  introspection-driven tools (Surrealist's Table Graph designer, ERD generators,
  any code reading `INFO FOR TABLE`) couldn't render the cross-table arrow.

  Two new behaviors:

  - **Explicit:** `field(name, FieldType.RECORD, target_table='user')` emits
    `DEFINE FIELD name ON TABLE ... TYPE record<user>;` (`option<record<user>>`
    when `nullable=True`). No `VALUE` clause — the parameterized type enforces
    the same constraint.
  - **Auto-detection (backward-compat):** when `target_table` isn't passed but
    `value=` matches the canonical `type::record("X", $value)` coercion pattern,
    the emitter lifts X into `target_table=X` automatically and drops the
    now-redundant VALUE clause. Existing schemas that used the `value=`
    convention get the typed form on the next migration with zero code changes.

  Non-canonical `value=` expressions (e.g. `value='string::lowercase($value.id)'`)
  are left untouched — only the exact `type::record("X", $value)` shape triggers
  the upgrade, so callers who deliberately wrote a custom coercion keep their
  VALUE clause.

  Applies to both `schema/sql.py::_generate_field_sql` (initial DEFINE FIELD)
  and `migration/diff.py::_field_to_sql` (incremental ADD FIELD diffs). 7
  regression tests in `tests/test_schema_sql.py::TestTypedRecordFields` and
  `tests/test_migration_generator.py::TestAddFieldBackfillSQL`.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2546 passed, 9 skipped, 2 xfailed**.

## [1.5.13] - 2026-05-17

### Fixed

- **`db.select` / `db.merge` / `db.update` / `db.delete` mishandled bracketed
  record-id targets emitted by `str(RecordID(...))` (#91).** After 1.5.11
  switched `RecordID.__str__` to unicode `⟨⟩` brackets for v3 compatibility,
  callers like `get_record(table, RecordID(table, 'a-b'), Model)` produced a
  target string `'table:⟨a-b⟩'`. `db.select` was passing the bracketed inner
  `'⟨a-b⟩'` as the `$id` param to `type::record($table, $id)`, so SurrealDB
  looked up `table:'⟨a-b⟩'` (a different, non-existent record) and returned
  None. Same hazard existed for `db.merge/update/delete` paths where the SDK's
  string parser doesn't understand the bracketed form. The fix:

  - Extended `_RECORD_ID_BRACKETED_PATTERN` to match both ASCII `<>` and
    unicode `⟨⟩` brackets.
  - Added `_strip_record_id_brackets` (used by `_denormalize_params` and
    `db.select`'s `type::record` dispatch) so the inner id is stripped of
    brackets before lookup.
  - Added `_normalize_target` (used by `db.merge/update/delete`) which
    converts any record-id-shaped string target into an `SdkRecordID`
    object so the SDK gets a type it understands instead of trying to
    parse the bracketed string itself.

  Surfaced immediately when downstream consumers started using `get_record`
  + `merge_record` for entities with hyphenated record ids.

- **`_denormalize_params` didn't recognize surql's own `RecordID` (#91).** When
  callers built `merge_record(table, id, {'community': RecordID(...)})`, the
  surql `RecordID` wrapper sailed straight through `_denormalize_params` and
  hit the SDK's CBOR encoder, which raised
  `('no encoder for type ', <class 'surql.types.record_id.RecordID'>)`.
  Added a `SurqlRecordID -> SdkRecordID` conversion branch so callers can hand
  the helpers either a string or a `RecordID` instance and have either work.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2539 passed**, 9 skipped, 2 xfailed.

## [1.5.12] - 2026-05-17

### Fixed

- **`_quote_value(None)` emitted SurrealQL `NULL` but `TYPE option<X>` columns
  require `NONE` (#89).** SurrealDB v3 strictly distinguishes NONE (absence of
  value) from NULL (explicit null value); `option<X>` fields reject NULL with
  `Expected 'none | X' but found 'NULL'`. Python `None` semantically maps to
  "no value", so NONE is the correct serialization. Affects `_quote_value`,
  `Eq('field', None)` (now renders `field = NONE`), and `value(None)`. Callers
  who want a genuine NULL comparison should use the `IsNull` operator (already
  exists and is unchanged). Surfaced immediately when downstream consumers
  started using the helpers for nullable string/array fields.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2539 passed, 9 skipped, 2 xfailed**.

## [1.5.11] - 2026-05-17

### Fixed

- **`RecordID.__str__` emitted ASCII `<>` brackets that SurrealDB v3 rejects (#87).**
  Every record id with special characters (hyphens, dots, colons) was being
  formatted as `f'{table}:<{id}>'`, and SurrealDB's v3 parser fails on the
  ASCII `<` with `Unexpected token \`<\`, expected a record-id key`. So
  every `upsert_record`, `Query().upsert(...)`, `merge_record`, etc. call
  that built its target via `str(RecordID(...))` for a composite id was
  silently producing invalid SQL. The fix emits unicode brackets U+27E8 /
  U+27E9 (`⟨⟩`) which v3's parser accepts. `RecordID.parse` now accepts
  BOTH ASCII and unicode forms for backward-compat reads.

- **`_quote_value` serialized lists and dicts as quoted strings (#87).**
  Falling through to `f"'{str(value)}'"` meant `[1, 2, 3]` became the
  literal SurrealQL string `'[1, 2, 3]'` and `{'k': 'v'}` became
  `"{'k': 'v'}"`. SurrealDB then rejected these against `TYPE array` /
  `TYPE object` columns. The fix emits proper SurrealQL array literals
  (`[1, 2, 3]`) and object literals (`{k: 'v'}`), recursing through
  `_quote_value` for nested values. Also added an explicit `RecordID`
  branch that emits the record-id literal directly (e.g. `user:alice`,
  `spec:⟨BFS:community:1⟩`) instead of quoting it as a string.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2539 passed, 9 skipped, 2 xfailed**.

## [1.5.10] - 2026-05-17

### Fixed

- **`_denormalize_params` over-coerced composite string IDs into RecordIDs (#85).**
  The record-id detection regex accepted any `<word>:<rest>` shape, which
  meant Cosmos-style composite identifiers (`BFS:community:1`,
  `tenant:env:version`, `{org}:{repo}:{branch}`) were being silently
  rewritten as `RecordID('BFS', 'community:1')` and rejected by SurrealDB
  at the schema layer with `Couldn't coerce value for field `x`: Expected
  `string` but found `BFS:community:1``. The id portion is now restricted
  to `[^:\s/]+` (no extra colons, whitespace, or slashes), and the
  angle-bracketed form `table:<id>` is matched by a parallel pattern that
  treats anything inside the brackets as the id — including colons —
  preserving SurrealDB v3's `community:<a:b:c>` escape hatch. Pattern
  detection is still best-effort; callers wanting an unambiguous record
  reference should pass `RecordID(table, id)` or `RecordRef(table, id)`
  directly.

  Regression coverage: `test_composite_string_id_not_record_id`,
  `test_angle_bracketed_id_with_colons_still_record_id`.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2537 passed, 9 skipped, 2 xfailed**.

## [1.5.9] - 2026-05-17

### Fixed

- **`migrate_up` silently swallowed per-statement errors inside the BEGIN/COMMIT
  transaction wrapper (#83).** The executor used to flush every migration as a
  single `BEGIN TRANSACTION; <stmts>; COMMIT TRANSACTION;` RPC for atomicity.
  surrealdb-py returns `null` for a partially-failed batched transaction (the
  per-statement error list never reaches the caller), so the executor saw no
  exception, wrote the history row, and logged `applied_count=1` while the
  schema was effectively empty. The fix runs statements individually so each
  failing statement raises a `QueryError` the executor wraps as
  `MigrationExecutionError(..., 'Failed to execute statement N ...')`. The
  history row is not written on failure.

  Trade-off: a mid-migration failure no longer rolls back earlier statements.
  For an additive migration the resulting state is incomplete but recoverable —
  fix the offending statement, drop the partial schema, re-run. The visibility
  win matters far more than the atomicity loss for the failure case, and the
  success case is unchanged.

  Regression coverage: `test_execute_migration_surfaces_midbatch_failure_no_history_write`.

### Verified

- `ruff check src tests` + `ruff format --check src tests` — clean.
- `mypy src --strict` — no issues.
- `uv run pytest --no-cov --ignore=tests/integration` — **2535 passed, 9 skipped, 2 xfailed**.

## [1.5.8] - 2026-05-16

### Added

- **`field(..., nullable=True)`** — emits `TYPE option<X>` instead of `TYPE X`
  so a SCHEMAFULL column accepts NONE. Without this knob, every CREATE that
  omits an optional column fails on SurrealDB v3 with
  `Couldn't coerce value for field <x>: Expected <type> but found NONE`.
  Applies to both `schema/sql.py` (initial DEFINE FIELD) and
  `migration/diff.py` (incremental ADD FIELD diffs). Default is `False`, so
  existing code is unaffected.

### Fixed — SurrealDB v3 grammar compatibility

- **Table PERMISSIONS clauses emitted invalid SurrealDB SQL.** `generate_table_sql`
  was emitting `DEFINE FIELD PERMISSIONS FOR SELECT ON TABLE x WHERE ...` for
  each permission action — not valid grammar in any SurrealDB version. SurrealDB
  rejected migrations with `Parse error: Unexpected token FOR, expected ON`.
  Permissions now fold into the `DEFINE TABLE` statement itself per the v3
  grammar: `DEFINE TABLE x SCHEMAFULL PERMISSIONS FOR select WHERE ... FOR create WHERE ...;`
  Action keys are emitted lowercase regardless of input casing.
- **Edge PERMISSIONS clauses were silently dropped.** `generate_edge_sql`
  ignored `EdgeDefinition.permissions` entirely. Edges built with
  `with_edge_permissions(...)` now correctly emit permissions on their
  `DEFINE TABLE ... TYPE RELATION ... PERMISSIONS ...;` statement.
- **Migration diff had the same broken PERMISSIONS SQL in parallel.**
  `_generate_modify_permissions_diff` emitted the invalid `DEFINE FIELD
  PERMISSIONS` form; `_generate_add_table_diffs` failed to fold permissions
  into the initial `DEFINE TABLE`. Both paths now use a shared
  `_permissions_clause_sql` helper and emit valid v3 syntax. Permission
  changes re-DEFINE the table (SurrealDB has no `ALTER TABLE`); rollback
  re-DEFINEs with the old permissions.
- **`type::thing(table, id)` was removed in SurrealDB v3.** Calling it now
  raises `Invalid function/constant path, did you maybe mean ``type::record```.
  The two-arg form `type::record(table, id)` IS the v3 constructor (verified
  against v3.0.4). Switched every emission: `RecordRef.to_surql`,
  `surreal_fn.type_record` / `type_thing`, `DatabaseClient.select`'s
  record-id dispatch (`SELECT * FROM type::record($table, $id)`), and
  `migration.history.record_migration`'s `CREATE type::record($table, $id) SET ...`.
  Earlier doc comments claiming `type::record(value, type)` was coercion-only
  described pre-v3 alpha behavior; v3.0+ uses constructor semantics for the
  two-arg form. `type_thing()` is kept as a deprecated alias that emits the
  same v3 output as `type_record()`.

### Fixed — docs / CI / record-ID coercion (originally scoped for 1.5.7)

- **Record-ID auto-coercion false positive.** `DatabaseClient` parameter
  normalization was coercing ordinary prose strings that merely contained
  a `word:word` substring into SurrealDB record references. Detection is
  now anchored so only genuine record-id literals are converted; plain
  text passes through untouched. (Already included; landed via PR #81 →
  main as part of 1.5.7.)
- **Docs**: fixed broken `v3-patterns.md` anchor links in `migration.md`
  and `query-ux.md` — the `#record-id-construction-...` fragment did not
  match the generated heading id, so `mkdocs build --strict` reported it.
- **Docs site**: `site_name` corrected to `surql-py` (was `surql`, so
  every page title and the site header showed the wrong project name);
  the description now notes the Python port.

### Changed

- **CI**: `ci.yml` runs only on pull requests (not on push); the nightly
  cron is disabled (`workflow_dispatch` only); `docs.yml` runs
  `mkdocs build --strict` on PRs so doc breakage is caught before merge.
  Fork-PR CI runs now route to `ubuntu-latest` runners explicitly.

### Tests

- Strengthened permissions test coverage: existing `test_table_with_permissions`
  was too lenient (`'FOR SELECT' in sql` matched the broken output). Added
  explicit equality assertions plus regression guards that
  `DEFINE FIELD PERMISSIONS` never appears, action keys are always
  lowercased, and edges with permissions render their PERMISSIONS clause.
- Updated all `type::thing` test assertions to expect the v3 `type::record`
  emission.
- Marked two `TestRecordMigrationAgainstEmbeddedDb` cases as `xfail` because
  the embedded `surrealdb` Python SDK (latest PyPI 2.0.0) still ships pre-v3
  grammar where `type::record(value, type)` is coercion. Production users run
  v3.0+ servers and get the correct constructor behavior; the xfail covers
  the SDK version skew, not a regression in our code. Re-enable once the
  Python SDK ships a v3-grammar wheel.

### Verified

- `ruff check src tests` and `ruff format --check src tests` — all clean.
- `mypy src --strict` — success, no issues found in 81 source files.
- `uv run pytest --no-cov --ignore=tests/integration` — **2531 passed, 9 skipped, 2 xfailed**.
- End-to-end verified against a live SurrealDB v3.0.4 Docker container by a
  downstream consumer: `migrate up` applied 17 tables + 1 `TYPE RELATION`
  edge with PERMISSIONS clauses; `INFO FOR DB` echoes
  `PERMISSIONS FOR select, create, update, delete WHERE tenant_id = $auth.tenant`
  on every table.

## [1.5.7] - 2026-05-05

### Fixed

- **`DatabaseClient` survives upstream SurrealDB recreate.** When the
  SurrealDB server is restarted mid-flight (docker `compose up -d
  surrealdb`, k8s pod restart, etc.), the SDK's WebSocket throws
  `'no close frame received or sent'` on every queued query and the
  pre-fix client never recovered until the host process restarted —
  consumers had to bounce every dependent service after a DB recycle.
  `execute` / `select` / `create` / `update` / `merge` / `delete` /
  `insert_relation` now classify transport-level disconnects via
  `_is_disconnect_error`, redial under a per-client `asyncio.Lock`
  (so concurrent failed callers coalesce on a single reconnect), and
  retry the call exactly once. Non-transport errors (bad SQL, schema
  violations, timeouts) still raise `QueryError` immediately.

## [1.5.6] - 2026-05-02

### Fixed

- **Migration runner / record-id construction (`type::record` vs
  `type::thing`).** `record_migration` and `DatabaseClient.select('table:id')`
  were emitting `type::record($table, $id)` to construct a record id, but
  in SurrealDB v3 the two-arg form of `type::record(value, type)` is a
  *type coercion* (cast `value` into `record<type>`), NOT a table+id
  constructor. Calling `type::record('_migration_history', '0001_init')`
  is interpreted as "coerce `'_migration_history'` into
  `record<0001_init>`" and fails with `Expected a record<0001_init> but
  cannot convert '_migration_history' into a record<0001_init>`. Replaced
  both call sites with `type::thing($table, $id)`, which is the actual
  record-id constructor. This blocked any downstream consumer from running
  migrations with surql-py 1.5.0--1.5.5.
- The `RecordRef` / `record_ref()` and `type_record()` Python helpers also
  rendered `type::record('table', id)` -- they now emit
  `type::thing('table', id)`. The Python function names are preserved for
  source compatibility (`type_record` is now an alias for `type_thing`).
- Updated `docs/v3-patterns.md`, `docs/migration.md`, `docs/query-ux.md`,
  `docs/api/index.md`, and `README.md` to reflect that `type::thing` is
  the constructor on both v2 and v3 (correcting the earlier claim that
  v3 renamed `type::thing` to `type::record`).
- Added `tests/test_migration_history.py::TestRecordMigrationAgainstEmbeddedDb`
  which round-trips `record_migration` through an embedded `mem://`
  SurrealDB so future regressions of this kind are caught locally without
  a Docker container.

## [1.5.5] - 2026-05-02

### Added

- **Polling LIVE-query fallback for the embedded SurrealDB engine.** The
  upstream `surrealdb` Python SDK's `AsyncEmbeddedSurrealConnection`
  inherits `live()` / `subscribe_live()` from the WebSocket connection but
  never initialises the `live_queues` attribute and the underlying Rust
  extension (`_surrealdb_ext.AsyncEmbeddedDB`) only exposes a one-shot
  `execute(cbor) -> bytes` call -- so calling `client.live(...)` on a
  `surrealkv://` / `mem://` / `file://` URL crashes with
  `AttributeError: 'AsyncEmbeddedSurrealConnection' object has no attribute
  'live_queues'`.

  `DatabaseClient.connect()` now detects embedded URLs (any of `mem://`,
  `memory://`, `file://`, `surrealkv://`, `rocksdb://`, `tikv://`) and
  installs a new `EmbeddedPollingStreamingManager` instead of the WS-native
  `StreamingManager`. The polling manager exposes the same surface
  (`live` / `subscribe` / `subscribe_with_callback` / `kill` / `kill_all` /
  `get_active_queries`) so existing call sites do not branch on engine
  type. It re-runs `SELECT * FROM <table>` on a configurable cadence
  (default 0.25s / 4 Hz) and emits `{'action': 'CREATE', 'result': <row>}`
  notifications for record ids that haven't been seen before, primed on
  the first tick so historical rows do not flood the consumer. Two new
  `ConnectionConfig` knobs control the fallback: `live_poll_interval_s`
  (default `0.25`) and `live_poll_max_seen_ids` (default `10_000`,
  bounded LRU).

  This is a degraded mode -- only CREATE events are produced; UPDATE and
  DELETE are not observable; latency is bounded by the poll interval --
  but it lets downstream consumers (e.g. tinytropolis's LIVE-channels
  dashboard pipeline) keep their embedded-database design constraint
  instead of having to spin up a sidecar `ws://` SurrealDB. Public
  re-exports: `EmbeddedPollingStreamingManager` and `is_embedded_url` from
  `surql.connection`.

## [1.5.4] - 2026-05-02

### Added

- `DatabaseClient.streaming` public property and `DatabaseClient.live(table,
  diff=False)` convenience method so callers no longer need to reach into
  the private `_streaming` attribute to start LIVE SELECTs. The property
  raises `ConnectionError` if the client is not connected and
  `StreamingError` if live queries are disabled on the connection.

## [1.5.3] - 2026-05-02

### Security

- **Transitive dependency floors for Dependabot advisories.** Added explicit
  minimum-version constraints to `[project.dependencies]` so uv resolves
  patched versions of packages that surql does not import directly but
  pulls in via `surrealdb`, `typer`, and the docs extra:
  - `aiohttp >= 3.13.4` (header injection, multipart bypass, SSRF, DNS DoS;
    one high + multiple medium / low advisories)
  - `pygments >= 2.20.0` (ReDoS in lexer)
  - `requests >= 2.33.0` (.netrc credential handling)
  - `urllib3 >= 2.6.3` (redirect / proxy handling; high severity)
  - `pytest >= 9.0.3` in the dev group (medium advisory)

  No public API or runtime behavior changes; lockfile-only impact.

## [1.5.2] - 2026-05-02

### Fixed

- **`_denormalize_params` URL false positive (silent CREATE/UPDATE failures).**
  The record-ID detection regex `^[a-zA-Z_][a-zA-Z0-9_]*:.+$` matched URL
  schemes like `http://`, `https://`, `ws://`, `wss://`, `file://`, and
  silently coerced URL strings into `RecordID` objects. SurrealDB returned a
  coerce error in the result text of an otherwise OK-status query response,
  which the wrapper swallowed -- writes that included URL params reported
  success but never persisted. Added a negative lookahead `(?!//)` after
  the colon so URL strings round-trip unchanged. Discovered while debugging
  per-workspace embedder configuration in a downstream consumer
  (`base_url='http://10.0.0.51:11434'` was being rewritten to
  `RecordID('http', '//10.0.0.51:11434')`).

## [1.5.1] - 2026-04-18

### Documentation

- **v3 patterns page** (`docs/v3-patterns.md`): datetime cast, `count() GROUP ALL`,
  `type::record` vs `type::thing`, buffered `BEGIN`/`COMMIT`, idempotent DDL, graph
  depth unrolling, and v3 integration CI setup.
- **Query UX helpers page** (`docs/query-ux.md`): before/after examples for every
  1.5.0 helper -- `type_record`/`type_thing`, function factories
  (`time_now_fn`, `math_*_fn`, `string_*`, `count_if`), result aliases
  (`extract_many`, `has_result`), `aggregate_records`, and the `Query.set(...)` /
  deferred-`update()` / expression-aware `select(...)` builder extensions.
- **Upgrade notes page** (`docs/migration.md`): 1.3.1 -> 1.4.0 -> 1.5.0 -> 1.5.1
  call-site migration guide.
- **CLI reference**: documented the `surql orchestrate deploy | status | validate`
  subcommands and added a command-group matrix to the overview.
- **README**: refreshed top-level examples to use the 1.5.0 first-class helpers.
- **Navigation**: new pages added to `mkdocs.yml` under `Getting Started` and
  `Guides`.

### Fixed

- Aligned `surql.__version__` with `pyproject.toml`.

## [1.5.0] - 2026-04-16

### Added

- **Record-ID helpers** (#47 / #2): `type_record(table, id)` and
  `type_thing(table, id)` return `SurrealFn` wrappers. Prefer `type_record` on
  v3; `type_thing` remains supported for v2 targets.
- **SurrealFn function factories** (#47 / #3): `time_now_fn`, `math_mean_fn`,
  `math_sum_fn`, `math_min_fn`, `math_max_fn`, `math_ceil_fn`, `math_floor_fn`,
  `math_round_fn`, `math_abs_fn`, `string_len`, `string_concat`, `string_lower`,
  `string_upper`, `count_if`. Each composes with `Query.set(...)`,
  `Query.select([...])`, and `aggregate_records(select={...})` without raw
  SurrealQL strings.
- **Result extraction aliases** (#47 / #4): `extract_many` (alias for
  `extract_result`) and `has_result` (alias for `has_results`) for naming that
  reads naturally next to `extract_one` / `extract_scalar`.
- **`aggregate_records`** (#47 / #1): typed `SELECT ... GROUP BY | GROUP ALL`
  helper that returns a list of dicts, hiding the SurrealDB response envelope.
- **Builder API**: `Query.set(**fields)` for fluent `SET` clauses, optional
  `data` argument on `Query.update(target, data=None)`, and projection items in
  `Query.select(...)` may now be `SurrealFn` / `Expression` instances (anything
  with `.to_surql()`).
- **Pre-push hook**: shipped `.githooks/pre-push` mirroring CI checks (ruff,
  mypy, pytest) with optional v3 integration opt-in. See `CONTRIBUTING.md` for
  setup.

## [1.4.0] - 2026-04-15

### Added

- **SurrealDB v3 support**: library now emits SurrealQL accepted by both v2 and
  v3 servers. No public Python API changes.
- **Datetime cast on `_migration_history`**: `record_migration()` emits
  `applied_at = <datetime> $applied_at` so v3 accepts the bootstrap insert.
- **Buffered `BEGIN`/`COMMIT`**: `DatabaseClient.execute()` now batches
  transaction-scoped statements into a single RPC frame so v3 honours the
  commit.
- **`GROUP ALL` on `count_records`**: `count_records()` appends `GROUP ALL` and
  accepts both envelope and bare-list response shapes.
- **`type::record`**: select record-ID targets route through `type::record(...)`
  on v3 (with `type::thing` still accepted for backwards compatibility).
- **Idempotent DDL**: `DEFINE TABLE _migration_history IF NOT EXISTS` and the
  `if_not_exists` flag across the schema generator so `surql migrate up` is safe
  to re-run.
- **Graph depth unrolling**: `traverse(...)` unrolls `{min..max}` graph-depth
  ranges into literal hop unions that v3 accepts.
- **SDK pin**: minimum `surrealdb` SDK bumped to v2.0.0a1 (speaks v3's RPC
  protocol).
- **CI**:
  - `v3-integration.yml` runs the integration suite against
    `surrealdb/surrealdb:v3.0.5` on every push.
  - Nightly matrix runs against `surrealdb/surrealdb:latest`.
  - Dependabot for pip + github-actions.
  - Conventional-commit PR title lint.
  - Dependency-review workflow.
  - Scheduled `pip-audit` security workflow.

### Fixed

- Narrow `is_migration_applied` probe to a targeted query (avoids false
  negatives on fresh databases).
- Narrow migration-table-missing probe to `does not exist` errors (other errors
  now propagate).

## [1.3.1] - 2026-04-13

### Fixed

- **Embedded migration execution**: Migrations now run end-to-end against embedded
  engines (`mem://`, `memory://`, `file://`, `surrealkv://`). Previously
  `execute_migration()` wrapped statements in `BEGIN TRANSACTION;` / `COMMIT
  TRANSACTION;`, which crashes with `IndexError: list index out of range` on
  embedded connections because the upstream `surrealdb` Python SDK's `query()`
  returns an empty result list for transaction-control statements in embedded
  mode. `execute_migration()` now detects embedded URL schemes on the client
  and skips the transaction wrapper. Migrations remain effectively atomic in
  embedded mode because the engine lives in the application process, so a
  crash during migration takes the whole process with it rather than leaving a
  partial remote schema.

## [1.3.0] - 2026-04-01

### Added

- **Aggregation functions** (#1): `math_mean()`, `math_sum()`, `math_max()`, `math_min()` for
  SurrealQL aggregation queries. Compose with `as_()` aliases and `group_by()`/`group_all()` clauses.
- **Query builder GROUP ALL** (#1): `group_all()` method on `Query` for full-table aggregation.
- **Record references** (#2): `record_ref(table, id)` generates `type::record('table', 'id')` expressions
  that render as raw SurrealQL in CRUD operations, not quoted as strings.
- **SurrealDB function values** (#3): `surql_fn(name, *args)` for passing server-side functions
  (time::now(), math::sum, etc.) as field values in CREATE/UPDATE operations.
- **Result extraction integration tests** (#4): Comprehensive tests for `extract_result()`,
  `extract_one()`, `extract_scalar()` against realistic SurrealDB response formats.
- **Embedded connection URLs**: `ConnectionConfig.validate_url` now accepts the full set of
  schemes supported by the underlying `surrealdb` SDK, including the embedded engines
  `mem://`, `memory://`, `file://`, and `surrealkv://`. `enable_live_queries=True` is now
  compatible with embedded engines (they run in-process), and only rejected for `http://` /
  `https://`. Enables edge/device deployments where each host owns its own SurrealDB
  instance without a separate server process.

## [1.2.1] - 2026-03-20

### Fixed

- **RecordID round-trip denormalization**: Added `_denormalize_params()` that recursively
  converts record ID strings (e.g. `'repo:abc123'`) back to `surrealdb.RecordID` objects
  before sending to the SDK. Applied to `create()`, `update()`, `merge()`, `execute()`, and
  `insert_relation()` input data/params. This fixes the round-trip where normalized response
  IDs (strings) were rejected by SurrealDB 3.x when passed back as field values in subsequent
  operations ("Expected `record` but found string")

### Testing

- Added 18 tests covering `_denormalize_params` unit behavior, round-trip identity,
  and input denormalization verification across all CRUD methods

## [1.2.0] - 2026-03-20

### Fixed

- **select() single-record unwrap**: `DatabaseClient.select()` now detects record ID targets
  (e.g. `user:alice`) and unwraps the single-element list returned by the SurrealDB 3.x SDK,
  returning a dict (or None) instead of a list for single-record selects
- **SDK RecordID normalization**: All `DatabaseClient` CRUD responses (`create`, `select`,
  `update`, `merge`, `delete`, `execute`, `insert_relation`) now recursively normalize
  SurrealDB SDK `RecordID` objects to plain strings, preventing type coercion errors when
  consumers pass returned IDs back as field values in subsequent operations

### Testing

- Added 38 tests covering `_is_record_id_target`, `_normalize_sdk_value`, single-record
  select unwrapping, and SDK type normalization across all CRUD methods

## [1.1.0] - 2026-03-19

### Added

- **HNSW vector indexes**: `IndexType.HNSW` for SurrealDB's HNSW (Hierarchical Navigable
  Small World) approximate nearest-neighbor index, the successor to MTREE in SurrealDB 2.x/3.x
- **HnswDistanceType enum**: 8 distance metrics -- CHEBYSHEV, COSINE, EUCLIDEAN, HAMMING,
  JACCARD, MANHATTAN, MINKOWSKI, PEARSON (superset of MTreeDistanceType)
- **hnsw_index() builder**: Convenience function for creating HNSW index definitions with
  dimension, distance metric, vector type, and optional EFC/M tuning parameters
- **HNSW SQL generation**: `generate_table_sql()` and `generate_edge_sql()` emit correct
  `DEFINE INDEX ... HNSW DIMENSION <n> DIST <dist> TYPE <type> [EFC <n>] [M <n>]` syntax
- **HNSW parsing**: Schema parser detects and extracts HNSW indexes with all parameters
  (dimension, distance, vector type, EFC, M) from SurrealDB INFO responses
- **HNSW validation**: Schema validator checks HNSW dimension, distance metric, vector type,
  EFC, and M parameters for code-vs-database consistency
- **HNSW migration diffs**: `diff_indexes()` generates correct forward/backward SQL for
  adding and dropping HNSW indexes
- **HNSW example**: `docs/examples/hnsw_vector_search.py` demonstrating HNSW usage with
  OpenAI embeddings, multiple distance types, and EFC/M tuning

### Testing

- **Test coverage**: 2215 tests passing (up from 2191 in 1.0.0), 9 skipped
- New test suite: `test_hnsw_diff.py` (24 tests covering SQL generation, add/drop diffs,
  all 8 distance types, EFC/M parameters, mixed index type diffs, error cases)

---

## [1.0.0] - 2026-03-13

### Added

- **Vector search threshold**: `vector_search()` now accepts a `threshold` parameter for
  MTREE similarity filtering, generating `<|K,DISTANCE,threshold|>` syntax
- **Similarity scoring**: `similarity_score()` method on Query adds
  `vector::similarity::{metric}(field, vector) AS alias` to SELECT fields
- **similarity_search_query()**: Convenience function combining `vector_search()` and
  `similarity_score()` for common vector search patterns (replaces manual SurrealQL
  construction in consumer projects)

### Fixed

- **Edge diff returns empty for modified edges**: `diff_edges()` now compares fields,
  indexes, events, and permissions when both old and new edges exist (previously returned
  an empty list with a TODO comment)
- **Event condition/action SQL injection**: Added `_validate_event_expression()` that
  rejects statement separators and SQL comments before interpolation into generated SQL
- **Permission rollback SQL always empty**: `_generate_modify_permissions_diff()` now
  generates rollback SQL from old permissions instead of always producing empty backward SQL
- **Bare exception blocks**: Narrowed 9 bare `except Exception:` blocks across migration/
  and connection/ modules to specific exception types

### Changed

- **Project renamed**: `reverie` -> `surql` (PyPI: `oneiriq-surql`, import: `surql`).
  Unified branding with the TypeScript SurrealDB toolkit under the Oneiriq org
- **Version 1.0.0**: First stable release. Development Status upgraded from Alpha to
  Production/Stable
- **CLI command**: `reverie` -> `surql` (e.g., `surql migrate up`, `surql schema show`)
- **Settings section**: `[tool.reverie]` -> `[tool.surql]` in pyproject.toml
- **Cache key prefix**: `reverie:` -> `surql:` by default
- **Split cli/schema.py** (1954 LOC): Extracted into `schema_inspect.py`, `schema_diff.py`,
  `schema_validate.py`, `schema_watch.py`, `schema_visualize.py` with thin command wrappers
- **Split cli/migrate.py** (1232 LOC): Extracted into `migrate_core.py`, `migrate_squash.py`,
  `migrate_advanced.py` with thin command wrappers
- **Split schema/validator.py** (1029 LOC): Extracted utility functions into
  `schema/validator_utils.py`. All files now comply with the 1000 LOC limit

### Testing

- **Test coverage**: 2191 tests passing (up from 2161 in 0.8.0)
- New test suites: `test_edge_diff.py` (edge diff, event validation, permission rollback)
- Extended: `test_query.py` with vector threshold and similarity scoring tests

---

## [0.8.0] - 2026-03-11

### Added

- **Typed Pydantic CRUD**: `create_typed()`, `get_typed()`, `query_typed()`, `update_typed()`,
  `upsert_typed()` functions that accept Pydantic model types and return validated model
  instances instead of raw dicts
- **DEFINE ACCESS support**: `AccessDefinition`, `AccessType`, `JwtConfig`, `RecordAccessConfig`
  schema types with `access_schema()`, `jwt_access()`, `record_access()` builders and
  `generate_access_sql()` for SurrealQL generation
- **IF NOT EXISTS support**: `if_not_exists` parameter on `generate_table_sql()`,
  `generate_edge_sql()`, and `generate_schema_sql()` for idempotent schema migrations
- **Reserved word validation**: `check_reserved_word()` and `SURREAL_RESERVED_WORDS` for
  detecting field names that collide with SurrealDB reserved words (emits warnings, not errors)

### Changed

- **Split query/builder.py**: Extracted `ReturnFormat` enum and 12 standalone free functions
  into `query/helpers.py`, bringing builder.py from 1137 to 947 LOC
- **Split query/graph.py**: Extracted `GraphQuery` class into `query/graph_query.py`, bringing
  graph.py from 1151 to 794 LOC. All files now comply with the 1000 LOC limit

### Testing

- **Test coverage**: 2161 tests passing (up from 2089 in 0.7.0)
- New test suites: `test_typed_crud.py`, `test_access.py`, `test_reserved_words.py`
- Extended: `test_schema_sql.py` with IF NOT EXISTS tests

---

## [0.7.0] - 2026-03-11

### Added

- **Upsert support**: `upsert()` query builder method and `upsert_record()` CRUD function
  for insert-or-update operations
- **Datetime coercion utilities**: `coerce_datetime()` and `coerce_record_datetimes()` for
  converting SurrealDB ISO datetime strings to Python datetime objects, including nanosecond
  truncation and timezone handling
- **SQL generation from schema definitions**: `generate_table_sql()`, `generate_edge_sql()`,
  and `generate_schema_sql()` for generating SurrealQL DEFINE statements directly from
  TableDefinition/EdgeDefinition objects
- **Additional exports**: `extract_result`, `extract_one`, `extract_scalar`, `has_results`,
  and `delete_records` added to package-level `__all__`
- **Field name validation**: Schema field builder functions now validate field names against
  SurrealDB identifier rules (alphanumeric + underscore, dot notation for nested fields)

### Fixed

- **CI format check was a no-op**: `ruff format src tests` (formats in-place, always passes)
  changed to `ruff format --check src tests`
- **GraphQuery.exists() mutated state**: `exists()` modified `self._limit` directly, violating
  immutability. Rewritten to use `count()` without mutation
- **SQL injection in migration diff defaults**: Field default values interpolated into SQL
  without sanitization. Added `_validate_default_value()` with safe literal pattern matching
- **Pytest marker mismatch**: Declared `asyncio` marker but tests use `anyio`. Fixed marker
  declaration to `anyio`
- **Migration executor lacked transactional wrapping**: Statements now execute within
  BEGIN/COMMIT/CANCEL TRANSACTION blocks for atomicity
- **Connection client reconnection**: Calling `connect()` when already connected now properly
  disconnects first before reconnecting
- **Cache TTL logic**: Fixed unreachable code path for custom TTL tracking
- **RecordID empty parts**: `parse()` now validates that table and id parts are non-empty
- **Nested transaction prevention**: Transaction manager checks for active transactions via
  `ContextVar` and raises `TransactionError` if nested
- **asyncio/trio incompatibility**: Replaced `asyncio.sleep`, `asyncio.gather`,
  `asyncio.Semaphore`, and `asyncio.create_task` with anyio equivalents in orchestration
  strategies and streaming module

### Changed

- **Edge schema RELATION mode validation**: Moved from construction-time to SQL generation
  time, allowing incremental composition via `with_from_table()`/`with_to_table()`
- **CHANGES file**: Updated to reflect versions 0.1.0 through 0.7.0

### Testing

- **Test coverage**: 2089 tests passing (up from ~1018)
- New test suites: `test_coerce.py`, `test_schema_sql.py`, `test_upsert.py`
- Field name validation tests added to `test_schema.py`

---

## [0.1.0] - 2026-01-02

### Added

- **SurrealDB Compatibility**: Complete compatibility with common SurrealDB patterns achieved
  - Result extraction utilities for handling SurrealDB response formats
    - `extract_result()` - Extract data from nested/flat result formats
    - `extract_one()` - Get first record or None
    - `extract_scalar()` - Extract aggregate values (COUNT, SUM, AVG, etc.)
    - `has_results()` - Check if result contains records
    - Location: `src/query/results.py:356-514`

  - RecordID angle bracket support for complex IDs
    - Support for `table:⟨complex-id⟩` format required by SurrealDB
    - Compatible with domain-based IDs like `outlet:⟨alaskabeacon.com⟩`
    - Compatible with compound IDs like `document:⟨domain:ulid⟩`
    - Location: `src/types/record_id.py:58-77`

  - SCHEMAFULL edge table support
    - `EdgeMode.SCHEMAFULL` for traditional edge tables with explicit in/out fields
    - `schemafull_edge()` helper function for traditional edge definitions
    - Compatible with entity_relation pattern
    - Location: `src/schema/edge.py:11-157`

  - Example implementations
    - `docs/examples/mtree_vector_search.py` - MTREE vector indexes (1024-dim, COSINE)
    - `docs/examples/schemafull_edge_example.py` - SCHEMAFULL edge table patterns

### Fixed

- **MTREE Index SQL Generation**: Changed from incorrect `FIELDS` keyword to correct `COLUMNS` keyword
  - Previous (incorrect): `DEFINE INDEX ... ON TABLE ... FIELDS embedding MTREE ...`
  - Current (correct): `DEFINE INDEX ... ON TABLE ... COLUMNS embedding MTREE ...`
  - Ensures compatibility with SurrealDB 1.0+ MTREE syntax
  - Location: `src/schema/table.py:393`
  - Tests: `tests/test_mtree_diff.py`

- **AsyncSurreal Client Implementation**: Verified correct usage of AsyncSurreal for async operations
  - Ensures all database operations use proper async/await patterns
  - Connection pooling and retry logic function correctly
  - Location: `src/connection/client.py:9, 89`

### Changed

- **RecordID Validation**: Enhanced to support both standard and angle bracket formats
  - Standard format: `table:id` (alphanumeric + underscores)
  - Angle bracket format: `table:⟨complex-id⟩` (any valid SurrealDB ID)
  - Backward compatible with existing code
  - Location: `src/types/record_id.py`

### Testing

- **Test Coverage**: 447 tests passing
  - Connection management (async operations, pooling, retry logic)
  - Schema definition (tables, fields, indexes, edges)
  - MTREE indexes (SQL generation, diff detection)
  - RecordID validation (standard and angle bracket formats)
  - Result extraction (nested/flat formats, aggregates)
  - CRUD operations (create, read, update, delete)
  - Query building (select, where, order, limit)
  - Migration system (up/down, history tracking)
  - Edge tables (TYPE RELATION and SCHEMAFULL modes)
  - CLI commands (migrate, schema, db)

---

[1.5.1]: https://github.com/Oneiriq/surql-py/releases/tag/v1.5.1
[1.5.0]: https://github.com/Oneiriq/surql-py/releases/tag/v1.5.0
[1.4.0]: https://github.com/Oneiriq/surql-py/releases/tag/v1.4.0
[1.3.1]: https://github.com/Oneiriq/surql-py/releases/tag/v1.3.1
[1.2.1]: https://github.com/Oneiriq/surql-py/releases/tag/v1.2.1
[1.2.0]: https://github.com/Oneiriq/surql-py/releases/tag/v1.2.0
[1.1.0]: https://github.com/Oneiriq/surql-py/releases/tag/v1.1.0
[1.0.0]: https://github.com/Oneiriq/surql-py/releases/tag/v1.0.0
[0.8.0]: https://github.com/Oneiriq/surql-py/releases/tag/v0.8.0
[0.7.0]: https://github.com/Oneiriq/surql-py/releases/tag/v0.7.0
[0.1.0]: https://github.com/Oneiriq/surql-py/releases/tag/v0.1.0
