Skip to content

Changelog

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

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[1.6.4] - 2026-05-18

Added

  • parse_edge_info(edge_name, info, define_table=None) in surql.schema.parser. Counterpart to parse_table_info for graph-edge tables defined via edge_schema / EdgeDefinition. Round-trips the three edge modes (RELATION, SCHEMAFULL, SCHEMALESS), the FROM <table> TO <table> clauses on TYPE RELATION edges, and the per-action PERMISSIONS clause SurrealDB v3 emits on the table-level DEFINE TABLE statement. Reuses the existing _parse_fields, _parse_indexes, _parse_events, and _parse_table_permissions helpers so every per-field shape the table parser handles (typed record links, option<X>, FLEXIBLE, sub-fields, fields named default) round-trips the same way on edges. For RELATION-mode edges the auto-emitted in and out fields are stripped on parse so they do not show up as orphan diffs against a code-side EdgeDefinition that (correctly) does not declare them. Exported from surql.schema alongside parse_table_info.

Before 1.6.4, drift detectors could check edges' presence/absence by cross-referencing ALL_EDGES.keys() against INFO FOR DB.tables but had no way to diff edge MODE, FROM/TO endpoints, or per-edge fields/indexes against the live DB — parse_table_info returned a TableDefinition with mode=SCHEMALESS and no edge metadata. With parse_edge_info plus the existing diff_edges, edge round-trip parity is now on the same footing as table parity.

Changed

  • surql.schema.__init__ re-exports parse_edge_info so downstream packages can from surql.schema import parse_edge_info next to the existing parse_table_info / parse_db_info.

[1.6.3] - 2026-05-18

Fixed

  • diff_tables still reported false-positive drift on typed record-link fields whose code-side declaration carried the canonical type::record("<target>", $value) coercion alongside target_table=. The 1.5.14 emitter (schema/sql.py::_record_type_clause) already drops this redundant VALUE on write — once target_table is set the typed record<X> declaration constrains the column on its own — but the in-memory FieldDefinition returned by the field() constructor still held the coercion string. A live DB introspected after migrate_up therefore reported value=None while the code-side definition reported value="type::record(...)", and every diff produced a spurious MODIFY_FIELD entry. Fix: in surql.schema.fields.field(), once target_table is set (whether passed explicitly or auto-detected from the coercion pattern) and the supplied value= round-trips through _detect_target_table_from_value to the same target, clear value to None before constructing the FieldDefinition. The mismatch case (target_table='X' plus value='type::record("Y", $value)') is left untouched — silently dropping a value that targets a different table would be a footgun.

  • parse_table_info silently lost table-level mode + PERMISSIONS on SurrealDB v3. Pre-1.6.3 the parser read info.get('tb', '') as the source of the DEFINE TABLE statement string, which is correct for the v2 INFO FOR TABLE shape but does not exist in v3 — v3 returns {events, fields, indexes, lives, tables} and surfaces the DEFINE TABLE string only via INFO FOR DB's tables.<name> dict. The result: on v3, the parsed TableDefinition always had permissions=None and mode=SCHEMALESS, and every consumer that declared table-level PERMISSIONS saw a false-positive MODIFY_PERMISSIONS diff on every call. Fix: parse_table_info accepts a new optional define_table: str | None = None parameter that, when passed, is used as the source of tb_definition for mode + PERMISSIONS parsing. Callers fetch INFO FOR DB once and pass db_info['tables'][<name>] alongside the per-table info dict. Behaviour is fully backwards-compatible: when define_table is None, the parser falls back to the legacy info.get('tb', '') and the v2 shape continues to round-trip unchanged.

  • _parse_table_permissions failed to match the comma-joined action-list shape SurrealDB v3 emits when multiple actions share a single rule. v3's serialiser collapses PERMISSIONS FOR select WHERE r FOR create WHERE r FOR update WHERE r FOR delete WHERE r down to the compact PERMISSIONS FOR select, create, update, delete WHERE r form. The pre-1.6.3 regex \bFOR\s+(select|create|update|delete)\s+WHERE\s+... only captured a single bare action keyword, so the comma-list form matched nothing and the parser returned None — feeding straight into the same false-positive MODIFY_PERMISSIONS diff on every consumer with collapsed per-action rules. Fix: the regex now captures ((?:action)(?:\s*,\s*(?:action))*) as a list, the parser splits on commas, and each per-action key in the returned dict gets the same rule string. Both the expanded form (one rule per FOR ... WHERE clause) and the comma-joined form (one rule shared across an action list) parse into the same dict[str, str] shape that table_schema(permissions=...) accepts. Mixed forms — some actions grouped via comma, others split into separate clauses — also work because each FOR <list> WHERE <rule> clause is parsed independently and exploded.

Regression coverage: 4 new tests in tests/test_diff_round_trip.py: test_record_field_with_target_table_drops_canonical_value, test_parse_table_info_uses_define_table_for_permissions, test_parse_table_permissions_handles_comma_joined_actions, and an end-to-end test_round_trip_typed_record_field_with_table_permissions_is_empty that builds a code-side TableDefinition with both a typed-record field and table-level PERMISSIONS, serialises via generate_table_sql, and feeds the result back through parse_table_info(define_table=...) asserting diff_tables(live, code) == []. Each test fails on 1.6.2 and passes on 1.6.3.

Verified

  • ruff check src tests + ruff format --check src tests — clean.
  • mypy src --strict — no issues.
  • uv run pytest --no-cov --ignore=tests/integration2620 passed, 9 skipped, 2 xfailed (was 2616 in 1.6.2; +4 new regression tests covering the fixes above).

[1.6.2] - 2026-05-17

Fixed

  • diff_tables reported false-positive drift on every consumer schema that uses 1.5.14's typed record-link fields, FLEXIBLE fields, PERMISSIONS clauses, or array sub-fields. surql.schema.parser.parse_table_info and surql.migration.diff.diff_tables were not round-trip-symmetric with the emitter. After migrate_up, SurrealDB v3 reformats the stored DEFINE statements before returning them via INFO FOR TABLEoption<X> is unfolded to none | X, FLEXIBLE moves to the head of the clause list, every per-field PERMISSIONS FULL default is materialised, and array fields get a companion <field>[*] or <field>.* entry holding the per-element type spec. The pre-1.6.2 parser captured only the first word after TYPE (so TYPE none | record<X> parsed as FieldType.ANY, losing both RECORD and target_table=X), did not parse table PERMISSIONS at all, and surfaced array sub-field entries as standalone fields — every one of which the diff then misclassified.

Concrete patterns the consumer hit, all of which now round-trip clean:

  • Typed record-link fields (field(name, FieldType.RECORD, target_table='X', nullable=True)) emitted as TYPE option<record<X>>, stored as TYPE none | record<X>, used to report Modify field X on every diff. The parser now lifts the none | union into nullable=True, extracts the target table out of record<X>, and _fields_equal now compares nullable and target_table alongside the other field attributes.
  • FLEXIBLE object fields (TYPE option<object> FLEXIBLE emitted, FLEXIBLE TYPE none | object returned) used to report a spurious Modify field per call. The clause-keyword slicer recognises FLEXIBLE in either position and the type-clause parser handles the union form.
  • Table-level PERMISSIONS clauses round-trip via a new _parse_table_permissions extractor. PERMISSIONS NONE and PERMISSIONS FULL (the SurrealDB default-deny / default-allow shapes a table gets when no per-action rules were declared) normalise to None, matching the code-side default; per-action rules (PERMISSIONS FOR select WHERE ...) parse into the same dict[str, str] shape table_schema(permissions=...) accepts. diff_permissions additionally treats None and {} as equivalent so the dict comparison never trips on the asymmetry.
  • Array sub-field entries (unresolved_refs[*], jurisdiction.*, embedding.*, nodes.*) — the per-element type annotation that INFO FOR TABLE returns alongside the parent array field — are now skipped at parse time so the diff doesn't emit Drop field X.* for them.

  • diff_tables crashed with Unsafe default value expression: 'ON ... TYPE ...' on tables with a field named default (or any field whose name contains DEFAULT as a substring). The pre-1.6.2 clause-extraction regexes (DEFAULT\s+(.+?)..., VALUE\s+(.+?)..., ASSERT\s+(.+?)...) had no word-boundary anchoring, so the field NAME default matched the DEFAULT clause keyword and the extractor captured the field-type slice (ON data_capture_spec TYPE none | string) as the alleged default value. That string then failed the safety check in _field_to_sql._validate_default_value and diff_tables raised instead of returning a diff. Fix: the parser now slices the DEFINE FIELD statement into clause-keyword-delimited bodies up-front (_split_field_clauses), skipping past the DEFINE FIELD <name> ON [TABLE] <table> prefix first so the field name is never scanned for clause keywords. Each extractor reads its own clause body verbatim rather than running its own ad-hoc regex over the full statement.

Regression coverage: 11 new tests in tests/test_diff_round_trip.py covering every false-positive pattern named above plus the non-function-call DEFAULT crash, the typed-record non-nullable variant, the nullable-string variant, and an end-to-end consumer-shape test mixing typed records, FLEXIBLE objects, array sub-fields, and PERMISSIONS in one table. Each test fails on 1.6.1 and passes on 1.6.2.

Verified

  • ruff check src tests + ruff format --check src tests — clean.
  • mypy src --strict — no issues.
  • uv run pytest --no-cov --ignore=tests/integration2616 passed, 9 skipped, 2 xfailed (was 2605 in 1.6.1; +11 new regression tests covering the fixes above).

[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_queryquery_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/integration2605 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/integration2601 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/integration2547 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/integration2546 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/integration2539 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/integration2539 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 everyupsert_record,Query().upsert(...),merge_record, etc. call that built its target viastr(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/integration2539 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 fieldx: Expectedstringbut foundBFS:community:1`. The id portion is now restricted to[^:\s/]+(no extra colons, whitespace, or slashes), and the angle-bracketed formtable:is matched by a parallel pattern that treats anything inside the brackets as the id — including colons — preserving SurrealDB v3'scommunity:escape hatch. Pattern detection is still best-effort; callers wanting an unambiguous record reference should passRecordID(table, id)orRecordRef(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/integration2537 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/integration2535 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 formtype::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)), andmigration.history.record_migration'sCREATE type::record($table, $id) SET .... Earlier doc comments claimingtype::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 astype_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/integration2531 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)