Items are ordered by priority within each section. Check off completed work and commit the file update so it stays current.
- Text-format DD parse + save (
# OpenADS Data Dictionary v1): TABLE, INDEX, USER, MEMBER, LINK, RI, DBPROP, USERPROP rows; atomic write-then-rename save. -
AdsConnect60opens a.addpath — parent dir becomes data dir. - Full DD ABI CRUD:
AdsDDAddTable,AdsDDRemoveTable,AdsDDCreateUser,AdsDDDeleteUser,AdsDDAddUserToGroup,AdsDDRemoveUserFromGroup,AdsDDCreate{,Drop,Modify}Link,AdsDDCreate/RemoveRefIntegrity,AdsDDSet/GetDatabaseProperty,AdsDDGet/SetUserProperty,AdsDDCreate,AdsDDAddTable90,AdsDDCreateRefIntegrity62. - Binary
.addreader —DataDict::load_add_binary_(). Parses allTablerecords from real ADS proprietary files. Fixture:tests/fixtures/adi/pmsys.add. (2026-05-24)
-
Binary
.addwrite support — full round-trip mutations on ADS proprietary Data Dictionary files.save_add_binary_()serializes all records back in exact binary layout (524-byte records, 2200-byte header updated in-place).add_table,remove_table,add_index_file,remove_index_file,create_user,delete_userall dispatch through the binary path whenbinary_format_is set. 4 round-trip tests added totests/unit/data_dict_test.cpp. (2026-05-24) -
AdsDDGetTableProperty/AdsDDSetTableProperty. Exported. Handles: RELATIVE_PATH (211), TABLE_PATH (205), TABLE_TYPE (204, inferred from extension), CHAR_TYPE (212), OBJ_ID (208), FIELD_COUNT (206, returns 0), and the numeric-zero boolean properties. Set returns AE_FUNCTION_NOT_AVAILABLE. 5 unit tests intests/unit/abi_dd_table_prop_test.cpp. (2026-05-24) -
AdsDDSetUserProperty— implemented and exported. Property code dispatch: 1102 (GROUP_MEMBERSHIP) →add_user_to_group; 1103 (BAD_LOGINS) → read-only no-op; all other codes (including 1101 PASSWORD, 1 COMMENT) stored as"prop_N"string properties, symmetrical withAdsDDGetUserProperty.AdsDDCreateUsernow stores password (prop_1101), description (prop_1), and optional group membership. 7 tests intests/unit/abi_dd_user_prop_test.cpp. Cross-checked againstf:\harbour3.2-bcc7.3\contrib\rddads\. (2026-05-24) -
ADI level-2 page navigation — fixed. Character-key ADI indexes (CICHAR/CHAR fields) use level-2 dense-leaf pages instead of level-3. Branch entries use a different format:
padded_key[(len+3)&~3] + cum[4 LE] + page[1]vs numeric'skey[8 BE] + cum[4 BE] + page[4 BE]. Compound-key tags (e.g. "F2;F14") are now parsed and their total key length computed correctly.is_dense_leaf()now accepts both level-2 and level-3 pages.encode_adt_keyhandles CICHAR/CHAR (returns raw field bytes). 2 new unit tests inabi_adi_smoke_test.cpp. (2026-05-24) -
RI enforcement at write time. INSERT (AdsWriteRecord after AdsAppendRecord): validates FK exists in parent table via linear scan; blank FK skips the check (NULL semantics). DELETE (AdsDeleteRecord): enforces
delete_opt— RESTRICT (2) rejects if children exist; CASCADE (1) marks child rows deleted; SETNULL (3) / SETDEFAULT (4) blank the child FK field.pending_appends()set tracks in-flight appends;in_ri_check()flag prevents recursive enforcement on cascade actions. UPDATE enforcement deferred (see below). 8 tests intests/unit/abi_dd_ri_test.cpp. (2026-05-25) -
RI enforcement on UPDATE (parent key change). PK snapshot captured at navigation time (AdsGotoRecord/Top/Bottom/Skip/Seek) via
pk_snapshots()map keyed by Table*. At AdsWriteRecord, the new PK (from dirty buffer) is compared to the old PK (from snapshot). If they differ,update_optis enforced: RESTRICT rejects and restores the old value on disk; CASCADE updates child FK fields; SETNULL/SETDEFAULT blanks child FK fields. On-disk rollback on RESTRICT:set_fieldwrites immediately so ri_enforce_update re-writes the old value when rejecting. 4 tests intests/unit/abi_dd_ri_test.cpp. (2026-05-26) -
DD authentication for local connections.
AdsConnect60checks the DD'sADS_DD_LOG_IN_REQUIREDproperty (prop_5). When set, the suppliedpucUser/pucPwdare validated against the DD: unknown user or password mismatch both returnAE_LOGIN_FAILED(7077). On success the authenticated username is stored onConnection::username_for future permission checks. 6 tests intests/unit/abi_dd_auth_test.cpp. (2026-05-25) -
Per-table access control / permission checking. DD stores
TABLEPERM <table>;<user_or_group>=<level>entries (0=none, 1=read, 2=write, 3=delete, 4=full). Effective level is max of direct user perm and any group memberships; tables with no ACL default to full access.AdsOpenTablewithusCheckRights≠0enforces the level (1 forADS_READONLY, 2 otherwise). SQLAdsExecuteSQLDirectwithcheck_rights≠0enforces per-operation (SELECT→1, INSERT/UPDATE→2, DELETE→3).AdsDDGetTablePropertyfor property 216 returns effective level. NewAdsDDSetUserTableRights/AdsDDGetUserTableRightsmanage per-user/group permissions programmatically. 7 tests intests/unit/abi_dd_perms_test.cpp. (2026-05-26) -
AdsDDGetFieldProperty/AdsDDSetFieldProperty— per-field metadata read/write. Structural props (name 301, type 302, length 303, decimals 304) read live from the table file via a brief open; stored props (required 305, default 306, rule 307, msg 308, comment 309) stored inDataDict::field_props_and persisted asFIELDPROProws in the text format. 6 tests intests/unit/abi_dd_field_prop_test.cpp. (2026-05-26) -
DD triggers —
AdsDDCreateTrigger/AdsDDDropTrigger/AdsDDGetTriggerProperty/AdsDDSetTriggerProperty. Model:TriggerEntry { name, table_alias, event_mask, container, procedure, priority, enabled, comment }inDataDict::triggers_. Persisted asTRIGGERrows in the text format. Event mask uses ADS_BEFORE/AFTER_INSERT/UPDATE/DELETE bits. Execution is a no-op stub (trigger definition is stored and queryable; user code not called). 5 tests intests/unit/abi_dd_trigger_test.cpp. (2026-05-26) -
DD stored procedures —
AdsDDCreateProcedure/AdsDDDropProcedure/AdsDDGetProcProperty/AdsDDSetProcProperty. Model:ProcEntry { name, container, procedure, input_params, output_params, comment }inDataDict::procs_. Persisted asPROCrows. Execution is a no-op stub. 4 tests intests/unit/abi_dd_proc_view_test.cpp. (2026-05-26) -
DD views —
AdsDDCreateView/AdsDDDropView/AdsDDGetViewProperty/AdsDDSetViewProperty. Model:ViewEntry { name, sql, comment }inDataDict::views_. Persisted asVIEWrows.AdsOpenTableexpansion of view alias →AdsExecuteSQLDirectdeferred (see system.* SQL item below). 5 tests intests/unit/abi_dd_proc_view_test.cpp. (2026-05-26) -
system.*SQL virtual tables —SELECT * FROM system.tablesand 10 other aliases:system.indexes,system.users,system.usergroups,system.permissions,system.relations,system.links,system.triggers,system.storedprocedures,system.views,system.dictionary. Each builds an in-memory temp DBF fromDataDictstate and opens it as a read-only cursor.AdsOpenTableview-alias expansion: opening a DD view name executes the view's SQL viaAdsExecuteSQLDirectand returns the cursor. Both DBF and ADT table types are reflected insystem.tables.TABLE_TYPE. 9 tests intests/unit/abi_sql_system_tables_test.cpp. (2026-05-26) -
AdsDDGetIndexProperty/AdsDDSetIndexProperty— per-index metadata read from open index bindings. Properties: file name (401), expression (402), unique (403), descending (404), condition (405, returns ""), key length (406), type (407, returns 0).AdsDDSetIndexPropertyreturns AE_FUNCTION_NOT_AVAILABLE. (2026-05-26) -
DD-related SQL statements — complete:
CREATE DATABASE "path" [PASSWORD ... DESCRIPTION ... ENCRYPT ...];GRANT right [("col")] ON object TO principal;REVOKE right [("col")] ON object FROM principal; 17 built-insp_*stored procedures viaEXECUTE PROCEDURE:sp_CreateUser/DropUser,sp_CreateGroup/DropGroup,sp_AddUserToGroup/RemoveUserFromGroup,sp_ModifyUserProperty/ModifyGroupProperty,sp_AddTableToDatabase/AddIndexFileToDatabase,sp_ModifyTableProperty/ModifyFieldProperty,sp_CreateReferentialIntegrity/DropReferentialIntegrity,sp_CreateLink/DropLink,sp_ModifyDatabase.system.iota(1-row scalar table) andsystem.columns(per-field metadata) added to virtual-table set.DataDictgains explicitGROUPstorage +create_group/delete_group. 10 tests intests/unit/abi_sql_dd_sql_test.cpp. (2026-05-26) -
ADS proprietary ADT encryption — out of scope for now. We use our own AES encryption (M11.2). ADS-original per-table encryption format not reversed; tables with that flag will open but return garbled values. Deferred indefinitely.
-
AdsPackTable/AdsZapTablefor ADT.platform::File::truncate()added (Win32 + POSIX).AdtDriver::zap()truncates the file tohdr_lenafter zeroing the record count, soTable::pack()(zap + re-append survivors) leaves no stale bytes. Tests intests/unit/abi_zap_pack_test.cppverify record count, field values, and exact file size post-operation. (2026-05-24)
- VFP table support (DBF
0x30/0x31).table.cpp:49returns an error for VFP-typed DBF files. Was in the original M4 plan but deferred. Needs aVfpDriverthat handles the_NullFlagssystem field for NULL bitmap and the VFP autoinc field type.
-
CONTAINS/LIKEin join-cursor and aggregateWHERE. Both the join-cursor compile path and the aggregateFILTERcompile path only handleCmp / AND / OR / NOT— anything else returnsAE_FUNCTION_NOT_AVAILABLE.CONTAINSandLIKEare the most common missing operators. Fixed inace_exports.cpp: join-cursor compile lambda, aggregate FILTERcflambda, and CASE WHENcompile_condlambda all now support LIKE (strip trailing spaces +sql_like_match) and CONTAINS (load.ftsviaFts::load/searchbefore building the lambda; capture the hit set byshared_ptr). 3 new tests intests/unit/abi_sql_contains_test.cpp. (2026-05-24) -
CASE WHENconditions beyondCmp/AND/OR/NOT. Fixed:compile_condlambda inace_exports.cppnow handlesKind::In(literal list),IsNull/IsNotNull, andBetweenin addition to the existingCmp/AND/OR/NOT. (2026-05-25) -
FTS query-time token lookup. Already wired:
CONTAINS(col, expr)is handled in all four SQL compile lambdas (main SELECT WHERE, join-cursor, aggregate FILTER, CASE WHEN). Token lookup hits the.ftsinverted index at compile time and captures the record set. (2026-05-24)
-
AdsDecryptTable/AdsEncryptRecord/AdsDecryptRecord— out of scope. We use our own encryption model (M11.2 AES). ADS-original per-record encryption format not reversed. Stubs returnAE_FUNCTION_NOT_AVAILABLE. -
Key derivation hardening.
Connection::set_encryption_passwordzero-pads the password to 32 bytes (noted inconnection.cpp:510). Should use PBKDF2 or Argon2 once a SHA-256 implementation is in the tree. Low urgency for local-use scenarios; critical before any multi-user deployment that stores encrypted tables long-term.
- WAL crash recovery — end-to-end crash recovery implemented and
tested.
Connection::open→recover_orphan_tx_()reads the WAL, identifies transactions with no COMMIT/ABORT (orphans), writes before-images back for UPDATE records, and marks appended rows deleted for APPEND records.LsnMapsidecar makes recovery crash-safe across interrupted passes. WAL APPEND record type added so orphan appends are tracked persistently. 3 tests intests/unit/abi_m5x_recovery_test.cpp. (2026-05-26)
-
AdsEval*Exprfamily — server-side expression evaluation used by Harbour/X#ADSRDD.prgserver-side query path. Implemented:AdsEvalLogicalExpr(AOF boolean expression at current record viaaof::evaluate_record),AdsEvalNumericExpr(field read or numeric literal parse),AdsEvalStringExpr(field read or string literal passthrough). (2026-05-25) -
AdsStmt*helpers — per-statement table-open settings (table_type, lock_type, char_type, read_only, check_rights, disable_enc, collation, passwords) stored in SqlStatement and threaded into AdsExecuteSQLDirect for SELECT/UPDATE/DELETE/INSERT. All 9 setter functions implemented. (2026-05-25) -
AdsRestructureTabletype conversion — CHANGE path now supports C↔N, C/N→L, L→C/N, and D↔C conversions. Raw-copy fallback for other pairs. Test updated. (2026-05-25) -
AdsContinue(LOCATE/CONTINUE loop). Implemented: filter-aware skip(1) on the underlying Table — sinceTable::skip()already walks past non-matching records when a filter or AOF bitmap is active,AdsContinueis a single-step forward move with*pbFound = eof() ? 0 : 1. Test intests/unit/abi_aof_test.cpp. (2026-05-24) -
Table-management stubs.
AdsCopyTableContent(field-name-matched copy between two open tables),AdsCloneTable(full structural clone including deleted records into a temp DBF; returns new handle), andAdsCopyTableStructure(schema-only copy, 0 records). All implemented and tested. (2026-05-25) -
Enumeration stubs.
AdsGetAllTablesenumerates all table handles owned by a connection (iteratesHandleRegistryforHandleKind::Table, filters viaowns_table_ptr).AdsGetAllIndexesenumerates all index handles bound to a table (iteratesindex_bindings()).AdsGetFTSIndexesreturns 0 — FTS indexes have no persistent handles in OpenADS. 3 tests intests/unit/abi_enum_test.cpp. (2026-05-25)
- Forward-only prefetch (M12.21) — disabled in M12.21b after cursor-drift regressions on indexed scans. Re-enable once the indexed-scan drift is understood and fixed.