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)insurql.schema.parser. Counterpart toparse_table_infofor graph-edge tables defined viaedge_schema/EdgeDefinition. Round-trips the three edge modes (RELATION,SCHEMAFULL,SCHEMALESS), theFROM <table> TO <table>clauses onTYPE RELATIONedges, and the per-actionPERMISSIONSclause SurrealDB v3 emits on the table-levelDEFINE TABLEstatement. Reuses the existing_parse_fields,_parse_indexes,_parse_events, and_parse_table_permissionshelpers so every per-field shape the table parser handles (typed record links,option<X>, FLEXIBLE, sub-fields, fields nameddefault) round-trips the same way on edges. ForRELATION-mode edges the auto-emittedinandoutfields are stripped on parse so they do not show up as orphan diffs against a code-sideEdgeDefinitionthat (correctly) does not declare them. Exported fromsurql.schemaalongsideparse_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-exportsparse_edge_infoso downstream packages canfrom surql.schema import parse_edge_infonext to the existingparse_table_info/parse_db_info.
[1.6.3] - 2026-05-18¶
Fixed¶
-
diff_tablesstill reported false-positive drift on typed record-link fields whose code-side declaration carried the canonicaltype::record("<target>", $value)coercion alongsidetarget_table=. The 1.5.14 emitter (schema/sql.py::_record_type_clause) already drops this redundant VALUE on write — oncetarget_tableis set the typedrecord<X>declaration constrains the column on its own — but the in-memoryFieldDefinitionreturned by thefield()constructor still held the coercion string. A live DB introspected aftermigrate_uptherefore reportedvalue=Nonewhile the code-side definition reportedvalue="type::record(...)", and every diff produced a spuriousMODIFY_FIELDentry. Fix: insurql.schema.fields.field(), oncetarget_tableis set (whether passed explicitly or auto-detected from the coercion pattern) and the suppliedvalue=round-trips through_detect_target_table_from_valueto the same target, clearvaluetoNonebefore constructing theFieldDefinition. The mismatch case (target_table='X'plusvalue='type::record("Y", $value)') is left untouched — silently dropping a value that targets a different table would be a footgun. -
parse_table_infosilently lost table-level mode + PERMISSIONS on SurrealDB v3. Pre-1.6.3 the parser readinfo.get('tb', '')as the source of theDEFINE TABLEstatement string, which is correct for the v2INFO FOR TABLEshape but does not exist in v3 — v3 returns{events, fields, indexes, lives, tables}and surfaces theDEFINE TABLEstring only viaINFO FOR DB'stables.<name>dict. The result: on v3, the parsedTableDefinitionalways hadpermissions=Noneandmode=SCHEMALESS, and every consumer that declared table-level PERMISSIONS saw a false-positiveMODIFY_PERMISSIONSdiff on every call. Fix:parse_table_infoaccepts a new optionaldefine_table: str | None = Noneparameter that, when passed, is used as the source oftb_definitionfor mode + PERMISSIONS parsing. Callers fetchINFO FOR DBonce and passdb_info['tables'][<name>]alongside the per-table info dict. Behaviour is fully backwards-compatible: whendefine_tableisNone, the parser falls back to the legacyinfo.get('tb', '')and the v2 shape continues to round-trip unchanged. -
_parse_table_permissionsfailed to match the comma-joined action-list shape SurrealDB v3 emits when multiple actions share a single rule. v3's serialiser collapsesPERMISSIONS FOR select WHERE r FOR create WHERE r FOR update WHERE r FOR delete WHERE rdown to the compactPERMISSIONS FOR select, create, update, delete WHERE rform. 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 returnedNone— feeding straight into the same false-positiveMODIFY_PERMISSIONSdiff 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 perFOR ... WHEREclause) and the comma-joined form (one rule shared across an action list) parse into the samedict[str, str]shape thattable_schema(permissions=...)accepts. Mixed forms — some actions grouped via comma, others split into separate clauses — also work because eachFOR <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/integration— 2620 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_tablesreported 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_infoandsurql.migration.diff.diff_tableswere not round-trip-symmetric with the emitter. Aftermigrate_up, SurrealDB v3 reformats the stored DEFINE statements before returning them viaINFO FOR TABLE—option<X>is unfolded tonone | X,FLEXIBLEmoves to the head of the clause list, every per-fieldPERMISSIONS FULLdefault 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 afterTYPE(soTYPE none | record<X>parsed asFieldType.ANY, losing bothRECORDandtarget_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 asTYPE option<record<X>>, stored asTYPE none | record<X>, used to reportModify field Xon every diff. The parser now lifts thenone |union intonullable=True, extracts the target table out ofrecord<X>, and_fields_equalnow comparesnullableandtarget_tablealongside the other field attributes. - FLEXIBLE object fields (
TYPE option<object> FLEXIBLEemitted,FLEXIBLE TYPE none | objectreturned) used to report a spuriousModify fieldper 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_permissionsextractor.PERMISSIONS NONEandPERMISSIONS FULL(the SurrealDB default-deny / default-allow shapes a table gets when no per-action rules were declared) normalise toNone, matching the code-side default; per-action rules (PERMISSIONS FOR select WHERE ...) parse into the samedict[str, str]shapetable_schema(permissions=...)accepts.diff_permissionsadditionally treatsNoneand{}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 thatINFO FOR TABLEreturns alongside the parentarrayfield — are now skipped at parse time so the diff doesn't emitDrop field X.*for them. -
diff_tablescrashed withUnsafe default value expression: 'ON ... TYPE ...'on tables with a field nameddefault(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 NAMEdefaultmatched 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_valueanddiff_tablesraised 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 theDEFINE 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/integration— 2616 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.commitskipped param normalisation, crashing onsurql.RecordIDbound params.DatabaseClient.executeroutes its incomingparamsdict through_denormalize_params, which convertssurql.types.record_id.RecordID(surql-py's Pydantic wrapper) tosurrealdb.RecordID(the SDK's native CBOR-encodable class) before the encoder sees them.Transaction.commitflushed buffered statements viaTransaction._raw_query→query_raw(or the fallbackDatabaseClient.execute) and passedself._paramsthrough unchanged — skipping the normalisation step entirely. The asymmetry meant callers who placedsurql.RecordIDvalues into bound params viatxn.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, andshortest_path— now accept an optionalconditions: list[str | Operator] | Nonekeyword. Each entry is rendered throughQuery.where(...), matching the exact shape thatquery_recordsalready accepts; multiple conditions combine with AND. Pre-1.6.0 these helpers emitted bareSELECT ... 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 — defaultingNoneleaves 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 theQuerybuilder so the WHERE composition is identical to the typed CRUD helpers' path. Regression coverage:tests/test_query_graph.py::TestTraverseConditionsplus 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 singleBEGIN TRANSACTION; ...; COMMIT TRANSACTION;request through the SDK'squerymethod. SurrealDB v3 collapses that response toNoneregardless of whether the batch succeeded OR was rolled back server-side — verified againstsurrealdb/surrealdb:v3.0.5withsurrealdb==2.0.0a1. The commit helper readNoneas 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_typeddiffer from their untyped siblings in return type only — untyped returnsdict[str, Any], typed revalidates the response into the model instance's class and returnsT. get_typedis a thin alias ofget_record(the untyped helper already returnsT | Nonewhen given amodel: type[T]).query_typedis NOT a typed variant ofquery_records: the former runs hand-written SurrealQL, the latter uses theQuerybuilder. 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
/sqlshows the sentinel surfaces as{'result': '__txn_ok__', 'status': 'OK'}on success and the statement list containsstatus: 'ERR'rows on rollback. The Python SDK'squerymethod collapses both shapes toNone; the SDK'squery_rawmethod preserves the full envelope. The commit path routes throughquery_rawand 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_migrationandcreate_blank_migrationwroteGenerated: {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)')emittedDEFINE FIELD author ... TYPE record VALUE type::record("user", $value)— bareTYPE recordplus 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 readingINFO FOR TABLE) couldn't render the cross-table arrow.
Two new behaviors:
- Explicit:
field(name, FieldType.RECORD, target_table='user')emitsDEFINE FIELD name ON TABLE ... TYPE record<user>;(option<record<user>>whennullable=True). NoVALUEclause — the parameterized type enforces the same constraint. - Auto-detection (backward-compat): when
target_tableisn't passed butvalue=matches the canonicaltype::record("X", $value)coercion pattern, the emitter lifts X intotarget_table=Xautomatically and drops the now-redundant VALUE clause. Existing schemas that used thevalue=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.deletemishandled bracketed record-id targets emitted bystr(RecordID(...))(#91). After 1.5.11 switchedRecordID.__str__to unicode⟨⟩brackets for v3 compatibility, callers likeget_record(table, RecordID(table, 'a-b'), Model)produced a target string'table:⟨a-b⟩'.db.selectwas passing the bracketed inner'⟨a-b⟩'as the$idparam totype::record($table, $id), so SurrealDB looked uptable:'⟨a-b⟩'(a different, non-existent record) and returned None. Same hazard existed fordb.merge/update/deletepaths where the SDK's string parser doesn't understand the bracketed form. The fix: -
Extended
_RECORD_ID_BRACKETED_PATTERNto match both ASCII<>and unicode⟨⟩brackets. - Added
_strip_record_id_brackets(used by_denormalize_paramsanddb.select'stype::recorddispatch) so the inner id is stripped of brackets before lookup. - Added
_normalize_target(used bydb.merge/update/delete) which converts any record-id-shaped string target into anSdkRecordIDobject 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_paramsdidn't recognize surql's ownRecordID(#91). When callers builtmerge_record(table, id, {'community': RecordID(...)}), the surqlRecordIDwrapper sailed straight through_denormalize_paramsand hit the SDK's CBOR encoder, which raised('no encoder for type ', <class 'surql.types.record_id.RecordID'>). Added aSurqlRecordID -> SdkRecordIDconversion branch so callers can hand the helpers either a string or aRecordIDinstance 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 SurrealQLNULLbutTYPE option<X>columns requireNONE(#89). SurrealDB v3 strictly distinguishes NONE (absence of value) from NULL (explicit null value);option<X>fields reject NULL withExpected 'none | X' but found 'NULL'. PythonNonesemantically maps to "no value", so NONE is the correct serialization. Affects_quote_value,Eq('field', None)(now rendersfield = NONE), andvalue(None). Callers who want a genuine NULL comparison should use theIsNulloperator (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 asf'{table}:<{id}>', and SurrealDB's v3 parser fails on the ASCII<withUnexpected 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_valueserialized lists and dicts as quoted strings (#87). Falling through tof"'{str(value)}'"meant[1, 2, 3]became the literal SurrealQL string'[1, 2, 3]'and{'k': 'v'}became"{'k': 'v'}". SurrealDB then rejected these againstTYPE array/TYPE objectcolumns. The fix emits proper SurrealQL array literals ([1, 2, 3]) and object literals ({k: 'v'}), recursing through_quote_valuefor nested values. Also added an explicitRecordIDbranch 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_paramsover-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 asRecordID('BFS', 'community:1')and rejected by SurrealDB at the schema layer withCouldn'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/integration— 2537 passed, 9 skipped, 2 xfailed.
[1.5.9] - 2026-05-17¶
Fixed¶
migrate_upsilently swallowed per-statement errors inside the BEGIN/COMMIT transaction wrapper (#83). The executor used to flush every migration as a singleBEGIN TRANSACTION; <stmts>; COMMIT TRANSACTION;RPC for atomicity. surrealdb-py returnsnullfor 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 loggedapplied_count=1while the schema was effectively empty. The fix runs statements individually so each failing statement raises aQueryErrorthe executor wraps asMigrationExecutionError(..., '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)— emitsTYPE option<X>instead ofTYPE Xso a SCHEMAFULL column accepts NONE. Without this knob, every CREATE that omits an optional column fails on SurrealDB v3 withCouldn't coerce value for field <x>: Expected <type> but found NONE. Applies to bothschema/sql.py(initial DEFINE FIELD) andmigration/diff.py(incremental ADD FIELD diffs). Default isFalse, so existing code is unaffected.
Fixed — SurrealDB v3 grammar compatibility¶
- Table PERMISSIONS clauses emitted invalid SurrealDB SQL.
generate_table_sqlwas emittingDEFINE FIELD PERMISSIONS FOR SELECT ON TABLE x WHERE ...for each permission action — not valid grammar in any SurrealDB version. SurrealDB rejected migrations withParse error: Unexpected token FOR, expected ON. Permissions now fold into theDEFINE TABLEstatement 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_sqlignoredEdgeDefinition.permissionsentirely. Edges built withwith_edge_permissions(...)now correctly emit permissions on theirDEFINE TABLE ... TYPE RELATION ... PERMISSIONS ...;statement. - Migration diff had the same broken PERMISSIONS SQL in parallel.
_generate_modify_permissions_diffemitted the invalidDEFINE FIELD PERMISSIONSform;_generate_add_table_diffsfailed to fold permissions into the initialDEFINE TABLE. Both paths now use a shared_permissions_clause_sqlhelper and emit valid v3 syntax. Permission changes re-DEFINE the table (SurrealDB has noALTER TABLE); rollback re-DEFINEs with the old permissions. type::thing(table, id)was removed in SurrealDB v3. Calling it now raisesInvalid 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.
DatabaseClientparameter normalization was coercing ordinary prose strings that merely contained aword:wordsubstring 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.mdanchor links inmigration.mdandquery-ux.md— the#record-id-construction-...fragment did not match the generated heading id, somkdocs build --strictreported it. - Docs site:
site_namecorrected tosurql-py(wassurql, so every page title and the site header showed the wrong project name); the description now notes the Python port.
Changed¶
- CI:
ci.ymlruns only on pull requests (not on push); the nightly cron is disabled (workflow_dispatchonly);docs.ymlrunsmkdocs build --stricton PRs so doc breakage is caught before merge. Fork-PR CI runs now route toubuntu-latestrunners explicitly.
Tests¶
- Strengthened permissions test coverage: existing
test_table_with_permissionswas too lenient ('FOR SELECT' in sqlmatched the broken output). Added explicit equality assertions plus regression guards thatDEFINE FIELD PERMISSIONSnever appears, action keys are always lowercased, and edges with permissions render their PERMISSIONS clause. - Updated all
type::thingtest assertions to expect the v3type::recordemission. - Marked two
TestRecordMigrationAgainstEmbeddedDbcases asxfailbecause the embeddedsurrealdbPython SDK (latest PyPI 2.0.0) still ships pre-v3 grammar wheretype::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 testsandruff 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 upapplied 17 tables + 1TYPE RELATIONedge with PERMISSIONS clauses;INFO FOR DBechoesPERMISSIONS FOR select, create, update, delete WHERE tenant_id = $auth.tenanton every table.
[1.5.7] - 2026-05-05¶
Fixed¶
DatabaseClientsurvives upstream SurrealDB recreate. When the SurrealDB server is restarted mid-flight (dockercompose 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_relationnow classify transport-level disconnects via_is_disconnect_error, redial under a per-clientasyncio.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 raiseQueryErrorimmediately.
[1.5.6] - 2026-05-02¶
Fixed¶
- Migration runner / record-id construction (
type::recordvstype::thing).record_migrationandDatabaseClient.select('table:id')were emittingtype::record($table, $id)to construct a record id, but in SurrealDB v3 the two-arg form oftype::record(value, type)is a type coercion (castvalueintorecord<type>), NOT a table+id constructor. Callingtype::record('_migration_history', '0001_init')is interpreted as "coerce'_migration_history'intorecord<0001_init>" and fails withExpected a record<0001_init> but cannot convert '_migration_history' into a record<0001_init>. Replaced both call sites withtype::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()andtype_record()Python helpers also renderedtype::record('table', id)-- they now emittype::thing('table', id). The Python function names are preserved for source compatibility (type_recordis now an alias fortype_thing). - Updated
docs/v3-patterns.md,docs/migration.md,docs/query-ux.md,docs/api/index.md, andREADME.mdto reflect thattype::thingis the constructor on both v2 and v3 (correcting the earlier claim that v3 renamedtype::thingtotype::record). - Added
tests/test_migration_history.py::TestRecordMigrationAgainstEmbeddedDbwhich round-tripsrecord_migrationthrough an embeddedmem://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
surrealdbPython SDK'sAsyncEmbeddedSurrealConnectioninheritslive()/subscribe_live()from the WebSocket connection but never initialises thelive_queuesattribute and the underlying Rust extension (_surrealdb_ext.AsyncEmbeddedDB) only exposes a one-shotexecute(cbor) -> bytescall -- so callingclient.live(...)on asurrealkv:///mem:///file://URL crashes withAttributeError: '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.streamingpublic property andDatabaseClient.live(table, diff=False)convenience method so callers no longer need to reach into the private_streamingattribute to start LIVE SELECTs. The property raisesConnectionErrorif the client is not connected andStreamingErrorif 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 viasurrealdb,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.3in the dev group (medium advisory)
No public API or runtime behavior changes; lockfile-only impact.
[1.5.2] - 2026-05-02¶
Fixed¶
_denormalize_paramsURL false positive (silent CREATE/UPDATE failures). The record-ID detection regex^[a-zA-Z_][a-zA-Z0-9_]*:.+$matched URL schemes likehttp://,https://,ws://,wss://,file://, and silently coerced URL strings intoRecordIDobjects. 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 toRecordID('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::recordvstype::thing, bufferedBEGIN/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 theQuery.set(...)/ deferred-update()/ expression-awareselect(...)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 | validatesubcommands 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.ymlunderGetting StartedandGuides.
Fixed¶
- Aligned
surql.__version__withpyproject.toml.
1.5.0 - 2026-04-16¶
Added¶
- Record-ID helpers (#47 / #2):
type_record(table, id)andtype_thing(table, id)returnSurrealFnwrappers. Prefertype_recordon v3;type_thingremains 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 withQuery.set(...),Query.select([...]), andaggregate_records(select={...})without raw SurrealQL strings. - Result extraction aliases (#47 / #4):
extract_many(alias forextract_result) andhas_result(alias forhas_results) for naming that reads naturally next toextract_one/extract_scalar. aggregate_records(#47 / #1): typedSELECT ... GROUP BY | GROUP ALLhelper that returns a list of dicts, hiding the SurrealDB response envelope.- Builder API:
Query.set(**fields)for fluentSETclauses, optionaldataargument onQuery.update(target, data=None), and projection items inQuery.select(...)may now beSurrealFn/Expressioninstances (anything with.to_surql()). - Pre-push hook: shipped
.githooks/pre-pushmirroring CI checks (ruff, mypy, pytest) with optional v3 integration opt-in. SeeCONTRIBUTING.mdfor 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()emitsapplied_at = <datetime> $applied_atso 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 ALLoncount_records:count_records()appendsGROUP ALLand accepts both envelope and bare-list response shapes.type::record: select record-ID targets route throughtype::record(...)on v3 (withtype::thingstill accepted for backwards compatibility).- Idempotent DDL:
DEFINE TABLE _migration_history IF NOT EXISTSand theif_not_existsflag across the schema generator sosurql migrate upis safe to re-run. - Graph depth unrolling:
traverse(...)unrolls{min..max}graph-depth ranges into literal hop unions that v3 accepts. - SDK pin: minimum
surrealdbSDK bumped to v2.0.0a1 (speaks v3's RPC protocol). - CI:
v3-integration.ymlruns the integration suite againstsurrealdb/surrealdb:v3.0.5on every push.- Nightly matrix runs against
surrealdb/surrealdb:latest. - Dependabot for pip + github-actions.
- Conventional-commit PR title lint.
- Dependency-review workflow.
- Scheduled
pip-auditsecurity workflow.
Fixed¶
- Narrow
is_migration_appliedprobe to a targeted query (avoids false negatives on fresh databases). - Narrow migration-table-missing probe to
does not existerrors (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://). Previouslyexecute_migration()wrapped statements inBEGIN TRANSACTION;/COMMIT TRANSACTION;, which crashes withIndexError: list index out of rangeon embedded connections because the upstreamsurrealdbPython SDK'squery()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 withas_()aliases andgroup_by()/group_all()clauses. - Query builder GROUP ALL (#1):
group_all()method onQueryfor full-table aggregation. - Record references (#2):
record_ref(table, id)generatestype::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_urlnow accepts the full set of schemes supported by the underlyingsurrealdbSDK, including the embedded enginesmem://,memory://,file://, andsurrealkv://.enable_live_queries=Trueis now compatible with embedded engines (they run in-process), and only rejected forhttp:///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 tosurrealdb.RecordIDobjects before sending to the SDK. Applied tocreate(),update(),merge(),execute(), andinsert_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 ("Expectedrecordbut found string")
Testing¶
- Added 18 tests covering
_denormalize_paramsunit 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
DatabaseClientCRUD responses (create,select,update,merge,delete,execute,insert_relation) now recursively normalize SurrealDB SDKRecordIDobjects 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.HNSWfor 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()andgenerate_edge_sql()emit correctDEFINE 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.pydemonstrating 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 athresholdparameter for MTREE similarity filtering, generating<|K,DISTANCE,threshold|>syntax - Similarity scoring:
similarity_score()method on Query addsvector::similarity::{metric}(field, vector) AS aliasto SELECT fields - similarity_search_query(): Convenience function combining
vector_search()andsimilarity_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.pywith thin command wrappers - Split cli/migrate.py (1232 LOC): Extracted into
migrate_core.py,migrate_squash.py,migrate_advanced.pywith 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.pywith 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,RecordAccessConfigschema types withaccess_schema(),jwt_access(),record_access()builders andgenerate_access_sql()for SurrealQL generation - IF NOT EXISTS support:
if_not_existsparameter ongenerate_table_sql(),generate_edge_sql(), andgenerate_schema_sql()for idempotent schema migrations - Reserved word validation:
check_reserved_word()andSURREAL_RESERVED_WORDSfor detecting field names that collide with SurrealDB reserved words (emits warnings, not errors)
Changed¶
- Split query/builder.py: Extracted
ReturnFormatenum and 12 standalone free functions intoquery/helpers.py, bringing builder.py from 1137 to 947 LOC - Split query/graph.py: Extracted
GraphQueryclass intoquery/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.pywith IF NOT EXISTS tests
0.7.0 - 2026-03-11¶
Added¶
- Upsert support:
upsert()query builder method andupsert_record()CRUD function for insert-or-update operations - Datetime coercion utilities:
coerce_datetime()andcoerce_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(), andgenerate_schema_sql()for generating SurrealQL DEFINE statements directly from TableDefinition/EdgeDefinition objects - Additional exports:
extract_result,extract_one,extract_scalar,has_results, anddelete_recordsadded 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 toruff format --check src tests - GraphQuery.exists() mutated state:
exists()modifiedself._limitdirectly, violating immutability. Rewritten to usecount()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
asynciomarker but tests useanyio. Fixed marker declaration toanyio - 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
ContextVarand raisesTransactionErrorif nested - asyncio/trio incompatibility: Replaced
asyncio.sleep,asyncio.gather,asyncio.Semaphore, andasyncio.create_taskwith 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 formatsextract_one()- Get first record or Noneextract_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
- Support for
-
SCHEMAFULL edge table support
EdgeMode.SCHEMAFULLfor traditional edge tables with explicit in/out fieldsschemafull_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
FIELDSkeyword to correctCOLUMNSkeyword - 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)