diff --git a/CHANGELOG.md b/CHANGELOG.md index bef8d8ac..b71250d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# v1.8.0-alpha6 +# v1.8.0-alpha9 * Added `TI_PROTO_CLIENT_REQ_EMIT_PEER` protocol, pr #414. * Changed `copy()` behavior for wrapped type, pr #415. @@ -9,6 +9,12 @@ * Include export enumerator members of type `thing`, pr #420. * Fixed missing "ID" (`#`) field in export, issue #421. * Added `type_all()` function, pr #422. +* Fixed handling non-stored thing with `to_type()`, issue #424. +* Added commit history with new functions, pr #425 and discussion #423. + - `commit()`: https://docs.thingsdb.io/v1/collection-api/commit/ + - `history()`: https://docs.thingsdb.io/v1/thingsdb-api/history/ + - `set_history()`: https://docs.thingsdb.io/v1/thingsdb-api/set_history/ + - `del_history()`: https://docs.thingsdb.io/v1/thingsdb-api/del_history/ # v1.7.6 diff --git a/CMakeLists.txt b/CMakeLists.txt index 87d60ee6..2923ff8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,6 +137,8 @@ set(SOURCES src/ti/closure.c src/ti/collection.c src/ti/collections.c + src/ti/commit.c + src/ti/commits.c src/ti/condition.c src/ti/connect.c src/ti/counters.c @@ -238,11 +240,12 @@ set(SOURCES src/ti/store/storeaccess.c src/ti/store/storecollection.c src/ti/store/storecollections.c + src/ti/store/storecommits.c src/ti/store/storeenums.c src/ti/store/storegcollect.c src/ti/store/storemodules.c - src/ti/store/storenames.c src/ti/store/storenamedrooms.c + src/ti/store/storenames.c src/ti/store/storeprocedures.c src/ti/store/storestatus.c src/ti/store/storetasks.c diff --git a/inc/doc.h b/inc/doc.h index ba681849..e1469a1a 100644 --- a/inc/doc.h +++ b/inc/doc.h @@ -20,14 +20,15 @@ #define DOC_BYTES DOC_SEE("collection-api/bytes") #define DOC_CHANGE_ID DOC_SEE("collection-api/change_id") #define DOC_CLOSURE DOC_SEE("collection-api/closure") +#define DOC_COMMIT DOC_SEE("collection-api/commit") #define DOC_DATETIME DOC_SEE("collection-api/datetime") #define DOC_DEEP DOC_SEE("collection-api/deep") #define DOC_DEL_ENUM DOC_SEE("collection-api/del_enum") #define DOC_DEL_TYPE DOC_SEE("collection-api/del_type") #define DOC_ENUM DOC_SEE("collection-api/enum") +#define DOC_ENUMS_INFO DOC_SEE("collection-api/enums_info") #define DOC_ENUM_INFO DOC_SEE("collection-api/enum_info") #define DOC_ENUM_MAP DOC_SEE("collection-api/enum_map") -#define DOC_ENUMS_INFO DOC_SEE("collection-api/enums_info") #define DOC_ERR DOC_SEE("collection-api/err") #define DOC_EXPORT DOC_SEE("collection-api/export") #define DOC_FLOAT DOC_SEE("collection-api/float") @@ -153,6 +154,7 @@ #define DOC_COLLECTIONS_INFO DOC_SEE("thingsdb-api/collections_info") #define DOC_DEL_COLLECTION DOC_SEE("thingsdb-api/del_collection") #define DOC_DEL_EXPIRED DOC_SEE("thingsdb-api/del_expired") +#define DOC_DEL_HISTORY DOC_SEE("thingsdb-api/del_history") #define DOC_DEL_MODULE DOC_SEE("thingsdb-api/del_module") #define DOC_DEL_NODE DOC_SEE("thingsdb-api/del_node") #define DOC_DEL_TOKEN DOC_SEE("thingsdb-api/del_token") @@ -164,6 +166,7 @@ #define DOC_HAS_NODE DOC_SEE("thingsdb-api/has_node") #define DOC_HAS_TOKEN DOC_SEE("thingsdb-api/has_token") #define DOC_HAS_USER DOC_SEE("thingsdb-api/has_user") +#define DOC_HISTORY DOC_SEE("thingsdb-api/history") #define DOC_MODULE_INFO DOC_SEE("thingsdb-api/module_info") #define DOC_MODULES_INFO DOC_SEE("thingsdb-api/modules_info") #define DOC_NEW_COLLECTION DOC_SEE("thingsdb-api/new_collection") @@ -178,6 +181,7 @@ #define DOC_RESTORE DOC_SEE("thingsdb-api/restore") #define DOC_REVOKE DOC_SEE("thingsdb-api/revoke") #define DOC_SET_DEFAULT_DEEP DOC_SEE("thingsdb-api/set_default_deep") +#define DOC_SET_HISTORY DOC_SEE("thingsdb-api/set_history") #define DOC_SET_MODULE_CONF DOC_SEE("thingsdb-api/set_module_conf") #define DOC_SET_MODULE_SCOPE DOC_SEE("thingsdb-api/set_module_scope") #define DOC_SET_PASSWORD DOC_SEE("thingsdb-api/set_password") diff --git a/inc/ti.h b/inc/ti.h index 184bfee9..aa0bfdbf 100644 --- a/inc/ti.h +++ b/inc/ti.h @@ -107,6 +107,7 @@ struct ti_s vec_t * users; /* ti_user_t */ vec_t * access_node; /* ti_auth_t */ vec_t * access_thingsdb; /* ti_auth_t */ + vec_t * commits; /* ti_commit_t */ smap_t * procedures; /* ti_procedure_t */ smap_t * names; /* weak map for ti_name_t */ smap_t * qcache; /* pointer to cache in stack */ diff --git a/inc/ti/collection.h b/inc/ti/collection.h index 15073503..24f97864 100644 --- a/inc/ti/collection.h +++ b/inc/ti/collection.h @@ -25,6 +25,7 @@ ti_collection_t * ti_collection_create( uint8_t deep); void ti_collection_destroy(ti_collection_t * collection); void ti_collection_drop(ti_collection_t * collection); +int ti_collection_to_pk(ti_collection_t * collection, msgpack_packer * pk); _Bool ti_collection_name_check(const char * name, size_t n, ex_t * e); int ti_collection_rename( ti_collection_t * collection, diff --git a/inc/ti/collection.inline.h b/inc/ti/collection.inline.h index 13a9cfc4..b33474b9 100644 --- a/inc/ti/collection.inline.h +++ b/inc/ti/collection.inline.h @@ -8,36 +8,6 @@ #include #include -static inline int ti_collection_to_pk( - ti_collection_t * collection, - msgpack_packer * pk) -{ - return -( - msgpack_pack_map(pk, 7) || - - mp_pack_str(pk, "collection_id") || - msgpack_pack_uint64(pk, collection->id) || - - mp_pack_str(pk, "name") || - mp_pack_strn(pk, collection->name->data, collection->name->n) || - - mp_pack_str(pk, "created_at") || - msgpack_pack_uint64(pk, collection->created_at) || - - mp_pack_str(pk, "things") || - msgpack_pack_uint64(pk, collection->things->n + collection->gc->n) || - - mp_pack_str(pk, "time_zone") || - mp_pack_strn(pk, collection->tz->name, collection->tz->n) || - - mp_pack_str(pk, "default_deep") || - msgpack_pack_uint64(pk, collection->deep) || - - mp_pack_str(pk, "next_free_id") || - msgpack_pack_uint64(pk, collection->next_free_id) - ); -} - /* * Return a thing with a borrowed reference from the collection, or, * if not found, tries to restore the thing from the garbage collector. diff --git a/inc/ti/collection.t.h b/inc/ti/collection.t.h index 27357b9a..994d5b54 100644 --- a/inc/ti/collection.t.h +++ b/inc/ti/collection.t.h @@ -8,11 +8,12 @@ typedef struct ti_collection_s ti_collection_t; #include #include -#include #include +#include +#include #include -#include #include +#include #include #include #include @@ -39,6 +40,7 @@ struct ti_collection_s uv_mutex_t * lock; /* only for watch/ unwatch/ away-mode */ vec_t * futures; /* no reference, type: ti_future_t */ vec_t * vtasks; /* tasks, type: ti_vtask_t */ + vec_t * commits; /* migration changes ti_commit_t */ guid_t guid; /* derived from collection->id */ }; diff --git a/inc/ti/commit.h b/inc/ti/commit.h new file mode 100644 index 00000000..0c6dde6f --- /dev/null +++ b/inc/ti/commit.h @@ -0,0 +1,41 @@ +/* + * ti/commit.h + */ +#ifndef TI_COMMIT_H_ +#define TI_COMMIT_H_ + +#include +#include +#include +#include +#include +#include +#include + +typedef struct ti_commit_s ti_commit_t; + +ti_commit_t * ti_commit_from_up(mp_unp_t * up); +ti_commit_t * ti_commit_make( + uint64_t id, + const char * code, + ti_raw_t * by, + ti_raw_t * message); +void ti_commit_destroy(ti_commit_t * commit); +int ti_commit_to_pk(ti_commit_t * commit, msgpack_packer * pk); +int ti_commit_to_client_pk( + ti_commit_t * commit, + _Bool detail, + msgpack_packer * pk); +ti_val_t * ti_commit_as_mpval(ti_commit_t * commit, _Bool detail); + +struct ti_commit_s +{ + uint64_t id; /* equal to the change id */ + time_t ts; /* timestamp of the change */ + ti_raw_t * code; /* original query string */ + ti_raw_t * message; /* never NULL */ + ti_raw_t * by; /* never NULL */ + ti_raw_t * err_msg; /* may be NULL */ +}; + +#endif /* TI_COMMIT_H_ */ diff --git a/inc/ti/commits.h b/inc/ti/commits.h new file mode 100644 index 00000000..f43e78ea --- /dev/null +++ b/inc/ti/commits.h @@ -0,0 +1,56 @@ +/* + * ti/commits.h + */ +#ifndef TI_COMMITS_H_ +#define TI_COMMITS_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TI_COMMITS_MASK TI_AUTH_QUERY|TI_AUTH_CHANGE + +typedef struct +{ + ti_raw_t * scope; + ti_raw_t * contains; + ti_regex_t * match; + ti_vint_t * id; + ti_vint_t * last; + ti_vint_t * first; + ti_datetime_t * before; + ti_datetime_t * after; + ti_vbool_t * has_err; + ti_vbool_t * detail; +} ti_commits_options_t; + +typedef struct +{ + vec_t ** commits; + vec_t ** access; + uint64_t scope_id; +} ti_commits_history_t; + +void ti_commits_destroy(vec_t ** commits); +int ti_commits_options( + ti_commits_options_t * options, + ti_thing_t * thing, + _Bool allow_detail, + ex_t * e); +int ti_commits_history( + ti_commits_history_t * history, + ti_commits_options_t * options, + ex_t * e); +vec_t ** ti_commits_from_scope(ti_raw_t * scope, ex_t * e); +int ti_commits_set_history(vec_t ** commits, _Bool state); +vec_t * ti_commits_find(vec_t * commits, ti_commits_options_t * options); +vec_t * ti_commits_del(vec_t ** commits, ti_commits_options_t * options); + +#endif /* TI_COMMITS_H_ */ diff --git a/inc/ti/fn/fn.h b/inc/ti/fn/fn.h index 5bfdad0c..4817344a 100644 --- a/inc/ti/fn/fn.h +++ b/inc/ti/fn/fn.h @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -474,6 +476,17 @@ static inline int fn_not_thingsdb_or_collection_scope( return e->nr; } +static inline int fn_commit(const char * name, ti_query_t * query, ex_t * e) +{ + if (*ti_query_commits(query) && !query->commit) + ex_set(e, EX_OPERATION, + "function `%s` requires a commit " + "before it can be used in the `%s` scope"DOC_COMMIT, + name, + ti_query_scope_name(query)); + return e->nr; +} + static int fn_call(ti_query_t * query, cleri_node_t * nd, ex_t * e) { cleri_node_t * child = nd->children; /* first in argument list */ diff --git a/inc/ti/fn/fncommit.h b/inc/ti/fn/fncommit.h new file mode 100644 index 00000000..35362dc1 --- /dev/null +++ b/inc/ti/fn/fncommit.h @@ -0,0 +1,89 @@ +#include + +static int do__f_commit(ti_query_t * query, cleri_node_t * nd, ex_t * e) +{ + const int nargs = fn_get_nargs(nd); + ti_raw_t * message; + vec_t ** commits = ti_query_commits(query); + ti_commit_t * commit; + + if (fn_not_thingsdb_or_collection_scope("commit", query, e) || + fn_nargs("commit", DOC_COMMIT, 1, nargs, e) || + ti_do_statement(query, nd->children, e) || + fn_arg_str("commit", DOC_COMMIT, 1, query->rval, e)) + return e->nr; + + message = (ti_raw_t *) query->rval; + query->rval = (ti_val_t *) ti_nil_get(); + + if (!(*commits)) + { + ex_set(e, EX_OPERATION, + "commit history is not enabled" + DOC_SET_HISTORY); + goto fail0; + } + + switch ((ti_query_with_enum) query->with_tp) + { + case TI_QUERY_WITH_PARSERES: + /* we use parseres, so it must be a query */ + break; + case TI_QUERY_WITH_PROCEDURE: + ex_set(e, EX_OPERATION, + "function `commit` is not allowed in a procedure"); + goto fail0; + case TI_QUERY_WITH_FUTURE: + ex_set(e, EX_OPERATION, + "function `commit` is not allowed in a future"); + goto fail0; + case TI_QUERY_WITH_TASK: + case TI_QUERY_WITH_TASK_FINISH: + ex_set(e, EX_OPERATION, + "function `commit` is not allowed in a task"); + goto fail0; + } + + if (!query->change) + { + ex_set(e, EX_OPERATION, "commit without a change"DOC_SET_HISTORY); + goto fail0; + } + + if (!query->user) + { + ex_set(e, EX_OPERATION, "commit without a user"DOC_SET_HISTORY); + goto fail0; + } + + if (!message->n) + { + ex_set(e, EX_VALUE_ERROR, "commit message must not be empty"); + goto fail0; + } + + if (query->commit) + { + ex_set(e, EX_OPERATION, "commit message already set"); + goto fail0; + } + + commit = ti_commit_make( + query->change->id, + query->with.parseres->str, + query->user->name, + message); + + if (!commit || vec_push(commits, commit)) + { + ti_commit_destroy(commit); + ex_set_mem(e); + goto fail0; + } + + query->commit = commit; + +fail0: + ti_val_unsafe_drop((ti_val_t *) message); + return e->nr; +} diff --git a/inc/ti/fn/fndelhistory.h b/inc/ti/fn/fndelhistory.h new file mode 100644 index 00000000..d0de2afb --- /dev/null +++ b/inc/ti/fn/fndelhistory.h @@ -0,0 +1,48 @@ +#include + +static int do__f_del_history(ti_query_t * query, cleri_node_t * nd, ex_t * e) +{ + const int nargs = fn_get_nargs(nd); + ti_commits_options_t options = {0}; + ti_commits_history_t history = {0}; + vec_t * deleted; + ti_task_t * task; + + if (fn_not_thingsdb_scope("del_history", query, e) || + fn_nargs("del_history", DOC_DEL_HISTORY, 1, nargs, e) || + ti_do_statement(query, nd->children, e) || + fn_arg_thing("del_history", DOC_DEL_HISTORY, 1, query->rval, e) || + ti_commits_options(&options, (ti_thing_t *) query->rval, false, e) || + ti_commits_history(&history, &options, e)) + return e->nr; + + if (ti_access_check_err(*history.access, query->user, TI_COMMITS_MASK, e)) + return e->nr; + + deleted = ti_commits_del(history.commits, &options); + if (!deleted) + goto fail; + + if (deleted->n) + { + task = ti_task_get_task( + query->change, + query->collection ? query->collection->root : ti.thing0); + if (!task || ti_task_add_del_history(task, history.scope_id, deleted)) + goto undo; + } + ti_val_unsafe_drop(query->rval); + query->rval = (ti_val_t *) ti_vint_create((int64_t) deleted->n); + if (!query->rval) + ex_set_mem(e); + + vec_destroy(deleted, (vec_destroy_cb) ti_commit_destroy); + return e->nr; +undo: + for (vec_each(deleted, ti_commit_t, commit)) + VEC_push(*history.commits, commit); + vec_destroy(deleted, NULL); +fail: + ex_set_mem(e); + return e->nr; +} diff --git a/inc/ti/fn/fndelprocedure.h b/inc/ti/fn/fndelprocedure.h index 9273dbb1..89ab6920 100644 --- a/inc/ti/fn/fndelprocedure.h +++ b/inc/ti/fn/fndelprocedure.h @@ -8,6 +8,7 @@ static int do__f_del_procedure(ti_query_t * query, cleri_node_t * nd, ex_t * e) smap_t * procedures = ti_query_procedures(query); if (fn_not_thingsdb_or_collection_scope("del_procedure", query, e) || + fn_commit("del_procedure", query, e) || fn_nargs("del_procedure", DOC_DEL_PROCEDURE, 1, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("del_procedure", DOC_DEL_PROCEDURE, 1, query->rval, e)) diff --git a/inc/ti/fn/fndeltype.h b/inc/ti/fn/fndeltype.h index c66c0258..67678a53 100644 --- a/inc/ti/fn/fndeltype.h +++ b/inc/ti/fn/fndeltype.h @@ -7,6 +7,7 @@ static int do__f_del_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) ti_task_t * task; if (fn_not_collection_scope("del_type", query, e) || + fn_commit("del_type", query, e) || fn_nargs("del_type", DOC_DEL_TYPE, 1, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("del_type", DOC_DEL_TYPE, 1, query->rval, e)) diff --git a/inc/ti/fn/fnhistory.h b/inc/ti/fn/fnhistory.h new file mode 100644 index 00000000..f0b3112f --- /dev/null +++ b/inc/ti/fn/fnhistory.h @@ -0,0 +1,60 @@ +#include + +static int do__f_history(ti_query_t * query, cleri_node_t * nd, ex_t * e) +{ + const int nargs = fn_get_nargs(nd); + ti_commits_options_t options = {0}; + ti_commits_history_t history = {0}; + vec_t * filtered; + _Bool detail; + uint32_t i = 0; + + if (fn_not_thingsdb_scope("history", query, e) || + fn_nargs("history", DOC_HISTORY, 1, nargs, e) || + ti_do_statement(query, nd->children, e) || + fn_arg_thing("history", DOC_HISTORY, 1, query->rval, e) || + ti_commits_options(&options, (ti_thing_t *) query->rval, true, e) || + ti_commits_history(&history, &options, e)) + return e->nr; + + if (ti_access_check_err(*history.access, query->user, TI_COMMITS_MASK, e)) + return e->nr; + + filtered = ti_commits_find(*history.commits, &options); + if (!filtered) + goto fail; + + detail = options.detail && options.detail->bool_; + + for (vec_each_addr(filtered, void, commit), i++) + { + ti_val_t * v = ti_commit_as_mpval(*commit, detail); + if (!v) + goto fail; + *commit = v; + } + + ti_val_unsafe_drop(query->rval); + if (options.id) + { + assert(filtered->n <= 1); + + query->rval = (ti_val_t *) (filtered->n + ? VEC_first(filtered) + : ti_nil_get()); + vec_destroy(filtered, NULL); + } + else + { + query->rval = (ti_val_t *) ti_varr_from_vec(filtered); + if (!query->rval) + goto fail; + } + + return e->nr; +fail: + filtered->n = i; + vec_destroy(filtered, (vec_destroy_cb) ti_val_unassign_drop); + ex_set_mem(e); + return e->nr; +} diff --git a/inc/ti/fn/fnmodenum.h b/inc/ti/fn/fnmodenum.h index 75235759..3ad01fdf 100644 --- a/inc/ti/fn/fnmodenum.h +++ b/inc/ti/fn/fnmodenum.h @@ -313,6 +313,7 @@ static int do__f_mod_enum(ti_query_t * query, cleri_node_t * nd, ex_t * e) const int nargs = fn_get_nargs(nd); if (fn_not_collection_scope("mod_enum", query, e) || + fn_commit("mod_enum", query, e) || fn_nargs_min("mod_enum", DOC_MOD_ENUM, 3, nargs, e) || ti_do_statement(query, child, e) || fn_arg_str_slow("mod_enum", DOC_MOD_ENUM, 1, query->rval, e)) diff --git a/inc/ti/fn/fnmodprocedure.h b/inc/ti/fn/fnmodprocedure.h index 08600c75..bfe15a0a 100644 --- a/inc/ti/fn/fnmodprocedure.h +++ b/inc/ti/fn/fnmodprocedure.h @@ -9,6 +9,7 @@ static int do__f_mod_procedure(ti_query_t * query, cleri_node_t * nd, ex_t * e) smap_t * procedures = ti_query_procedures(query); if (fn_not_thingsdb_or_collection_scope("mod_procedure", query, e) || + fn_commit("mod_procedure", query, e) || fn_nargs("mod_procedure", DOC_MOD_PROCEDURE, 2, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("mod_procedure", DOC_MOD_PROCEDURE, 1, query->rval, e)) diff --git a/inc/ti/fn/fnmodtype.h b/inc/ti/fn/fnmodtype.h index 4887dd6d..90659345 100644 --- a/inc/ti/fn/fnmodtype.h +++ b/inc/ti/fn/fnmodtype.h @@ -1489,6 +1489,7 @@ static int do__f_mod_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) const int nargs = fn_get_nargs(nd); if (fn_not_collection_scope("mod_type", query, e) || + fn_commit("mod_type", query, e) || fn_nargs_min("mod_type", DOC_MOD_TYPE, 3, nargs, e) || ti_do_statement(query, child, e) || fn_arg_str_slow("mod_type", DOC_MOD_TYPE, 1, query->rval, e)) @@ -1546,7 +1547,7 @@ static int do__f_mod_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) fn_arg_name_check("mod_type", DOC_MOD_TYPE, 3, query->rval, e)) goto fail1; - name = ti_names_from_raw((ti_raw_t *) query->rval); + name = ti_names_from_raw_slow((ti_raw_t *) query->rval); if (!name) { ex_set_mem(e); diff --git a/inc/ti/fn/fnnewprocedure.h b/inc/ti/fn/fnnewprocedure.h index a1336366..17dd78df 100644 --- a/inc/ti/fn/fnnewprocedure.h +++ b/inc/ti/fn/fnnewprocedure.h @@ -11,6 +11,7 @@ static int do__f_new_procedure(ti_query_t * query, cleri_node_t * nd, ex_t * e) smap_t * procedures = ti_query_procedures(query); if (fn_not_thingsdb_or_collection_scope("new_procedure", query, e) || + fn_commit("new_procedure", query, e) || fn_nargs("new_procedure", DOC_NEW_PROCEDURE, 2, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("new_procedure", DOC_NEW_PROCEDURE, 1, query->rval, e)) diff --git a/inc/ti/fn/fnnewtype.h b/inc/ti/fn/fnnewtype.h index 228d22ad..2089739a 100644 --- a/inc/ti/fn/fnnewtype.h +++ b/inc/ti/fn/fnnewtype.h @@ -11,6 +11,7 @@ static int do__f_new_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) uint8_t flags = 0; if (fn_not_collection_scope("new_type", query, e) || + fn_commit("new_type", query, e) || fn_nargs_range("new_type", DOC_NEW_TYPE, 1, 3, nargs, e) || ti_do_statement(query, child, e) || fn_arg_str("new_type", DOC_NEW_TYPE, 1, query->rval, e)) diff --git a/inc/ti/fn/fnrenameenum.h b/inc/ti/fn/fnrenameenum.h index 0a829a6f..b47b4d93 100644 --- a/inc/ti/fn/fnrenameenum.h +++ b/inc/ti/fn/fnrenameenum.h @@ -8,6 +8,7 @@ static int do__f_rename_enum(ti_query_t * query, cleri_node_t * nd, ex_t * e) ti_raw_t * oname, * nname; if (fn_not_collection_scope("rename_enum", query, e) || + fn_commit("rename_enum", query, e) || fn_nargs("rename_enum", DOC_RENAME_ENUM, 2, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("rename_enum", DOC_RENAME_ENUM, 1, query->rval, e)) diff --git a/inc/ti/fn/fnrenameprocedure.h b/inc/ti/fn/fnrenameprocedure.h index d0e815b7..c78dae6f 100644 --- a/inc/ti/fn/fnrenameprocedure.h +++ b/inc/ti/fn/fnrenameprocedure.h @@ -9,6 +9,7 @@ static int do__f_rename_procedure(ti_query_t * query, cleri_node_t * nd, ex_t * smap_t * procedures = ti_query_procedures(query); if (fn_not_thingsdb_or_collection_scope("rename_procedure", query, e) || + fn_commit("rename_procedure", query, e) || fn_nargs("rename_procedure", DOC_RENAME_PROCEDURE, 2, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("rename_procedure", DOC_RENAME_PROCEDURE, 1, query->rval, e)) diff --git a/inc/ti/fn/fnrenametype.h b/inc/ti/fn/fnrenametype.h index 5ff5823e..f40d262c 100644 --- a/inc/ti/fn/fnrenametype.h +++ b/inc/ti/fn/fnrenametype.h @@ -9,6 +9,7 @@ static int do__f_rename_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) if (fn_not_collection_scope("rename_type", query, e) || + fn_commit("rename_type", query, e) || fn_nargs("rename_type", DOC_RENAME_TYPE, 2, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("rename_type", DOC_RENAME_TYPE, 1, query->rval, e)) diff --git a/inc/ti/fn/fnsetenum.h b/inc/ti/fn/fnsetenum.h index 0f55e69f..7a105245 100644 --- a/inc/ti/fn/fnsetenum.h +++ b/inc/ti/fn/fnsetenum.h @@ -11,6 +11,7 @@ static int do__f_set_enum(ti_query_t * query, cleri_node_t * nd, ex_t * e) uint64_t ts_now = util_now_usec(); if (fn_not_collection_scope("set_enum", query, e) || + fn_commit("set_enum", query, e) || fn_nargs("set_enum", DOC_SET_ENUM, 2, nargs, e) || ti_do_statement(query, nd->children, e) || fn_arg_str("set_enum", DOC_SET_ENUM, 1, query->rval, e)) diff --git a/inc/ti/fn/fnsethistory.h b/inc/ti/fn/fnsethistory.h new file mode 100644 index 00000000..b8e34202 --- /dev/null +++ b/inc/ti/fn/fnsethistory.h @@ -0,0 +1,53 @@ +#include + +static int do__f_set_history(ti_query_t * query, cleri_node_t * nd, ex_t * e) +{ + const int nargs = fn_get_nargs(nd); + _Bool state; + ti_task_t * task; + uint64_t scope_id; + vec_t ** access_, ** commits; + + if (fn_not_thingsdb_scope("set_history", query, e) || + fn_nargs("set_history", DOC_SET_HISTORY, 2, nargs, e) || + ti_do_statement(query, nd->children, e)) + return e->nr; + + access_ = ti_val_get_access(query->rval, e, &scope_id); + if (e->nr || ti_access_check_err(*access_, query->user, TI_COMMITS_MASK, e)) + return e->nr; + + commits = ti_commits_from_scope((ti_raw_t *) query->rval, e); + if (e->nr) + return e->nr; + + ti_val_unsafe_drop(query->rval); + query->rval = NULL; + + /* read state */ + if (ti_do_statement(query, nd->children->next->next, e) || + fn_arg_bool("set_history", DOC_SET_HISTORY, 2, query->rval, e)) + return e->nr; + + state = VBOOL(query->rval); + ti_val_unsafe_drop(query->rval); + query->rval = (ti_val_t *) ti_nil_get(); + + if (state != !!(*commits)) + { + if (ti_commits_set_history(commits, state)) + { + ex_set_mem(e); + return e->nr; + } + + task = ti_task_get_task( + query->change, + query->collection ? query->collection->root : ti.thing0); + + if (!task || ti_task_add_set_history(task, scope_id, state)) + ex_set_mem(e); /* task cleanup is not required */ + } + + return e->nr; +} diff --git a/inc/ti/fn/fnsettype.h b/inc/ti/fn/fnsettype.h index 335ade99..58f5e7a1 100644 --- a/inc/ti/fn/fnsettype.h +++ b/inc/ti/fn/fnsettype.h @@ -14,6 +14,7 @@ static int do__f_set_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) _Bool hid = false; if (fn_not_collection_scope("set_type", query, e) || + fn_commit("set_type", query, e) || fn_nargs_range("set_type", DOC_SET_TYPE, 2, 4, nargs, e) || ti_do_statement(query, child, e) || fn_arg_str("set_type", DOC_SET_TYPE, 1, query->rval, e)) diff --git a/inc/ti/fn/fntotype.h b/inc/ti/fn/fntotype.h index 2d21930c..b2e3baa6 100644 --- a/inc/ti/fn/fntotype.h +++ b/inc/ti/fn/fntotype.h @@ -11,6 +11,19 @@ static int do__f_to_type(ti_query_t * query, cleri_node_t * nd, ex_t * e) if (!ti_val_is_object(query->rval)) return fn_call_try("to_type", query, nd, e); + if (query->collection && + query->collection->commits && + query->collection->root == (ti_thing_t *) query->rval && + !query->commit) + { + ex_set(e, EX_OPERATION, + "function `to_type` requires a commit before " + "it can be used on the `root()` of a `%s` scope " + "with commit history enabled"DOC_COMMIT, + ti_query_scope_name(query)); + return e->nr; + } + if (fn_nargs("to_type", DOC_THING_TO_TYPE, 1, nargs, e) || ti_query_test_thing_operation(query, e)) return e->nr; diff --git a/inc/ti/names.h b/inc/ti/names.h index 4977cb8b..665f4fc1 100644 --- a/inc/ti/names.h +++ b/inc/ti/names.h @@ -38,15 +38,13 @@ static inline ti_name_t * ti_names_get(const char * str, size_t n) return ti_names_new(str, n); } -static inline ti_name_t * ti_names_from_str(const char * str) -{ - return ti_names_get(str, strlen(str)); -} +ti_name_t * ti_names_get_slow(const char * str, size_t n); -static inline ti_name_t * ti_names_from_raw(ti_raw_t * raw) -{ - return ti_names_get((const char *) raw->data, raw->n); -} +#define ti_names_from_raw(raw__) \ + ti_names_get((const char *) (raw__)->data, (raw__)->n) +#define ti_names_from_raw_slow(raw__) \ + ti_names_get_slow((const char *) (raw__)->data, (raw__)->n) +#define ti_names_from_str_slow(str__) ti_names_get_slow((str__), strlen(str__)) static inline ti_name_t * ti_names_weak_from_raw(ti_raw_t * raw) { diff --git a/inc/ti/query.inline.h b/inc/ti/query.inline.h index 721255c5..66ca43ca 100644 --- a/inc/ti/query.inline.h +++ b/inc/ti/query.inline.h @@ -72,6 +72,13 @@ static inline uint64_t ti_query_scope_id(ti_query_t * query) : TI_SCOPE_NODE; } +static inline vec_t ** ti_query_commits(ti_query_t * query) +{ + return query->collection + ? &query->collection->commits + : &ti.commits; +} + static inline int ti_query_test_vset_operation(ti_query_t * query, ex_t * e) { if (!query->change && ti_vset_is_stored((ti_vset_t *) query->rval)) diff --git a/inc/ti/query.t.h b/inc/ti/query.t.h index 42920447..c5f56b44 100644 --- a/inc/ti/query.t.h +++ b/inc/ti/query.t.h @@ -9,16 +9,17 @@ typedef int (*ti_query_vars_walk_cb)(void * data, void * arg); #include #include +#include #include #include -#include #include #include +#include #include #include -#include #include #include +#include #include #include @@ -72,7 +73,6 @@ typedef union ti_vtask_t * vtask; /* when called as task */ } ti_query_with_t; - struct ti_query_s { uint32_t local_stack; /* variable scopes start here */ @@ -92,6 +92,7 @@ struct ti_query_s ti_change_t * change; /* with reference, only when a change is required otherwise NULL */ + ti_commit_t * commit; /* NULL if no migration change */ vec_t * immutable_cache; /* ti_val_t, Only for immutable and collection independent variable and temporary used for procedures to populate the closure arguments diff --git a/inc/ti/store.h b/inc/ti/store.h index 541ae33a..0fa398b6 100644 --- a/inc/ti/store.h +++ b/inc/ti/store.h @@ -20,6 +20,7 @@ struct ti_store_s char * access_node_fn; char * access_thingsdb_fn; char * collections_fn; + char * commits_fn; char * id_stat_fn; char * names_fn; char * prev_path; diff --git a/inc/ti/store/storecollection.h b/inc/ti/store/storecollection.h index 8ec7b4b8..298e3296 100644 --- a/inc/ti/store/storecollection.h +++ b/inc/ti/store/storecollection.h @@ -49,7 +49,9 @@ char * ti_store_collection_gcthings_fn( char * ti_store_collection_named_rooms_fn( const char * path, uint64_t collection_id); - +char * ti_store_collection_commits_fn( + const char * path, + uint64_t collection_id); struct ti_store_collection_s { @@ -57,6 +59,7 @@ struct ti_store_collection_s char * collection_fn; char * props_fn; char * collection_path; + char * commits_fn; char * names_fn; char * named_rooms_fn; char * procedures_fn; diff --git a/inc/ti/store/storecommits.h b/inc/ti/store/storecommits.h new file mode 100644 index 00000000..e778e38f --- /dev/null +++ b/inc/ti/store/storecommits.h @@ -0,0 +1,13 @@ +/* + * ti/store/storecommits.h + */ +#ifndef TI_STORE_COMMITS_H_ +#define TI_STORE_COMMITS_H_ + +#include +#include + +int ti_store_commits_store(vec_t * commits, const char * fn); +int ti_store_commits_restore(vec_t ** commits, const char * fn); + +#endif /* TI_STORE_COMMITS_H_ */ diff --git a/inc/ti/task.h b/inc/ti/task.h index eb4022a9..76a7f80f 100644 --- a/inc/ti/task.h +++ b/inc/ti/task.h @@ -188,5 +188,11 @@ int ti_task_add_whitelist_del( ti_user_t * user, ti_val_t * val, int wid); +int ti_task_add_commit_add(ti_task_t * task, ti_commit_t * commit); +int ti_task_add_set_history(ti_task_t * task, uint64_t scope_id, _Bool state); +int ti_task_add_del_history( + ti_task_t * task, + uint64_t scope_id, + vec_t * commits); #endif /* TI_TASK_H_ */ diff --git a/inc/ti/task.t.h b/inc/ti/task.t.h index 8de001b8..0e13f0f8 100644 --- a/inc/ti/task.t.h +++ b/inc/ti/task.t.h @@ -92,6 +92,9 @@ typedef enum TI_TASK_ROOM_SET_NAME, /* 79 */ TI_TASK_WHITELIST_ADD, /* 80 */ TI_TASK_WHITELIST_DEL, /* 81 */ + TI_TASK_SET_HISTORY, /* 82 */ + TI_TASK_DEL_HISTORY, /* 83 */ + TI_TASK_COMMIT, /* 84 */ } ti_task_enum; typedef struct ti_task_s ti_task_t; diff --git a/inc/ti/version.h b/inc/ti/version.h index a3025d8a..c8ff1680 100644 --- a/inc/ti/version.h +++ b/inc/ti/version.h @@ -25,7 +25,7 @@ * "-rc0" * "" */ -#define TI_VERSION_PRE_RELEASE "-alpha6" +#define TI_VERSION_PRE_RELEASE "-alpha9" #define TI_MAINTAINER \ "Jeroen van der Heijden " diff --git a/itest/run_all_tests.py b/itest/run_all_tests.py index aeed45e1..cdfd5122 100755 --- a/itest/run_all_tests.py +++ b/itest/run_all_tests.py @@ -7,6 +7,7 @@ from test_backup import TestBackup from test_changes import TestChanges from test_collection_functions import TestCollectionFunctions +from test_commits import TestCommits from test_datetime import TestDatetime from test_dict import TestDict from test_doc_url import TestDocUrl @@ -78,6 +79,7 @@ def no_mem_test(test_class): run_test(TestBackup(), hide_version=hide_version()) run_test(TestChanges(), hide_version=hide_version()) run_test(TestCollectionFunctions(), hide_version=hide_version()) + run_test(TestCommits(), hide_version=hide_version()) run_test(TestDatetime(), hide_version=hide_version()) run_test(TestDict(), hide_version=hide_version()) if args.doc_test is True: diff --git a/itest/test_advanced.py b/itest/test_advanced.py index 3de9378f..2d791fcb 100755 --- a/itest/test_advanced.py +++ b/itest/test_advanced.py @@ -1570,6 +1570,8 @@ async def test_assign_in_def(self, client): async def test_export(self, client): script = r''' +try(commit('Source: collection `stuff`')); + new_type('Friend'); new_type('Person'); new_type('Root'); @@ -2841,6 +2843,48 @@ async def test_return_no_args(self, client): \t}; }""") + async def test_recursive_enum_export(self, client): + q = client.query + await q("""//ti + t = {}; + set_enum('E', {A: t}); + E{A}.value().t = E{A}.value(); + """) + + script = await q("""//ti + export(); + """) + self.assertEqual(script, r''' +try(commit('Source: collection `stuff`')); + + +set_enum('E', { + A: {}, +}); + + + +mod_enum('E', 'mod', 'A', { + t: { + t: { + t: { + t: {} /* WARN: max deep reached */, + }, + }, + }, +}); + + + + +'DONE'; +'''.lstrip().replace(' ', '\t')) + await q("""//ti + new_collection('other') + """, scope='/t') + res = await q(script, scope='//other') + self.assertEqual(res, 'DONE') + if __name__ == '__main__': run_test(TestAdvanced()) diff --git a/itest/test_commits.py b/itest/test_commits.py new file mode 100755 index 00000000..4a891cfb --- /dev/null +++ b/itest/test_commits.py @@ -0,0 +1,579 @@ +#!/usr/bin/env python +import asyncio +from lib import run_test +from lib import default_test_setup +from lib.testbase import TestBase +from lib.client import get_client +from thingsdb.exceptions import TypeError +from thingsdb.exceptions import NumArgumentsError +from thingsdb.exceptions import LookupError +from thingsdb.exceptions import OperationError +from thingsdb.exceptions import ForbiddenError + + +class TestCommits(TestBase): + + title = 'Test commit history' + + def with_node1(self): + if hasattr(self, 'node1'): + return True + print(''' + WARNING: Test requires a second node!!! + ''') + + @default_test_setup(num_nodes=2, seed=1, threshold_full_storage=10) + async def async_run(self): + + await self.node0.init_and_run() + + client0 = await get_client(self.node0) + client0.set_default_scope('//stuff') + + await self.node1.join_until_ready(client0) + + client1 = await get_client(self.node1) + client1.set_default_scope('//stuff') + + await self.run_tests(client0.query, client1.query, client0) + + for client in (client0, client1): + client.close() + await client.wait_closed() + + async def test_set_history(self, q0, q1, c0): + with self.assertRaisesRegex( + LookupError, + r'function `set_history` is undefined in the `@node` scope; ' + r'you might want to query the `@thingsdb` scope\?'): + await q0("""//ti + set_history(); + """, scope='/n') + + with self.assertRaisesRegex( + NumArgumentsError, + r'function `set_history` takes 2 arguments but 0 were given;'): + await q0("""//ti + set_history(); + """, scope='/t') + + with self.assertRaisesRegex( + TypeError, + r'expecting a scope to be of type `str` but ' + r'got type `nil` instead'): + await q0("""//ti + set_history(nil, true); + """, scope='/t') + + with self.assertRaisesRegex( + ValueError, + r'invalid scope; scopes must not be empty'): + await q0("""//ti + set_history("", true); + """, scope='/t') + + with self.assertRaisesRegex( + OperationError, + r'commit history is not supported by `@node` scopes'): + await q0("""//ti + set_history("/n", true); + """, scope='/t') + + with self.assertRaisesRegex( + LookupError, + r'collection `s` not found'): + await q0("""//ti + set_history("//s", true); + """, scope='/t') + + with self.assertRaisesRegex( + TypeError, + r'function `set_history` expects argument 2 to be of ' + r'type `bool` but got type `int` instead;'): + await q0("""//ti + set_history("//stuff", 0); + """, scope='/t') + + await q0("""//ti + set_history("//stuff", true); + """, scope='/t') + for q in (q0, q1): + r = await q("""//ti + wse(); + collection_info('stuff').load().commit_history; + """, scope='/t') + self.assertEqual(r, 0) + + await q0("""//ti + set_history("//stuff", false); + """, scope='/t') + for q in (q0, q1): + r = await q("""//ti + wse(); + collection_info('stuff').load().commit_history; + """, scope='/t') + self.assertEqual(r, 'disabled') + + await q0("""//ti + set_history("/t", true); + """, scope='/t') + for q in (q0, q1): + r = await q("""//ti + wse(); + node_info().load().commit_history; + """, scope='/n') + self.assertEqual(r, 0) + + await q0("""//ti + set_history("/t", false); + """, scope='/t') + for q in (q0, q1): + r = await q("""//ti + wse(); + node_info().load().commit_history; + """, scope='/n') + self.assertEqual(r, 'disabled') + + async def test_access(self, q0, q1, c0): + await q0("""//ti + new_collection('junk'); + new_user('test1'); + set_password('test1', 'test'); + grant('/t', 'test1', QUERY); + grant('//stuff', 'test1', QUERY|CHANGE); + grant('//junk', 'test1', QUERY); + """, scope='/t') + + q = (await get_client( + self.node0, + auth=['test1', 'test'], + auto_reconnect=False)).query + + with self.assertRaisesRegex( + ForbiddenError, + r'user `test1` is missing the required ' + r'privileges \(`CHANGE`\) on scope `@thingsdb`;'): + await q("""//ti + set_history('//junk', true); + """, scope='/t') + + await q0("""//ti + grant('/t', 'test1', QUERY|CHANGE); + """, scope='/t') + + with self.assertRaisesRegex( + ForbiddenError, + r'user `test1` is missing the required ' + r'privileges \(`QUERY\|CHANGE`\) on ' + r'scope `@collection:junk`;'): + await q("""//ti + set_history('//junk', true); + """, scope='/t') + await q("""//ti + set_history('//stuff', true); + set_history('/t', false); + """, scope='/t') + + async def test_history(self, q0, q1, c0): + await q0("""//ti + set_history('//stuff', false); + set_history('/t', false); + """, scope='/t') + + with self.assertRaisesRegex( + LookupError, + r'function `history` is undefined in the `@node` scope; ' + r'you might want to query the `@thingsdb` scope\?'): + await q0("""//ti + history({last:0}); + """, scope='/n') + + with self.assertRaisesRegex( + LookupError, + r'no scope found with commit history enabled;'): + await q0("""//ti + history({last:0}); + """, scope='/t') + + with self.assertRaisesRegex( + OperationError, + r'commit history is not enabled for the `/t` scope'): + await q0("""//ti + history({scope: '/t', last:0}); + """, scope='/t') + + with self.assertRaisesRegex( + OperationError, + r'commit history is not enabled for the `//stuff` scope'): + await q0("""//ti + history({scope: '//stuff', last:0}); + """, scope='/t') + + with self.assertRaisesRegex( + OperationError, + r'commit history is not supported by `@node` scopes'): + await q0("""//ti + history({scope: '/n', last:0}); + """, scope='/t') + + with self.assertRaisesRegex( + LookupError, + r'collection `s` not found'): + await q0("""//ti + history({scope: '//s', last:0}); + """, scope='/t') + + await q0("""//ti + set_history('//stuff', true); + set_history('/t', true); + """, scope='/t') + + with self.assertRaisesRegex( + ValueError, + r'option `last` expects an integer value greater ' + r'than or equal to 0'): + await q0("""//ti + history({scope: '/t', last:-1}); + """, scope='/t') + + with self.assertRaisesRegex( + ValueError, + r'option `contains` must be a non empty string'): + await q0("""//ti + history({scope: '/t', contains:""}); + """, scope='/t') + + with self.assertRaisesRegex( + TypeError, + r'expecting `last` to be of type `int` but got ' + r'type `bool` instead'): + await q0("""//ti + history({scope: '/t', last:false}); + """, scope='/t') + + with self.assertRaisesRegex( + LookupError, + r'more than one scope has commit history enabled; you must ' + r'use the `scope` option to tell ThingsDB which scope ' + r'to use;'): + await q0("""//ti + history({last:0}); + """, scope='/t') + + with self.assertRaisesRegex( + LookupError, + r'at least one of the following options must be set: ' + r'`contains`, `match`, `id`, `first`, `last`, `before`, ' + r'`after`, `has_err`;'): + await q0("""//ti + history({scope: '//stuff'}); + """, scope='/t') + + with self.assertRaisesRegex( + ValueError, + r'invalid option `x` was provided; valid options are: ' + r'`scope`, `contains`, `match`, `id`, `first`, `last`, ' + r'`before`, `after`, `has_err`, `detail`'): + await q0("""//ti + history({scope: '//stuff', x: ''}); + """, scope='/t') + + async def test_del_history(self, q0, q1, c0): + await q0("""//ti + set_history('//stuff', false); + set_history('/t', false); + """, scope='/t') + + with self.assertRaisesRegex( + LookupError, + r'function `del_history` is undefined in the `@node` scope; ' + r'you might want to query the `@thingsdb` scope\?'): + await q0("""//ti + del_history({last:0}); + """, scope='/n') + + with self.assertRaisesRegex( + LookupError, + r'no scope found with commit history enabled;'): + await q0("""//ti + del_history({last:0}); + """, scope='/t') + + await q0("""//ti + set_history('//stuff', true); + set_history('/t', true); + """, scope='/t') + + with self.assertRaisesRegex( + ValueError, + r'invalid option `x` was provided; valid options are: ' + r'`scope`, `contains`, `match`, `id`, `first`, `last`, ' + r'`before`, `after`, `has_err`$'): + await q0("""//ti + del_history({scope: '//stuff', x: ''}); + """, scope='/t') + + async def test_commit(self, q0, q1, c0): + await q0("""//ti + set_history('//stuff', false); + set_history('/t', false); + """, scope='/t') + with self.assertRaisesRegex( + LookupError, + r'function `commit` is undefined in the `@node` scope; ' + r'you might want to query the `@thingsdb` or ' + r'a `@collection` scope\?'): + await q0("""//ti + commit('Test'); + """, scope='/n') + with self.assertRaisesRegex( + OperationError, + r'commit history is not enabled'): + await q0("""//ti + commit('Test'); + new_type('A'); + """) + + await q0("""//ti + set_history('//stuff', true); + set_history('/t', true); + """, scope='/t') + + with self.assertRaisesRegex( + NumArgumentsError, + r'function `commit` takes 1 argument but 0 were given'): + await q0("""//ti + commit(); + new_type('A'); + """) + with self.assertRaisesRegex( + TypeError, + r'function `commit` expects argument 1 to be of type `str` ' + r'but got type `int` instead'): + await q0("""//ti + commit(123); + new_type('A'); + """) + with self.assertRaisesRegex( + OperationError, + r'commit without a change;'): + await q0("""//ti + commit('Test'); + """) + with self.assertRaisesRegex( + OperationError, + r'function `commit` is not allowed in a future'): + await q0("""//ti + future(||{ + commit('Test'); + new_type('F'); + }); + """) + + for i in range(10): + # use f-string format as we want T0..9 be part of the code + await q0(f"""//ti + commit('Create type T{i}'); + set_type('T{i}', {{name: 'str'}}); + """) + + await asyncio.sleep(3.0) + + for i in range(10): + # without f-string to ensure message + await q0("""//ti + commit(`Add x to type T{i}`); + mod_type(`T{i}`, 'add', 'x', 'int'); + """, i=i) + + before, b5, after, a5, both, deleted, t0m, t0c = await q0("""//ti + wse(); + moment = datetime().move('seconds', -2); + [ + history({ + scope: '//stuff', + before: moment, + }), + history({ + scope: '//stuff', + before: moment, + last: 5, + }), + history({ + scope: '//stuff', + after: moment, + }), + history({ + scope: '//stuff', + after: moment, + first: 5, + }), + history({ + scope: '//stuff', + before: moment, + after: moment, + }), + del_history({ + scope: '//stuff', + before: moment, + after: moment, + }), + history({ + scope: '//stuff', + match: /\\bT0\\b/, + }), + history({ + scope: '//stuff', + contains: 'T0' + }), + ]; + """, scope='/t') + + self.assertEqual(len(before), 10) + self.assertEqual(len(after), 10) + self.assertEqual(len(both), 0) + self.assertEqual(len(t0m), 2) + self.assertEqual(deleted, 0) + self.assertEqual(t0m, t0c) + self.assertTrue(before[0]['id'] < after[0]['id']) + self.assertEqual(before[-5:], b5) + self.assertEqual(after[:5], a5) + + with self.assertRaisesRegex( + OperationError, + r'function `to_type` requires a commit before it can be used ' + r'on the `root\(\)` of a `@collection` scope with commit ' + r'history enabled;'): + await q0("""//ti + .to_type('T0'); + """) + + try: + await q0("""//ti + commit(`Add another type T0 (should fail)`); + new_type('T0'); + """) + except Exception as e: + err_msg = str(e) + else: + err_msg = None + + self.assertTrue(isinstance(err_msg, str)) + + await self.node0.shutdown() + await self.node0.run() + + for q in (q0, q1): + with_err, no_err, both0, both1, both2 = await q("""//ti + [ + history({ + scope: '//stuff', + has_err: true, + }), + history({ + scope: '//stuff', + has_err: false, + }), + history({ + scope: '//stuff', + match: /.*/, + }), + history({ + scope: '//stuff', + last: 9999, + }), + history({ + scope: '//stuff', + first: 9999, + }), ]; + """, scope='/t') + self.assertEqual(len(with_err), 1) + self.assertEqual(len(no_err), 20) + self.assertEqual(len(both0), 21) + self.assertEqual(len(both1), 21) + self.assertEqual(len(both2), 21) + self.assertEqual(with_err[0]['err_msg'], err_msg) + + commit_id = with_err[0]['id'] + + commit = await q0("""//ti + history({ + scope: '//stuff', + id: commit_id, + detail: true, + }); + """, commit_id=commit_id, scope='/t') + + self.assertEqual(commit['id'], commit_id) + self.assertTrue('code' in commit) + self.assertTrue('should fail' in commit['code']) + + n = await q0("""//ti + del_history({ + scope: '//stuff', + first: 3, + }); + """, commit_id=commit_id, scope='/t') + self.assertEqual(n, 3) + n = await q0("""//ti + del_history({ + scope: '//stuff', + last: 3, + }); + """, commit_id=commit_id, scope='/t') + self.assertEqual(n, 3) + + for q in (q0, q1): + t0, t4, t9, n = await q("""//ti + wse(); + [ + history({ + scope: '//stuff', + contains: 'T0', + }), + history({ + scope: '//stuff', + contains: 'T4', + }), + history({ + scope: '//stuff', + contains: 'T9', + }), + collection_info('stuff').load().commit_history, + ]; + """, scope='/t') + self.assertEqual(len(t0), 1) + self.assertEqual(len(t4), 2) + self.assertEqual(len(t9), 1) + self.assertEqual(n, 15) + + await q0("""//ti + commit('Test TI scope'); + new_user('A'); + """, scope='/t') + + for q in (q0, q1): + await q('wse()') + n = await q('node_info().load().commit_history;', scope='/n') + self.assertEqual(n, 1) + + async def test_from_procedure(self, q0, q1, c0): + await q0("""//ti + set_history('//stuff', true); + set_history('/t', false); + """, scope='/t') + await q0("""//ti + commit('add procedure...'); + new_procedure('test_commit', |msg| { + commit(msg); + wse(); + }); + """) + with self.assertRaisesRegex( + OperationError, + r'function `commit` is not allowed in a procedure'): + await c0.run('test_commit', 'test') + + +if __name__ == '__main__': + run_test(TestCommits()) diff --git a/itest/test_node_functions.py b/itest/test_node_functions.py index 11c0bc7e..135dc446 100755 --- a/itest/test_node_functions.py +++ b/itest/test_node_functions.py @@ -84,7 +84,7 @@ async def test_node_info(self, client): node = await client.query('node_info();') - self.assertEqual(len(node), 41) + self.assertEqual(len(node), 42) self.assertIn("node_id", node) self.assertIn("version", node) @@ -127,6 +127,7 @@ async def test_node_info(self, client): self.assertIn('modules_path', node) self.assertIn('architecture', node) self.assertIn('platform', node) + self.assertIn('commit_history', node) self.assertTrue(isinstance(node["node_id"], int)) self.assertTrue(isinstance(node["version"], str)) @@ -169,6 +170,7 @@ async def test_node_info(self, client): self.assertTrue(isinstance(node["modules_path"], str)) self.assertTrue(isinstance(node["architecture"], str)) self.assertTrue(isinstance(node["platform"], str)) + self.assertEqual(node["commit_history"], "disabled") async def test_nodes_info(self, client): with self.assertRaisesRegex( diff --git a/itest/test_relations.py b/itest/test_relations.py index 779f3455..99612add 100755 --- a/itest/test_relations.py +++ b/itest/test_relations.py @@ -29,7 +29,7 @@ async def async_run(self): client = await get_client(self.node0) client.set_default_scope('//stuff') - # add another node otherwise backups are not possible + # add another node if hasattr(self, 'node1'): await self.node1.join_until_ready(client) @@ -415,6 +415,8 @@ async def test_set_set_init_state(self, client0): await client.query(r'''wse();''') res = await client.query(r'''export();''') self.assertEqual(res, r''' +try(commit('Source: collection `stuff`')); + new_type('A'); new_type('B'); new_type('C'); diff --git a/itest/test_thingsdb_functions.py b/itest/test_thingsdb_functions.py index 14f87064..fe06fe39 100755 --- a/itest/test_thingsdb_functions.py +++ b/itest/test_thingsdb_functions.py @@ -118,7 +118,7 @@ async def test_collections_info(self, client): collections = await client.query('collections_info();') self.assertEqual(len(collections), 1) - self.assertEqual(len(collections[0]), 7) + self.assertEqual(len(collections[0]), 8) self.assertIn("collection_id", collections[0]) self.assertIn("next_free_id", collections[0]) @@ -127,6 +127,7 @@ async def test_collections_info(self, client): self.assertIn("created_at", collections[0]) self.assertIn("time_zone", collections[0]) self.assertIn("default_deep", collections[0]) + self.assertIn("commit_history", collections[0]) self.assertTrue(isinstance(collections[0]["collection_id"], int)) self.assertTrue(isinstance(collections[0]["next_free_id"], int)) @@ -135,6 +136,7 @@ async def test_collections_info(self, client): self.assertTrue(isinstance(collections[0]["created_at"], int)) self.assertTrue(isinstance(collections[0]["time_zone"], str)) self.assertTrue(isinstance(collections[0]["default_deep"], int)) + self.assertEqual(collections[0]["commit_history"], "disabled") # at least one info should be checked for a correct created_at info self.assertGreater(collections[0]['created_at'], now - 60) diff --git a/itest/test_type.py b/itest/test_type.py index 5ae4116d..3042a010 100755 --- a/itest/test_type.py +++ b/itest/test_type.py @@ -1,19 +1,14 @@ #!/usr/bin/env python import asyncio -import pickle import time from lib import run_test from lib import default_test_setup from lib.testbase import TestBase from lib.client import get_client -from thingsdb.exceptions import AssertionError from thingsdb.exceptions import ValueError from thingsdb.exceptions import TypeError from thingsdb.exceptions import NumArgumentsError -from thingsdb.exceptions import BadDataError from thingsdb.exceptions import LookupError -from thingsdb.exceptions import OverflowError -from thingsdb.exceptions import ZeroDivisionError from thingsdb.exceptions import OperationError @@ -2283,6 +2278,58 @@ async def test_tel(self, client): E{}.tel = 'abc'; """) + async def test_to_type_no_store(self, client0): + # this test is for bug #424 + q0 = client0.query + await q0("""//ti- + set_type('T', {name: 'str<::default>'}); + t = {}; + t.to_type('T'); + t; + .o = {}; + .x = 42; + """) + + client1 = await get_client(self.node1) + client1.set_default_scope('//stuff') + + await self.wait_nodes_ready(client0) + q1 = client1.query + + for q in (q0, q1): + r = await q("""//ti + .x; + """) + self.assertEqual(r, 42) + + r = await q("""//ti + nse(); + t = {}; + t.to_type('T'); + change_id(); + """) + self.assertIs(r, None) + + with self.assertRaisesRegex( + OperationError, + r'operation on a stored thing; remove `nse\(...\)` to ' + r'enforce a change'): + await q0("""//ti + nse(); + .o.to_type('T'); + """) + + await q0("""//ti + .o.to_type('T'); + """) + + for q in (q0, q1): + r = await q("""//ti + wse(); + .o.name; + """) + self.assertEqual(r, "default") + if __name__ == '__main__': run_test(TestType()) diff --git a/src/ti.c b/src/ti.c index c52739d2..f3824f0d 100644 --- a/src/ti.c +++ b/src/ti.c @@ -89,6 +89,7 @@ int ti_create(void) ti.node = NULL; ti.store = NULL; ti.tasks = NULL; + ti.commits = NULL; ti.access_node = vec_new(0); ti.access_thingsdb = vec_new(0); ti.procedures = smap_create(); @@ -164,6 +165,7 @@ void ti_destroy(void) vec_destroy(ti.access_node, (vec_destroy_cb) ti_auth_destroy); vec_destroy(ti.access_thingsdb, (vec_destroy_cb) ti_auth_destroy); + vec_destroy(ti.commits, (vec_destroy_cb) ti_commit_destroy); smap_destroy(ti.procedures, (smap_destroy_cb) ti_procedure_destroy); /* remove late since counters can be updated */ @@ -955,7 +957,7 @@ int ti_this_node_to_pk(msgpack_packer * pk) const char * architecture = osarch_get_arch(); return ( - msgpack_pack_map(pk, 41) || + msgpack_pack_map(pk, 42) || /* 1 */ mp_pack_str(pk, "node_id") || msgpack_pack_uint32(pk, ti.node->id) || @@ -1082,9 +1084,15 @@ int ti_this_node_to_pk(msgpack_packer * pk) /* 40 */ mp_pack_str(pk, "next_free_id") || msgpack_pack_uint64(pk, ti.node->next_free_id) || - /* 4 */ + /* 41 */ mp_pack_str(pk, "libwebsockets_version") || - mp_pack_str(pk, lws_get_library_version()) + mp_pack_str(pk, lws_get_library_version()) || + /* 42 */ + mp_pack_str(pk, "commit_history") || + (ti.commits + ? msgpack_pack_uint32(pk, ti.commits->n) + : mp_pack_str(pk, "disabled") + ) ); } diff --git a/src/ti/collection.c b/src/ti/collection.c index 0dd19b04..491f4814 100644 --- a/src/ti/collection.c +++ b/src/ti/collection.c @@ -69,6 +69,7 @@ ti_collection_t * ti_collection_create( collection->futures = vec_new(4); collection->vtasks = vec_new(4); collection->named_rooms = smap_create(); + collection->commits = NULL; memcpy(&collection->guid, guid, sizeof(guid_t)); @@ -100,6 +101,7 @@ void ti_collection_destroy(ti_collection_t * collection) ti_val_drop((ti_val_t *) collection->name); vec_destroy(collection->access, (vec_destroy_cb) ti_auth_destroy); vec_destroy(collection->vtasks, (vec_destroy_cb) ti_vtask_drop); + vec_destroy(collection->commits, (vec_destroy_cb) ti_commit_destroy); smap_destroy(collection->procedures, (smap_destroy_cb) ti_procedure_destroy); smap_destroy(collection->named_rooms, NULL); ti_types_destroy(collection->types); @@ -127,6 +129,39 @@ void ti_collection_drop(ti_collection_t * collection) log_critical(EX_MEMORY_S); } +int ti_collection_to_pk(ti_collection_t * collection, msgpack_packer * pk) +{ + return -( + msgpack_pack_map(pk, 8) || + + mp_pack_str(pk, "collection_id") || + msgpack_pack_uint64(pk, collection->id) || + + mp_pack_str(pk, "name") || + mp_pack_strn(pk, collection->name->data, collection->name->n) || + + mp_pack_str(pk, "created_at") || + msgpack_pack_uint64(pk, collection->created_at) || + + mp_pack_str(pk, "things") || + msgpack_pack_uint64(pk, collection->things->n + collection->gc->n) || + + mp_pack_str(pk, "time_zone") || + mp_pack_strn(pk, collection->tz->name, collection->tz->n) || + + mp_pack_str(pk, "default_deep") || + msgpack_pack_uint64(pk, collection->deep) || + + mp_pack_str(pk, "next_free_id") || + msgpack_pack_uint64(pk, collection->next_free_id) || + + mp_pack_str(pk, "commit_history") || + (collection->commits + ? msgpack_pack_uint32(pk, collection->commits->n) + : mp_pack_str(pk, "disabled")) + ); +} + _Bool ti_collection_name_check(const char * name, size_t n, ex_t * e) { if (n < ti_collection_min_name || n >= ti_collection_max_name) diff --git a/src/ti/commit.c b/src/ti/commit.c new file mode 100644 index 00000000..13f700cb --- /dev/null +++ b/src/ti/commit.c @@ -0,0 +1,191 @@ +/* + * ti/commit.c + */ +#include +#include +#include +#include +#include +#include + +static ti_commit_t * commit_create( + uint64_t id, + time_t ts, + const char * code, + size_t code_n, + const char * message, + size_t message_n, + const char * by, + size_t by_n, + const char * err_msg, + size_t err_msg_n) +{ + ti_commit_t * commit = malloc(sizeof(ti_commit_t)); + if (!commit) + return NULL; + + commit->id = id; + commit->ts = ts; + + commit->code = ti_str_create(code, code_n); + if (!commit->code) + goto fail0; + + commit->message = ti_str_create(message, message_n); + if (!commit->message) + goto fail1; + + commit->by = ti_str_create(by, by_n); + if (!commit->by) + goto fail2; + + if (err_msg) + { + commit->err_msg = ti_str_create(err_msg, err_msg_n); + if (!commit->err_msg) + goto fail3; + } + else + commit->err_msg = NULL; + + return commit; +fail3: + ti_val_unsafe_drop((ti_val_t *) commit->by); +fail2: + ti_val_unsafe_drop((ti_val_t *) commit->message); +fail1: + ti_val_unsafe_drop((ti_val_t *) commit->code); +fail0: + free(commit); + return NULL; +} + +ti_commit_t * ti_commit_from_up(mp_unp_t * up) +{ + mp_obj_t obj, mp_id, mp_ts, mp_code, mp_by, mp_message, mp_err_msg; + + if (mp_next(up, &obj) != MP_ARR || obj.via.sz != 6 || + mp_next(up, &mp_id) != MP_U64 || + mp_next(up, &mp_ts) != MP_U64 || + mp_next(up, &mp_code) != MP_STR || + mp_next(up, &mp_message) != MP_STR || + mp_next(up, &mp_by) != MP_STR || + (mp_next(up, &mp_err_msg) != MP_STR && mp_err_msg.tp != MP_NIL)) + return NULL; + return commit_create( + mp_id.via.u64, + (time_t) mp_ts.via.u64, + mp_code.via.str.data, mp_code.via.str.n, + mp_message.via.str.data, mp_message.via.str.n, + mp_by.via.str.data, mp_by.via.str.n, + mp_err_msg.tp == MP_STR ? mp_err_msg.via.str.data : NULL, + mp_err_msg.tp == MP_STR ? mp_err_msg.via.str.n : 0 + ); +} + +ti_commit_t * ti_commit_make( + uint64_t id, + const char * code, + ti_raw_t * by, + ti_raw_t * message) +{ + ti_commit_t * commit = malloc(sizeof(ti_commit_t)); + if (!commit) + return NULL; + + commit->id = id; + commit->ts = (time_t) util_now_usec(); + + commit->code = ti_str_from_str(code); + if (!commit->code) + { + free(commit); + return NULL; + } + + commit->by = ti_grab(by); + commit->message = ti_grab(message); + commit->err_msg = NULL; + return commit; +} + +void ti_commit_destroy(ti_commit_t * commit) +{ + if (!commit) + return; + ti_val_unsafe_drop((ti_val_t *) commit->code); + ti_val_unsafe_drop((ti_val_t *) commit->message); + ti_val_unsafe_drop((ti_val_t *) commit->by); + ti_val_drop((ti_val_t *) commit->err_msg); + free(commit); +} + +int ti_commit_to_pk(ti_commit_t * commit, msgpack_packer * pk) +{ + return -( + msgpack_pack_array(pk, 6) || + msgpack_pack_uint64(pk, commit->id) || + msgpack_pack_uint64(pk, (uint64_t) commit->ts) || + mp_pack_strn(pk, commit->code->data, commit->code->n) || + mp_pack_strn(pk, commit->message->data, commit->message->n) || + mp_pack_strn(pk, commit->by->data, commit->by->n) || ( + commit->err_msg + ? mp_pack_strn(pk, commit->err_msg->data, commit->err_msg->n) + : msgpack_pack_nil(pk) + ) + ); +} + +int ti_commit_to_client_pk( + ti_commit_t * commit, + _Bool detail, + msgpack_packer * pk) +{ + const char * isotimestr = ti_datetime_ts_str((const time_t *) &commit->ts); + return -( + msgpack_pack_map(pk, 4 + !!detail + !!commit->err_msg) || + + mp_pack_str(pk, "id") || + msgpack_pack_uint64(pk, commit->id) || + + mp_pack_str(pk, "created_on") || + mp_pack_str(pk, isotimestr) || + + mp_pack_str(pk, "by") || + mp_pack_strn(pk, commit->by->data, commit->by->n) || + + mp_pack_str(pk, "message") || + mp_pack_strn(pk, commit->message->data, commit->message->n) || + + (detail && ( + mp_pack_str(pk, "code") || + mp_pack_strn(pk, commit->code->data, commit->code->n) + )) || + + (commit->err_msg && ( + mp_pack_str(pk, "err_msg") || + mp_pack_strn(pk, commit->err_msg->data, commit->err_msg->n) + )) + ); +} + +ti_val_t * ti_commit_as_mpval(ti_commit_t * commit, _Bool detail) +{ + ti_raw_t * raw; + msgpack_packer pk; + msgpack_sbuffer buffer; + + mp_sbuffer_alloc_init(&buffer, sizeof(ti_raw_t), sizeof(ti_raw_t)); + msgpack_packer_init(&pk, &buffer, msgpack_sbuffer_write); + + if (ti_commit_to_client_pk(commit, detail, &pk)) + { + msgpack_sbuffer_destroy(&buffer); + return NULL; + } + + raw = (ti_raw_t *) buffer.data; + ti_raw_init(raw, TI_VAL_MPDATA, buffer.size); + + return (ti_val_t *) raw; +} diff --git a/src/ti/commits.c b/src/ti/commits.c new file mode 100644 index 00000000..4c9a45ae --- /dev/null +++ b/src/ti/commits.c @@ -0,0 +1,482 @@ +/* + * ti/commits.c + */ +#include +#include +#include +#include + +typedef struct +{ + ti_commits_options_t * options; + ex_t * e; + _Bool allow_detail; +} commits__options_t; + +static int commits__options( + ti_raw_t * key, + ti_val_t * val, + commits__options_t * w) +{ + if (ti_raw_eq_strn(key, "scope", strlen("scope"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_str(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `scope` to be of type `"TI_VAL_STR_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + w->options->scope = (ti_raw_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "contains", strlen("contains"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_str(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `contains` to be of type `"TI_VAL_STR_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + if (((ti_raw_t *) val)->n == 0) + { + ex_set(w->e, EX_VALUE_ERROR, + "option `contains` must be a non empty string"); + return w->e->nr; + } + w->options->contains = (ti_raw_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "match", strlen("match"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_regex(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `match` to be of type `"TI_VAL_REGEX_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + w->options->match = (ti_regex_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "id", strlen("id"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_int(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `id` to be of type `"TI_VAL_INT_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + if (VINT(val) < 0) + { + ex_set(w->e, EX_VALUE_ERROR, + "option `id` expects an " + "integer value greater than or equal to 0"); + return w->e->nr; + } + w->options->id = (ti_vint_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "last", strlen("last"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_int(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `last` to be of type `"TI_VAL_INT_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + if (VINT(val) < 0) + { + ex_set(w->e, EX_VALUE_ERROR, + "option `last` expects an " + "integer value greater than or equal to 0"); + return w->e->nr; + } + w->options->last = (ti_vint_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "first", strlen("first"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_int(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `first` to be of type `"TI_VAL_INT_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + if (VINT(val) < 0) + { + ex_set(w->e, EX_VALUE_ERROR, + "option `first` expects an " + "integer value greater than or equal to 0"); + return w->e->nr; + } + w->options->first = (ti_vint_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "before", strlen("before"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_datetime(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `before` to be of " + "type `"TI_VAL_DATETIME_S"` or `"TI_VAL_TIMEVAL_S"` " + "but got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + w->options->before = (ti_datetime_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "after", strlen("after"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_datetime(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `after` to be of " + "type `"TI_VAL_DATETIME_S"` or `"TI_VAL_TIMEVAL_S"` " + "but got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + w->options->after = (ti_datetime_t *) val; + return 0; + } + if (ti_raw_eq_strn(key, "has_err", strlen("has_err"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_bool(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `has_err` to be of type `"TI_VAL_BOOL_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + w->options->has_err = (ti_vbool_t *) val; + return 0; + } + if (w->allow_detail && ti_raw_eq_strn(key, "detail", strlen("detail"))) + { + if (ti_val_is_nil(val)) + return 0; + if (!ti_val_is_bool(val)) + { + ex_set(w->e, EX_TYPE_ERROR, + "expecting `detail` to be of type `"TI_VAL_BOOL_S"` but " + "got type `%s` instead", + ti_val_str(val)); + return w->e->nr; + } + w->options->detail = (ti_vbool_t *) val; + return 0; + } + ex_set(w->e, EX_VALUE_ERROR, + "invalid option `%.*s` was provided; valid options are: `scope`, " + "`contains`, `match`, `id`, `first`, `last`, `before`, " + "`after`, `has_err`%s", + key->n, key->data, + w->allow_detail ? ", `detail`" : ""); + return w->e->nr; +} + +int ti_commits_options( + ti_commits_options_t * options, + ti_thing_t * thing, + _Bool allow_detail, + ex_t * e) +{ + commits__options_t w = { + .options=options, + .e=e, + .allow_detail=allow_detail, + }; + + if (ti_thing_walk(thing, (ti_thing_item_cb) commits__options, &w)) + return e->nr; /* error is set */ + + if (!options->contains && + !options->match && + !options->id && + !options->first && + !options->last && + !options->before && + !options->after && + !options->has_err) + ex_set(e, EX_LOOKUP_ERROR, + "at least one of the following options must be set: " + "`contains`, `match`, `id`, `first`, `last`, `before`, `after`" + ", `has_err`"DOC_HISTORY""DOC_DEL_HISTORY); + return e->nr; +} + +int ti_commits_history( + ti_commits_history_t * history, + ti_commits_options_t * options, + ex_t * e) + { + history->commits = &ti.commits; + history->access = &ti.access_thingsdb; + history->scope_id = 0; + + if (options->scope) + { + ti_scope_t scope; + if (ti_scope_init( + &scope, + (const char *) options->scope->data, + options->scope->n, + e)) + return e->nr; + + switch(scope.tp) + { + case TI_SCOPE_THINGSDB: + history->commits = &ti.commits; + history->access = &ti.access_thingsdb; + break; + case TI_SCOPE_NODE: + ex_set(e, EX_OPERATION, + "commit history is not supported by `@node` scopes"); + return e->nr; + case TI_SCOPE_COLLECTION: + { + ti_collection_t * collection = ti_collections_get_by_strn( + scope.via.collection_name.name, + scope.via.collection_name.sz); + if (!collection) + { + ex_set(e, EX_LOOKUP_ERROR, + "collection `%.*s` not found", + scope.via.collection_name.sz, + scope.via.collection_name.name); + return e->nr; + } + history->commits = &collection->commits; + history->access = &collection->access; + history->scope_id = collection->id; + break; + } + } + if (!(*history->commits)) + ex_set(e, EX_OPERATION, + "commit history is not enabled for the `%.*s` scope", + options->scope->n, + options->scope->data); + return e->nr; + } + + for (vec_each(ti.collections->vec, ti_collection_t, collection)) + { + if (collection->commits) + { + if (*history->commits) + { + ex_set(e, EX_LOOKUP_ERROR, + "more than one scope has commit history enabled; you must " + "use the `scope` option to tell ThingsDB which scope " + "to use"DOC_HISTORY""DOC_DEL_HISTORY); + return e->nr; + } + history->commits = &collection->commits; + history->access = &collection->access; + history->scope_id = collection->id; + } + } + + if (!(*history->commits)) + ex_set(e, EX_LOOKUP_ERROR, + "no scope found with commit history enabled"DOC_SET_HISTORY); + return e->nr; +} + +vec_t ** ti_commits_from_scope(ti_raw_t * scope, ex_t * e) +{ + ti_scope_t scope_; + if (ti_scope_init(&scope_, (const char *) scope->data, scope->n, e)) + return NULL; + + switch(scope_.tp) + { + case TI_SCOPE_THINGSDB: + return &ti.commits; + case TI_SCOPE_NODE: + ex_set(e, EX_OPERATION, + "commit history is not supported by `@node` scopes"); + return NULL; + case TI_SCOPE_COLLECTION: + { + ti_collection_t * collection = ti_collections_get_by_strn( + scope_.via.collection_name.name, + scope_.via.collection_name.sz); + if (collection) + return &collection->commits; + + ex_set(e, EX_LOOKUP_ERROR, "collection `%.*s` not found", + scope_.via.collection_name.sz, + scope_.via.collection_name.name); + return NULL; + } + } + ex_set_internal(e); + return NULL; +} + +void ti_commits_destroy(vec_t ** commits) +{ + vec_destroy(*commits, (vec_destroy_cb) ti_commit_destroy); + *commits = NULL; +} +int ti_commits_set_history(vec_t ** commits, _Bool state) +{ + if (state) + { + if (*commits) + return 0; + + *commits = vec_new(8); + return -!(*commits); + } + ti_commits_destroy(commits); + return 0; +} + +static _Bool commits__match( + ti_commit_t * commit, + ti_commits_options_t * options) +{ + if (options->contains && ( + !ti_raw_contains(commit->message, options->contains) && + !ti_raw_contains(commit->code, options->contains) && + !ti_raw_contains(commit->by, options->contains) && ( + !commit->err_msg || + !ti_raw_contains(commit->err_msg, options->contains) + ) + )) + return false; + if (options->match && ( + !ti_regex_test(options->match, commit->message) && + !ti_regex_test(options->match, commit->code) && + !ti_regex_test(options->match, commit->by) && ( + !commit->err_msg || + !ti_regex_test(options->match, commit->err_msg) + ) + )) + return false; + + if (options->id && commit->id != (uint64_t) VINT(options->id)) + return false; + + if (options->has_err && !!commit->err_msg != options->has_err->bool_) + return false; + + return true; +} + +static void commits__init( + vec_t * commits, + ti_commits_options_t * options, + uint32_t * begin, + uint32_t * end) +{ + uint32_t n; + *begin = 0; + *end = commits->n; + + if (options->before) + { + uint32_t i = 0; + for (vec_each(commits, ti_commit_t, commit), ++i) + if (commit->ts >= options->before->ts) + break; + *end = i; + } + if (options->after) + { + uint32_t i = commits->n; + for (vec_each_rev(commits, ti_commit_t, commit) --i) + if (commit->ts <= options->after->ts) + break; + *begin = i; + } + + n = *end > *begin ? *end - *begin : 0; + + if (options->last && n > options->last->int_) + *begin = *end - options->last->int_; + + if (options->first && n > options->first->int_) + *end = *begin + options->first->int_; +} + +vec_t * ti_commits_find(vec_t * commits, ti_commits_options_t * options) +{ + uint32_t begin, end;; + vec_t * vec = vec_new(8); + if (!vec) + return NULL; + + commits__init(commits, options, &begin, &end); + for (;begin < end; ++begin) + { + ti_commit_t * commit = VEC_get(commits, begin); + if (commits__match(commit, options) && vec_push(&vec, commit)) + goto fail; + } + + return vec; +fail: + vec_destroy(vec, NULL); + return NULL; +} + +vec_t * ti_commits_del(vec_t ** commits, ti_commits_options_t * options) +{ + uint32_t begin, end; + vec_t * vec = vec_new(8); + if (!vec) + return NULL; + + commits__init(*commits, options, &begin, &end); + while (begin < end) + { + ti_commit_t * commit = VEC_get(*commits, --end); + if (commits__match(commit, options) && + vec_push(&vec, vec_remove(*commits, end))) + goto fail; + } + (void) vec_shrink(commits); + return vec; +fail: + /* revert */ + for (vec_each(vec, ti_commit_t, commit)) + VEC_push(*commits, commit); + vec_destroy(vec, NULL); + return NULL; +} diff --git a/src/ti/ctask.c b/src/ti/ctask.c index 2a025b68..573d9a99 100644 --- a/src/ti/ctask.c +++ b/src/ti/ctask.c @@ -3280,6 +3280,42 @@ int ctask__arr_remove(ti_thing_t * thing, mp_unp_t * up) return 0; } +/* + * Returns 0 on success + * - for example: '{name: closure}' + */ +static int ctask__commit(ti_thing_t * thing, mp_unp_t * up) +{ + vec_t ** commits = &thing->collection->commits; + ti_commit_t * commit = ti_commit_from_up(up); + if (!commit) + { + log_critical( + "failed to unpack commit from task `commit_add` " + "for the @collection:%.*s scope", + thing->collection->name->n, thing->collection->name->data); + return -1; + } + if (!(*commits)) + { + log_error( + "commits not enabled for the @collection:%.*s scope", + thing->collection->name->n, thing->collection->name->data); + goto fail; + } + if (vec_push(commits, commit)) + { + log_error( + "failed to add commit for the @collection:%.*s scope", + thing->collection->name->n, thing->collection->name->data); + goto fail; + } + return 0; +fail: + ti_commit_destroy(commit); + return -1; +} + /* * Unpacker should be at point 'task': ... */ @@ -3384,6 +3420,9 @@ int ti_ctask_run(ti_thing_t * thing, mp_unp_t * up) case TI_TASK_ROOM_SET_NAME: return ctask__room_set_name(thing, up); case TI_TASK_WHITELIST_ADD: break; case TI_TASK_WHITELIST_DEL: break; + case TI_TASK_SET_HISTORY: break; + case TI_TASK_DEL_HISTORY: break; + case TI_TASK_COMMIT: return ctask__commit(thing, up); } log_critical("unknown collection task: %"PRIu64, mp_task.via.u64); diff --git a/src/ti/export.c b/src/ti/export.c index ba3e4c8e..d898d120 100644 --- a/src/ti/export.c +++ b/src/ti/export.c @@ -646,9 +646,19 @@ static int export__write_to_type(ti_fmt_t * fmt, ti_collection_t * collection) : 0; } +static int export__commit(ti_fmt_t * fmt, ti_collection_t * collection) +{ + return buf_append_fmt( + &fmt->buf, + "try(commit('Source: collection `%.*s`'));\n\n", + collection->name->n, collection->name->data); +} + static int export__collection(ti_fmt_t * fmt, ti_collection_t * collection) { + return -( + export__commit(fmt, collection) || export__write_new_types(fmt, collection->types) || export__write_enums(fmt, collection->enums) || export__write_types(fmt, collection->types) || diff --git a/src/ti/names.c b/src/ti/names.c index 38ba889c..48bf9cae 100644 --- a/src/ti/names.c +++ b/src/ti/names.c @@ -45,89 +45,89 @@ void ti_names_destroy(void) */ void ti_names_inject_common(void) { - (void) ti_names_from_str("_"); - (void) ti_names_from_str("a"); - (void) ti_names_from_str("b"); - (void) ti_names_from_str("c"); - (void) ti_names_from_str("x"); - (void) ti_names_from_str("y"); - (void) ti_names_from_str("z"); - (void) ti_names_from_str("tmp"); + (void) ti_names_from_str_slow("_"); + (void) ti_names_from_str_slow("a"); + (void) ti_names_from_str_slow("b"); + (void) ti_names_from_str_slow("c"); + (void) ti_names_from_str_slow("x"); + (void) ti_names_from_str_slow("y"); + (void) ti_names_from_str_slow("z"); + (void) ti_names_from_str_slow("tmp"); #ifdef TI_PRELOAD_NAMES - (void) ti_names_from_str("access"); - (void) ti_names_from_str("archive_files"); - (void) ti_names_from_str("archived_in_memory"); - (void) ti_names_from_str("arguments"); - (void) ti_names_from_str("cache_expiration_time"); - (void) ti_names_from_str("cached_names"); - (void) ti_names_from_str("cached_queries"); - (void) ti_names_from_str("client_port"); - (void) ti_names_from_str("collection_id"); - (void) ti_names_from_str("conf"); - (void) ti_names_from_str("connected_clients"); - (void) ti_names_from_str("created_at"); - (void) ti_names_from_str("created_on"); - (void) ti_names_from_str("db_stored_change_id"); - (void) ti_names_from_str("default"); - (void) ti_names_from_str("definition"); - (void) ti_names_from_str("doc"); - (void) ti_names_from_str("enum_id"); - (void) ti_names_from_str("events_in_queue"); - (void) ti_names_from_str("expiration_time"); - (void) ti_names_from_str("fields"); - (void) ti_names_from_str("file_template"); - (void) ti_names_from_str("file"); - (void) ti_names_from_str("files"); - (void) ti_names_from_str("global_committed_change_id"); - (void) ti_names_from_str("global_stored_change_id"); - (void) ti_names_from_str("has_password"); - (void) ti_names_from_str("http_api_port"); - (void) ti_names_from_str("http_status_port"); - (void) ti_names_from_str("id"); - (void) ti_names_from_str("ip_support"); - (void) ti_names_from_str("key"); - (void) ti_names_from_str("libcleri_version"); - (void) ti_names_from_str("libpcre2_version"); - (void) ti_names_from_str("libuv_version"); - (void) ti_names_from_str("local_committed_change_id"); - (void) ti_names_from_str("local_stored_change_id"); - (void) ti_names_from_str("log_level"); - (void) ti_names_from_str("max_files"); - (void) ti_names_from_str("members"); - (void) ti_names_from_str("methods"); - (void) ti_names_from_str("modified_at"); - (void) ti_names_from_str("msgpack_version"); - (void) ti_names_from_str("name"); - (void) ti_names_from_str("next_free_id"); - (void) ti_names_from_str("next_run"); - (void) ti_names_from_str("next_thing_id"); - (void) ti_names_from_str("node_id"); - (void) ti_names_from_str("node_name"); - (void) ti_names_from_str("node_port"); - (void) ti_names_from_str("privileges"); - (void) ti_names_from_str("relations"); - (void) ti_names_from_str("repeat"); - (void) ti_names_from_str("restarts"); - (void) ti_names_from_str("result_code"); - (void) ti_names_from_str("result_message"); - (void) ti_names_from_str("result_size_limit"); - (void) ti_names_from_str("scheduled_backups"); - (void) ti_names_from_str("scope"); - (void) ti_names_from_str("status"); - (void) ti_names_from_str("storage_path"); - (void) ti_names_from_str("syntax_version"); - (void) ti_names_from_str("tasks"); - (void) ti_names_from_str("threshold_query_cache"); - (void) ti_names_from_str("tokens"); - (void) ti_names_from_str("type_id"); - (void) ti_names_from_str("uptime"); - (void) ti_names_from_str("user_id"); - (void) ti_names_from_str("user"); - (void) ti_names_from_str("version"); - (void) ti_names_from_str("with_side_effects"); - (void) ti_names_from_str("wrap_only"); - (void) ti_names_from_str("yajl_version"); - (void) ti_names_from_str("zone"); + (void) ti_names_from_str_slow("access"); + (void) ti_names_from_str_slow("archive_files"); + (void) ti_names_from_str_slow("archived_in_memory"); + (void) ti_names_from_str_slow("arguments"); + (void) ti_names_from_str_slow("cache_expiration_time"); + (void) ti_names_from_str_slow("cached_names"); + (void) ti_names_from_str_slow("cached_queries"); + (void) ti_names_from_str_slow("client_port"); + (void) ti_names_from_str_slow("collection_id"); + (void) ti_names_from_str_slow("conf"); + (void) ti_names_from_str_slow("connected_clients"); + (void) ti_names_from_str_slow("created_at"); + (void) ti_names_from_str_slow("created_on"); + (void) ti_names_from_str_slow("db_stored_change_id"); + (void) ti_names_from_str_slow("default"); + (void) ti_names_from_str_slow("definition"); + (void) ti_names_from_str_slow("doc"); + (void) ti_names_from_str_slow("enum_id"); + (void) ti_names_from_str_slow("events_in_queue"); + (void) ti_names_from_str_slow("expiration_time"); + (void) ti_names_from_str_slow("fields"); + (void) ti_names_from_str_slow("file_template"); + (void) ti_names_from_str_slow("file"); + (void) ti_names_from_str_slow("files"); + (void) ti_names_from_str_slow("global_committed_change_id"); + (void) ti_names_from_str_slow("global_stored_change_id"); + (void) ti_names_from_str_slow("has_password"); + (void) ti_names_from_str_slow("http_api_port"); + (void) ti_names_from_str_slow("http_status_port"); + (void) ti_names_from_str_slow("id"); + (void) ti_names_from_str_slow("ip_support"); + (void) ti_names_from_str_slow("key"); + (void) ti_names_from_str_slow("libcleri_version"); + (void) ti_names_from_str_slow("libpcre2_version"); + (void) ti_names_from_str_slow("libuv_version"); + (void) ti_names_from_str_slow("local_committed_change_id"); + (void) ti_names_from_str_slow("local_stored_change_id"); + (void) ti_names_from_str_slow("log_level"); + (void) ti_names_from_str_slow("max_files"); + (void) ti_names_from_str_slow("members"); + (void) ti_names_from_str_slow("methods"); + (void) ti_names_from_str_slow("modified_at"); + (void) ti_names_from_str_slow("msgpack_version"); + (void) ti_names_from_str_slow("name"); + (void) ti_names_from_str_slow("next_free_id"); + (void) ti_names_from_str_slow("next_run"); + (void) ti_names_from_str_slow("next_thing_id"); + (void) ti_names_from_str_slow("node_id"); + (void) ti_names_from_str_slow("node_name"); + (void) ti_names_from_str_slow("node_port"); + (void) ti_names_from_str_slow("privileges"); + (void) ti_names_from_str_slow("relations"); + (void) ti_names_from_str_slow("repeat"); + (void) ti_names_from_str_slow("restarts"); + (void) ti_names_from_str_slow("result_code"); + (void) ti_names_from_str_slow("result_message"); + (void) ti_names_from_str_slow("result_size_limit"); + (void) ti_names_from_str_slow("scheduled_backups"); + (void) ti_names_from_str_slow("scope"); + (void) ti_names_from_str_slow("status"); + (void) ti_names_from_str_slow("storage_path"); + (void) ti_names_from_str_slow("syntax_version"); + (void) ti_names_from_str_slow("tasks"); + (void) ti_names_from_str_slow("threshold_query_cache"); + (void) ti_names_from_str_slow("tokens"); + (void) ti_names_from_str_slow("type_id"); + (void) ti_names_from_str_slow("uptime"); + (void) ti_names_from_str_slow("user_id"); + (void) ti_names_from_str_slow("user"); + (void) ti_names_from_str_slow("version"); + (void) ti_names_from_str_slow("with_side_effects"); + (void) ti_names_from_str_slow("wrap_only"); + (void) ti_names_from_str_slow("yajl_version"); + (void) ti_names_from_str_slow("zone"); #endif } @@ -145,3 +145,13 @@ ti_name_t * ti_names_new(const char * str, size_t n) return name; } +ti_name_t * ti_names_get_slow(const char * str, size_t n) +{ + ti_name_t * name = smap_getn(names, str, n); + if (name) + { + ti_incref(name); + return name; + } + return ti_names_new(str, n); +} diff --git a/src/ti/qbind.c b/src/ti/qbind.c index c146c494..271604f2 100644 --- a/src/ti/qbind.c +++ b/src/ti/qbind.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -97,6 +99,7 @@ #include #include #include +#include #include #include #include @@ -215,6 +218,7 @@ #include #include #include +#include #include #include #include @@ -302,11 +306,11 @@ static void qbind__statement(ti_qbind_t * qbind, cleri_node_t * nd); */ enum { - TOTAL_KEYWORDS = 277, + TOTAL_KEYWORDS = 281, MIN_WORD_LENGTH = 2, MAX_WORD_LENGTH = 17, - MIN_HASH_VALUE = 12, - MAX_HASH_VALUE = 751 + MIN_HASH_VALUE = 38, + MAX_HASH_VALUE = 796 }; /* @@ -318,32 +322,32 @@ static inline unsigned int qbind__hash( { static unsigned short asso_values[] = { - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 4, 3, - 6, 752, 3, 752, 3, 752, 4, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 2, 752, 19, 120, 85, - 23, 4, 136, 275, 180, 2, 6, 71, 24, 59, - 11, 41, 110, 44, 5, 2, 3, 28, 281, 191, - 227, 252, 25, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752, 752, 752, 752, 752, - 752, 752, 752, 752, 752, 752 + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 16, 15, + 18, 797, 19, 797, 18, 797, 16, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 15, 797, 20, 58, 82, + 24, 17, 152, 371, 112, 15, 18, 73, 17, 25, + 20, 22, 131, 15, 16, 15, 16, 52, 174, 300, + 204, 140, 29, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797, 797, 797, 797, 797, + 797, 797, 797, 797, 797, 797 }; register unsigned int hval = n; @@ -517,6 +521,7 @@ qbind__fmap_t qbind__fn_mapping[TOTAL_KEYWORDS] = { {.name="code", .fn=do__f_code, CHAIN_NE}, {.name="collection_info", .fn=do__f_collection_info, ROOT_NE}, {.name="collections_info", .fn=do__f_collections_info, ROOT_NE}, + {.name="commit", .fn=do__f_commit, ROOT_NE}, {.name="contains", .fn=do__f_contains, CHAIN_NE}, {.name="copy", .fn=do__f_copy, CHAIN_NE}, {.name="cos", .fn=do__f_cos, ROOT_NE}, @@ -529,6 +534,7 @@ qbind__fmap_t qbind__fn_mapping[TOTAL_KEYWORDS] = { {.name="del_collection", .fn=do__f_del_collection, ROOT_TE}, {.name="del_enum", .fn=do__f_del_enum, ROOT_CE}, {.name="del_expired", .fn=do__f_del_expired, ROOT_TE}, + {.name="del_history", .fn=do__f_del_history, ROOT_TE}, {.name="del_module", .fn=do__f_del_module, ROOT_TE}, {.name="del_node", .fn=do__f_del_node, ROOT_TE}, {.name="del_procedure", .fn=do__f_del_procedure, ROOT_BE}, @@ -577,6 +583,7 @@ qbind__fmap_t qbind__fn_mapping[TOTAL_KEYWORDS] = { {.name="has_token", .fn=do__f_has_token, ROOT_NE}, {.name="has_type", .fn=do__f_has_type, ROOT_NE}, {.name="has_user", .fn=do__f_has_user, ROOT_NE}, + {.name="history", .fn=do__f_history, ROOT_NE}, {.name="id", .fn=do__f_id, CHAIN_NE}, {.name="import", .fn=do__f_import, ROOT_CE}, {.name="index_of", .fn=do__f_index_of, CHAIN_NE}, @@ -701,6 +708,7 @@ qbind__fmap_t qbind__fn_mapping[TOTAL_KEYWORDS] = { {.name="set_closure", .fn=do__f_set_closure, CHAIN_BE}, {.name="set_default_deep", .fn=do__f_set_default_deep, ROOT_TE}, {.name="set_enum", .fn=do__f_set_enum, ROOT_CE}, + {.name="set_history", .fn=do__f_set_history, ROOT_TE}, {.name="set_log_level", .fn=do__f_set_log_level, ROOT_NE}, {.name="set_module_conf", .fn=do__f_set_module_conf, ROOT_TE}, {.name="set_module_scope", .fn=do__f_set_module_scope, ROOT_TE}, @@ -730,9 +738,9 @@ qbind__fmap_t qbind__fn_mapping[TOTAL_KEYWORDS] = { {.name="time_zones_info", .fn=do__f_time_zones_info, ROOT_NE}, {.name="timeit", .fn=do__f_timeit, ROOT_NE}, {.name="timeval", .fn=do__f_timeval, ROOT_NE}, + {.name="to", .fn=do__f_to, CHAIN_NE}, {.name="to_thing", .fn=do__f_to_thing, CHAIN_CE_X}, {.name="to_type", .fn=do__f_to_type, CHAIN_CE_X}, - {.name="to", .fn=do__f_to, CHAIN_NE}, {.name="trim", .fn=do__f_trim, CHAIN_NE}, {.name="trim_left", .fn=do__f_trim_left, CHAIN_NE}, {.name="trim_right", .fn=do__f_trim_right, CHAIN_NE}, @@ -750,8 +758,8 @@ qbind__fmap_t qbind__fn_mapping[TOTAL_KEYWORDS] = { {.name="upper", .fn=do__f_upper, CHAIN_NE}, {.name="user_info", .fn=do__f_user_info, ROOT_NE}, {.name="users_info", .fn=do__f_users_info, ROOT_NE}, - {.name="value_err", .fn=do__f_value_err, ROOT_NE}, {.name="value", .fn=do__f_value, CHAIN_NE}, + {.name="value_err", .fn=do__f_value_err, ROOT_NE}, {.name="values", .fn=do__f_values, CHAIN_NE}, {.name="vmap", .fn=do__f_vmap, CHAIN_NE}, {.name="week", .fn=do__f_week, CHAIN_NE}, diff --git a/src/ti/query.c b/src/ti/query.c index 62233935..ea8bc781 100644 --- a/src/ti/query.c +++ b/src/ti/query.c @@ -76,7 +76,7 @@ static ti_cpkg_t * query__cpkg_change(ti_query_t * query) return NULL; msgpack_packer_init(&pk, &buffer, msgpack_sbuffer_write); - msgpack_pack_array(&pk, tasks->n+2); + msgpack_pack_array(&pk, tasks->n + 2); msgpack_pack_uint64(&pk, query->change->id); msgpack_pack_uint64(&pk, tasks->n && query->collection ? query->collection->id @@ -954,8 +954,23 @@ void ti_query_run_parseres(ti_query_t * query) stop: if (query->change) - query__change_handle(query); /* errors will be logged only */ + { + if (query->commit) + { + ti_task_t * task; + + if (e.nr) /* set error if any, not critical if failed */ + query->commit->err_msg = ti_str_create(e.msg, e.n); + task = ti_task_get_task( + query->change, + query->collection ? query->collection->root : ti.thing0); + + if (task) + (void) ti_task_add_commit_add(task, query->commit); + } + query__change_handle(query); /* errors will be logged only */ + } ti_query_done(query, &e, &ti_query_send_response); } diff --git a/src/ti/store.c b/src/ti/store.c index 52248e54..0c4057ad 100644 --- a/src/ti/store.c +++ b/src/ti/store.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ static const char * store__tmp_path = "__tmp/"; static const char * store__access_node_fn = "access_node.mp"; static const char * store__access_thingsdb_fn = "access_thingsdb.mp"; static const char * store__collections_fn = "collections.mp"; +static const char * store__commits_fn = "commits.mp"; static const char * store__id_stat_fn = "idstat.mp"; static const char * store__names_fn = "names.mp"; static const char * store__procedures_fn = "procedures.mp"; @@ -58,6 +60,7 @@ static void store__set_filename(_Bool use_tmp) memcpy(store->access_node_fn + store->fn_offset, path, n); memcpy(store->access_thingsdb_fn + store->fn_offset, path, n); memcpy(store->collections_fn + store->fn_offset, path, n); + memcpy(store->commits_fn + store->fn_offset, path, n); memcpy(store->id_stat_fn + store->fn_offset, path, n); memcpy(store->names_fn + store->fn_offset, path, n); memcpy(store->procedures_fn + store->fn_offset, path, n); @@ -112,6 +115,7 @@ int ti_store_create(void) store->collections_fn = fx_path_join( store->tmp_path, store__collections_fn); + store->commits_fn = fx_path_join(store->tmp_path, store__commits_fn); store->id_stat_fn = fx_path_join(store->tmp_path, store__id_stat_fn); store->names_fn = fx_path_join(store->tmp_path, store__names_fn); store->procedures_fn = fx_path_join(store->tmp_path, store__procedures_fn); @@ -126,6 +130,7 @@ int ti_store_create(void) !store->access_node_fn || !store->access_thingsdb_fn || !store->collections_fn || + !store->commits_fn || !store->id_stat_fn || !store->names_fn || !store->procedures_fn || @@ -170,6 +175,7 @@ void ti_store_destroy(void) free(store->access_node_fn); free(store->access_thingsdb_fn); free(store->collections_fn); + free(store->commits_fn); free(store->id_stat_fn); free(store->names_fn); free(store->procedures_fn); @@ -212,7 +218,8 @@ int ti_store_store(void) ti_store_collections_store(store->collections_fn) || ti_store_procedures_store(ti.procedures, store->procedures_fn) || ti_store_tasks_store(ti.tasks->vtasks, store->tasks_fn) || - ti_store_modules_store(store->modules_fn)) + ti_store_modules_store(store->modules_fn) || + ti_store_commits_store(ti.commits, store->commits_fn)) goto failed; for (vec_each(ti.collections->vec, ti_collection_t, collection)) @@ -267,7 +274,10 @@ int ti_store_store(void) store_collection->procedures_fn) || ti_store_tasks_store( collection->vtasks, - store_collection->tasks_fn) + store_collection->tasks_fn) || + ti_store_commits_store( + collection->commits, + store_collection->commits_fn) ); } ti_store_collection_destroy(store_collection); @@ -348,7 +358,8 @@ int ti_store_restore(void) &ti.tasks->vtasks, store->tasks_fn, NULL) || - ti_store_modules_restore(store->modules_fn)); + ti_store_modules_restore(store->modules_fn) || + ti_store_commits_restore(&ti.commits, store->commits_fn)); if (rc) goto stop; @@ -430,7 +441,10 @@ int ti_store_restore(void) ti_store_procedures_restore( collection->procedures, store_collection->procedures_fn, - collection) + collection) || + ti_store_commits_restore( + &collection->commits, + store_collection->commits_fn) ); ti_store_collection_destroy(store_collection); diff --git a/src/ti/store/storecollection.c b/src/ti/store/storecollection.c index 28fa11bd..03348970 100644 --- a/src/ti/store/storecollection.c +++ b/src/ti/store/storecollection.c @@ -9,16 +9,17 @@ #include static const char * collection___access_fn = "access.mp"; +static const char * collection___commits_fn = "commits.mp"; static const char * collection___dat_fn = "collection.dat"; +static const char * collection___enums_fn = "enums.mp"; +static const char * collection___gcprops_fn = "gcprops.mp"; +static const char * collection___gcthings_fn = "gcthings.mp"; +static const char * collection___named_rooms_fn = "named_rooms.mp"; static const char * collection___procedures_fn = "procedures.mp"; -static const char * collection___tasks_fn = "tasks.mp"; static const char * collection___props_fn = "props.mp"; -static const char * collection___named_rooms_fn = "named_rooms.mp"; +static const char * collection___tasks_fn = "tasks.mp"; static const char * collection___things_fn = "things.mp"; static const char * collection___types_fn = "types.mp"; -static const char * collection___enums_fn = "enums.mp"; -static const char * collection___gcprops_fn = "gcprops.mp"; -static const char * collection___gcthings_fn = "gcthings.mp"; ti_store_collection_t * ti_store_collection_create( const char * path, @@ -40,6 +41,7 @@ ti_store_collection_t * ti_store_collection_create( store_collection->access_fn = fx_path_join(cpath, collection___access_fn); store_collection->collection_fn = fx_path_join(cpath, collection___dat_fn); + store_collection->commits_fn = fx_path_join(cpath, collection___commits_fn); store_collection->procedures_fn = fx_path_join(cpath, collection___procedures_fn); store_collection->tasks_fn = fx_path_join(cpath, collection___tasks_fn); store_collection->props_fn = fx_path_join(cpath, collection___props_fn); @@ -52,6 +54,7 @@ ti_store_collection_t * ti_store_collection_create( if ( !store_collection->access_fn || !store_collection->collection_fn || + !store_collection->commits_fn || !store_collection->procedures_fn || !store_collection->tasks_fn || !store_collection->props_fn || @@ -79,6 +82,7 @@ void ti_store_collection_destroy(ti_store_collection_t * store_collection) free(store_collection->access_fn); free(store_collection->collection_fn); free(store_collection->collection_path); + free(store_collection->commits_fn); free(store_collection->procedures_fn); free(store_collection->tasks_fn); free(store_collection->props_fn); @@ -309,3 +313,15 @@ char * ti_store_collection_gcthings_fn( free(cpath); return fn; } + +char * ti_store_collection_commits_fn( + const char * path, + uint64_t collection_id) +{ + char * fn, * cpath = ti_store_collection_get_path(path, collection_id); + if (!cpath) + return NULL; + fn = fx_path_join(cpath, collection___commits_fn); + free(cpath); + return fn; +} \ No newline at end of file diff --git a/src/ti/store/storecommits.c b/src/ti/store/storecommits.c new file mode 100644 index 00000000..8caeac4a --- /dev/null +++ b/src/ti/store/storecommits.c @@ -0,0 +1,129 @@ +/* + * ti/store/storecommits.c + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +int ti_store_commits_store(vec_t * commits, const char * fn) +{ + msgpack_packer pk; + FILE * f = fopen(fn, "w"); + if (!f) + { + log_errno_file("cannot open file", errno, fn); + return -1; + } + + msgpack_packer_init(&pk, f, msgpack_fbuffer_write); + + if (!commits) + { + if (msgpack_pack_map(&pk, 0)) + goto fail; + } + else + { + if (msgpack_pack_map(&pk, 1) || + mp_pack_str(&pk, "commits") || + msgpack_pack_array(&pk, commits->n) + ) goto fail; + + for (vec_each(commits, ti_commit_t, commit)) + if (ti_commit_to_pk(commit, &pk)) + goto fail; + } + + log_debug("stored commits to file: `%s`", fn); + goto done; +fail: + log_error("failed to write file: `%s`", fn); +done: + if (fclose(f)) + { + log_errno_file("cannot close file", errno, fn); + return -1; + } + (void) ti_sleep(5); + return 0; +} + +int ti_store_commits_restore(vec_t ** commits, const char * fn) +{ + int rc = -1; + fx_mmap_t fmap; + size_t i; + mp_obj_t obj; + mp_unp_t up; + ti_commit_t * commit; + + /* clear existing commits (may exist in the thingsdb scope) */ + ti_commits_destroy(commits); + + if (!fx_file_exist(fn)) + { + /* + * TODO: (COMPAT) This check may be removed when we no longer require + * backwards compatibility with previous versions of ThingsDB + * where the commits file did not exist. + */ + log_warning( + "no commits found; " + "file `%s` is missing", + fn); + return ti_store_commits_store(NULL, fn); + } + + fx_mmap_init(&fmap, fn); + if (fx_mmap_open(&fmap)) /* fx_mmap_open() is a log function */ + goto fail0; + + mp_unp_init(&up, fmap.data, fmap.n); + if (mp_next(&up, &obj) != MP_MAP || obj.via.sz > 1) + goto fail1; + + if (obj.via.sz == 0) + { + rc = 0; + goto done; /* done, commits disabled */ + } + + if (mp_skip(&up) != MP_STR || /* commits */ + mp_next(&up, &obj) != MP_ARR || + ti_commits_set_history(commits, true)) + goto fail1; + + for (i = obj.via.sz; i--;) + { + commit = ti_commit_from_up(&up); + if (!commit) + goto fail1; + + if (vec_push(commits, commit)) + goto fail2; + } + + rc = 0; + goto done; + +fail2: + ti_commit_destroy(commit); + +done: +fail1: + if (fx_mmap_close(&fmap)) + rc = -1; +fail0: + if (rc) + log_critical("failed to restore from file: `%s`", fn); + + return rc; +} diff --git a/src/ti/syncfull.c b/src/ti/syncfull.c index 9eecac0d..3c4496dc 100644 --- a/src/ti/syncfull.c +++ b/src/ti/syncfull.c @@ -30,6 +30,7 @@ typedef enum SYNCFULL__COLLECTIONS_FILE, SYNCFULL__MODULES_FILE, SYNCFULL__ID_STAT_FILE, + SYNCFULL__COMMITS_FILE, /* collection files */ SYNCFULL__COLLECTION_DAT_FILE, SYNCFULL__COLLECTION_TYPES_FILE, @@ -42,6 +43,7 @@ typedef enum SYNCFULL__COLLECTION_THINGS_FILE, SYNCFULL__COLLECTION_PROPS_FILE, SYNCFULL__COLLECTION_NAMED_ROOMS_FILE, + SYNCFULL__COLLECTION_COMMITS_FILE, /* end */ SYNCFULL__COLLECTION_END, } syncfull__file_t; @@ -70,6 +72,8 @@ static char * syncfull__get_fn(uint64_t scope_id, syncfull__file_t ft) return strdup(ti.store->modules_fn); case SYNCFULL__ID_STAT_FILE: return strdup(ti.store->id_stat_fn); + case SYNCFULL__COMMITS_FILE: + return strdup(ti.store->commits_fn); case SYNCFULL__COLLECTION_DAT_FILE: return ti_store_collection_dat_fn(path, scope_id); case SYNCFULL__COLLECTION_TYPES_FILE: @@ -92,6 +96,8 @@ static char * syncfull__get_fn(uint64_t scope_id, syncfull__file_t ft) return ti_store_collection_props_fn(path, scope_id); case SYNCFULL__COLLECTION_NAMED_ROOMS_FILE: return ti_store_collection_named_rooms_fn(path, scope_id); + case SYNCFULL__COLLECTION_COMMITS_FILE: + return ti_store_collection_commits_fn(path, scope_id); case SYNCFULL__COLLECTION_END: break; diff --git a/src/ti/task.c b/src/ti/task.c index a4d979f5..ea70dff6 100644 --- a/src/ti/task.c +++ b/src/ti/task.c @@ -3138,4 +3138,112 @@ int ti_task_add_whitelist_del( fail_pack: msgpack_sbuffer_destroy(&buffer); return -1; +} + +int ti_task_add_commit_add(ti_task_t * task, ti_commit_t * commit) +{ + size_t alloc = ( + 50 + + commit->code->n + + commit->message->n + + commit->by->n + + (commit->err_msg ? commit->err_msg->n : 0) + ); + ti_data_t * data; + msgpack_packer pk; + msgpack_sbuffer buffer; + + if (mp_sbuffer_alloc_init(&buffer, alloc, sizeof(ti_data_t))) + return -1; + msgpack_packer_init(&pk, &buffer, msgpack_sbuffer_write); + + msgpack_pack_array(&pk, 2); + + msgpack_pack_uint8(&pk, TI_TASK_COMMIT); + (void) ti_commit_to_pk(commit, &pk); + + data = (ti_data_t *) buffer.data; + ti_data_init(data, buffer.size); + + if (vec_push(&task->list, data)) + goto fail_data; + + task__upd_approx_sz(task, data); + return 0; + +fail_data: + free(data); + return -1; +} + +int ti_task_add_set_history(ti_task_t * task, uint64_t scope_id, _Bool state) +{ + size_t alloc = 40; + ti_data_t * data; + msgpack_packer pk; + msgpack_sbuffer buffer; + + if (mp_sbuffer_alloc_init(&buffer, alloc, sizeof(ti_data_t))) + return -1; + msgpack_packer_init(&pk, &buffer, msgpack_sbuffer_write); + + msgpack_pack_array(&pk, 2); + + msgpack_pack_uint8(&pk, TI_TASK_SET_HISTORY); + msgpack_pack_array(&pk, 2); + + msgpack_pack_uint64(&pk, scope_id); + mp_pack_bool(&pk, state); + + data = (ti_data_t *) buffer.data; + ti_data_init(data, buffer.size); + + if (vec_push(&task->list, data)) + goto fail_data; + + task__upd_approx_sz(task, data); + return 0; + +fail_data: + free(data); + return -1; +} + +int ti_task_add_del_history( + ti_task_t * task, + uint64_t scope_id, + vec_t * commits) +{ + size_t alloc = 28 + (commits->n * 9); + ti_data_t * data; + msgpack_packer pk; + msgpack_sbuffer buffer; + + if (mp_sbuffer_alloc_init(&buffer, alloc, sizeof(ti_data_t))) + return -1; + msgpack_packer_init(&pk, &buffer, msgpack_sbuffer_write); + + msgpack_pack_array(&pk, 2); + + msgpack_pack_uint8(&pk, TI_TASK_DEL_HISTORY); + msgpack_pack_array(&pk, 2); + + msgpack_pack_uint64(&pk, scope_id); + msgpack_pack_array(&pk, commits->n); + + for (vec_each(commits, ti_commit_t, commit)) + msgpack_pack_uint64(&pk, commit->id); + + data = (ti_data_t *) buffer.data; + ti_data_init(data, buffer.size); + + if (vec_push(&task->list, data)) + goto fail_data; + + task__upd_approx_sz(task, data); + return 0; + +fail_data: + free(data); + return -1; } \ No newline at end of file diff --git a/src/ti/ttask.c b/src/ti/ttask.c index 51dc1808..d320c065 100644 --- a/src/ti/ttask.c +++ b/src/ti/ttask.c @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -703,6 +705,130 @@ static int ttask__whitelist_del(mp_unp_t * up) return 0; } +static int ttask__set_history(mp_unp_t * up) +{ + vec_t ** commits; + mp_obj_t obj, mp_scope, mp_state; + + if (mp_next(up, &obj) != MP_ARR || obj.via.sz != 2 || + mp_next(up, &mp_scope) != MP_U64 || + mp_next(up, &mp_state) != MP_BOOL) + { + log_critical("task `set_history`: invalid task data"); + return -1; + } + + if (mp_scope.via.u64 > 1) + { + ti_collection_t * collection = \ + ti_collections_get_by_id(mp_scope.via.u64); + + if (!collection) + { + log_critical( + "task `set_history`: "TI_COLLECTION_ID" not found", + mp_scope.via.u64); + return -1; + } + commits = &collection->commits; + } + else + { + commits = &ti.commits; + } + + if (ti_commits_set_history(commits, mp_state.via.bool_)) + { + log_critical("failed to set `set_history`"); + return -1; + } + + return 0; +} + +static int ttask__del_history(mp_unp_t * up) +{ + vec_t ** commits; + mp_obj_t obj, mp_scope, mp_id; + size_t i; + + if (mp_next(up, &obj) != MP_ARR || obj.via.sz != 2 || + mp_next(up, &mp_scope) != MP_U64 || + mp_next(up, &obj) != MP_ARR) + { + log_critical("task `del_history`: invalid task data"); + return -1; + } + + if (mp_scope.via.u64 > 1) + { + ti_collection_t * collection = \ + ti_collections_get_by_id(mp_scope.via.u64); + + if (!collection) + { + log_critical( + "task `set_history`: "TI_COLLECTION_ID" not found", + mp_scope.via.u64); + return -1; + } + commits = &collection->commits; + } + else + { + commits = &ti.commits; + } + + for (i = obj.via.sz; i--;) + { + uint32_t j = 0; + if (mp_next(up, &mp_id) != MP_U64) + { + log_critical("task `del_history`: invalid task data"); + return -1; + } + + for (vec_each(*commits, ti_commit_t, commit), j++) + { + if (commit->id == mp_id.via.u64) + { + ti_commit_destroy(vec_remove(*commits, j)); + break; + } + } + } + + (void) vec_shrink(commits); + return 0; +} + +static int ttask__commit(mp_unp_t * up) +{ + vec_t ** commits = &ti.commits; + ti_commit_t * commit = ti_commit_from_up(up); + if (!commit) + { + log_critical( + "failed to unpack commit from task `commit_add` " + "on the @thingsdb scope"); + return -1; + } + if (!(*commits)) + { + log_error("commits not enabled for @thingsdb scope"); + goto fail; + } + if (vec_push(commits, commit)) + { + log_error("failed to add commit for @thingsdb scope"); + goto fail; + } + return 0; +fail: + ti_commit_destroy(commit); + return -1; +} + /* * Returns 0 on success * - for example: {'id': id, 'key': value}, 'expire_ts': ts, 'description':..} @@ -1735,6 +1861,9 @@ int ti_ttask_run(ti_change_t * change, mp_unp_t * up) case TI_TASK_ROOM_SET_NAME: break; case TI_TASK_WHITELIST_ADD: return ttask__whitelist_add(up); case TI_TASK_WHITELIST_DEL: return ttask__whitelist_del(up); + case TI_TASK_SET_HISTORY: return ttask__set_history(up); + case TI_TASK_DEL_HISTORY: return ttask__del_history(up); + case TI_TASK_COMMIT: return ttask__commit(up); } log_critical("unknown thingsdb task: %"PRIu64, mp_task.via.u64); diff --git a/src/ti/type.c b/src/ti/type.c index fed4615b..f974f4d3 100644 --- a/src/ti/type.c +++ b/src/ti/type.c @@ -1404,12 +1404,20 @@ int ti_type_convert( ti_val_t ** val = (ti_val_t **) vec_get_addr(w.vec, field->idx); if (!*val) { - if (change) + /* Depending from where called, we might have a thing->id and no + * change (when processing a change from disk or other node). + * We also might have a change, and no thing->id. see bug #424 */ + *val = field->dval_cb(field); + if (!val) + { + ex_set_mem(e); + goto fail0; + } + ti_val_attach(*val, thing, field); + if (change && thing->id) { ti_task_t * task = ti_task_get_task(change, thing); - - *val = field->dval_cb(field); - if (!*val || !task || ti_task_add_set( + if (!task || ti_task_add_set( task, (ti_raw_t *) field->name, *val)) @@ -1417,14 +1425,6 @@ int ti_type_convert( ex_set_mem(e); goto fail0; } - ti_val_attach(*val, thing, field); - } - else - { - ex_set(e, EX_TYPE_ERROR, - "conversion failed; property `%s` is missing", - field->name->str); - goto fail0; } } } diff --git a/src/ti/val.c b/src/ti/val.c index 3fdfb099..652ef95d 100644 --- a/src/ti/val.c +++ b/src/ti/val.c @@ -756,26 +756,26 @@ int ti_val_init_common(void) "-_"); /* names */ - val__async_name = (ti_val_t *) ti_names_from_str("async"); - val__data_name = (ti_val_t *) ti_names_from_str("data"); - val__time_name = (ti_val_t *) ti_names_from_str("time"); - val__year_name = (ti_val_t *) ti_names_from_str("year"); - val__month_name = (ti_val_t *) ti_names_from_str("month"); - val__day_name = (ti_val_t *) ti_names_from_str("day"); - val__hour_name = (ti_val_t *) ti_names_from_str("hour"); - val__minute_name = (ti_val_t *) ti_names_from_str("minute"); - val__second_name = (ti_val_t *) ti_names_from_str("second"); - val__gmt_offset_name = (ti_val_t *) ti_names_from_str("gmt_offset"); - val__module_name = (ti_val_t *) ti_names_from_str("module"); - val__deep_name = (ti_val_t *) ti_names_from_str("deep"); - val__flags_name = (ti_val_t *) ti_names_from_str("flags"); - val__load_name = (ti_val_t *) ti_names_from_str("load"); - val__beautify_name = (ti_val_t *) ti_names_from_str("beautify"); - val__parent_name = (ti_val_t *) ti_names_from_str("parent"); - val__parent_type_name = (ti_val_t *) ti_names_from_str("parent_type"); - val__key_name = (ti_val_t *) ti_names_from_str("key"); - val__key_type_name = (ti_val_t *) ti_names_from_str("key_type"); - val__unnamed_name = (ti_val_t *) ti_names_from_str("__unnamed__"); + val__async_name = (ti_val_t *) ti_names_from_str_slow("async"); + val__data_name = (ti_val_t *) ti_names_from_str_slow("data"); + val__time_name = (ti_val_t *) ti_names_from_str_slow("time"); + val__year_name = (ti_val_t *) ti_names_from_str_slow("year"); + val__month_name = (ti_val_t *) ti_names_from_str_slow("month"); + val__day_name = (ti_val_t *) ti_names_from_str_slow("day"); + val__hour_name = (ti_val_t *) ti_names_from_str_slow("hour"); + val__minute_name = (ti_val_t *) ti_names_from_str_slow("minute"); + val__second_name = (ti_val_t *) ti_names_from_str_slow("second"); + val__gmt_offset_name = (ti_val_t *) ti_names_from_str_slow("gmt_offset"); + val__module_name = (ti_val_t *) ti_names_from_str_slow("module"); + val__deep_name = (ti_val_t *) ti_names_from_str_slow("deep"); + val__flags_name = (ti_val_t *) ti_names_from_str_slow("flags"); + val__load_name = (ti_val_t *) ti_names_from_str_slow("load"); + val__beautify_name = (ti_val_t *) ti_names_from_str_slow("beautify"); + val__parent_name = (ti_val_t *) ti_names_from_str_slow("parent"); + val__parent_type_name = (ti_val_t *) ti_names_from_str_slow("parent_type"); + val__key_name = (ti_val_t *) ti_names_from_str_slow("key"); + val__key_type_name = (ti_val_t *) ti_names_from_str_slow("key_type"); + val__unnamed_name = (ti_val_t *) ti_names_from_str_slow("__unnamed__"); val__re_email = (ti_val_t *) ti_regex_from_str("/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$/"); val__re_url = (ti_val_t *) ti_regex_from_str("/^(https?|ftp):\\/\\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$/"); val__re_tel = (ti_val_t *) ti_regex_from_str("/^[\\+]?(\\([0-9]{1,4}\\)[-\\s\\.]?){0,2}([0-9]{1,4}[-\\s]?){3,5}$/");