{"id":34160021,"url":"https://github.com/debevv/camellia","last_synced_at":"2026-03-13T04:31:09.368Z","repository":{"id":46215371,"uuid":"444851680","full_name":"debevv/camellia","owner":"debevv","description":"A lightweight, persistent, hierarchical key-value store, written in Go","archived":false,"fork":false,"pushed_at":"2022-03-07T15:47:16.000Z","size":2461,"stargazers_count":31,"open_issues_count":3,"forks_count":7,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-20T17:49:11.930Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/debevv.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-01-05T15:21:15.000Z","updated_at":"2025-05-15T03:59:21.000Z","dependencies_parsed_at":"2022-09-15T02:42:29.656Z","dependency_job_id":null,"html_url":"https://github.com/debevv/camellia","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/debevv/camellia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/debevv%2Fcamellia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/debevv%2Fcamellia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/debevv%2Fcamellia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/debevv%2Fcamellia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/debevv","download_url":"https://codeload.github.com/debevv/camellia/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/debevv%2Fcamellia/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30457998,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T03:55:51.346Z","status":"ssl_error","status_checked_at":"2026-03-13T03:55:33.055Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-15T08:10:58.186Z","updated_at":"2026-03-13T04:31:09.360Z","avatar_url":"https://github.com/debevv.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# camellia 💮 A lightweight, persistent, hierarchical key-value store\n\n`camellia` is a Go library that implements a hierarchical, persistent key-value store, backed by a SQLite database.  \nIts minimal footprint (just a single `.db` file) makes it suitable for usage in embedded systems, or simply as a minimalist application settings container.  \nAdditionally, this repository contains the companion `cml` command line utility, useful to read, write and import/export a `camellia` DB.  \nThe project was born to be the system-wide settings registry of a Linux embedded device, similar to the one found in Windows.\n\n- Library\n\n  - [API at a glance](#api-at-a-glance)\n  - [API reference](#api-reference)\n  - [Installation and prerequisites](#installation-and-prerequisites)\n  - [Overview](#overview)\n  - [Types](#types)\n  - [JSON import/export](#json-importexport)\n  - [Hooks](#hooks)\n\n- `cml` command\n  - [Command line at a glance](#command-line-at-a-glance)\n  - [Installation](#installation)\n  - [Output of cml help](#output-of-cml-help)\n  - [Database path](#database-path)\n\n---\n\n## Library\n\n## API at a glance\n\n```go\npackage examples\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\tcml \"github.com/debevv/camellia\"\n)\n\nfunc main() {\n\t_, err := cml.Open(\"/home/debevv/camellia.db\")\n\tif err != nil {\n\t\tfmt.Printf(\"Error initializing camellia - %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// Set a string value\n\tcml.Set(\"status/userIdentifier\", \"ABCDEF123456\")\n\n\t// Set a boolean value\n\tcml.Set(\"status/system/areWeOk\", true)\n\n\t// Set a float value\n\tcml.Set(\"sensors/temperature/latestValue\", -48.0)\n\n\t// Set an integer value\n\tcml.Set(\"sensors/saturation/latestValue\", 99)\n\n\t// Read a single float64 value\n\ttemp, err := cml.Get[float64](\"sensors/temperature/latestValue\")\n\tfmt.Printf(\"Last temperature is: %f\", temp)\n\n\t// Read a single bool value\n\tok, err := cml.Get[bool](\"sensors/temperature/latestValue\")\n\tfmt.Printf(\"Are we ok? %t\", ok)\n\n\t// Delete an entry and its children\n\terr = cml.Delete(\"sensors\")\n\n\t// Read a tree of entries\n\tsens, err := cml.GetEntry(\"sensors\")\n\tfmt.Printf(\"Timestamp of last update of saturation value: %v\", sens.Children[\"saturation\"].LastUpdate)\n\n\t// Export whole DB as JSON\n\tj, err := cml.ValuesToJSON(\"\")\n\tfmt.Printf(\"All DB values:\\n%s\", j)\n\n\t// Import DB from JSON file\n\tfile, err := os.Open(\"db.json\")\n\tcml.SetValuesFromJSON(file, false)\n\n\t// Register a callback called after a value is set\n\tcml.SetPostSetHook(\"status/system/areWeOk\", func(path, value string) error {\n\t\tif value == \"true\" {\n\t\t\tfmt.Printf(\"System went back to normal\")\n\t\t} else {\n\t\t\tfmt.Printf(\"Something bad happened\")\n\t\t}\n\n\t\treturn nil\n\t}, true)\n\n\t// Close the DB\n\tcml.Close()\n}\n```\n\n## API reference\n\nhttps://pkg.go.dev/github.com/debevv/camellia\n\n## Installation and prerequisites\n\n### Prerequisites\n\n- Go `1.18` or greater, since this module makes use of generics\n- A C compiler and `libsqlite3`, given the dependency to [go-sqlite3](https://github.com/mattn/go-sqlite3)\n\n### Installation\n\nInside a module, run:\n\n```\ngo get github.com/debevv/camellia\n```\n\n## Overview\n\n### Entries\n\nThe data model is extremely simple.  \nEvery entity in the DB is ab `Entry`. An `Entry` has the following properties:\n\n```go\nPath       string\nLastUpdate time.Time\nIsValue    bool\n```\n\nWhen `IsValue == true`, the `Entry` carries a value, and it's a leaf node in the hierarchy. Values are always represented as `string`s:\n\n```go\nValue string\n```\n\nWhen `IsValue == false`, the `Entry` does not carry a value, but it can have `Children`. It is the equivalent of a directory in a file system:\n\n```go\nChildren map[string]*Entry\n```\n\nThis leads to the complete definition an `Entry`:\n\n```go\ntype Entry struct {\n\tPath       string\n\tLastUpdate time.Time\n\tIsValue    bool\n\tValue      string\n\tChildren   map[string]*Entry\n}\n```\n\n### Paths\n\nPaths are defined as strings separated by slashes (`/`). At the moment of writing this document, no limits are imposed to the length of a segment or to the length of the full path.  \nThe root Entry is identified by an empty string.  \nWhen specifying a path, additional slashes are automatically ignored, so, for example\n\n```\n/my/path\n```\n\nor\n\n```\n///my///path//\n```\n\nare equivalent to\n\n```\nmy/path\n```\n\nand an an empty string is equivalent to `/` or `////`.\n\n### Database versioning and migration\n\nThe schema of the DB is versioned, so after updating the library, `Init()` may return `ErrDBVersionMismatch`. In this case, you should perform the migration of the DB by calling `Migrate()`.\n\n### Setting and forcing\n\nWhen setting a value, if a an Entry at that path already exists, but it's a non-value Entry, the operation fails.  \nForcing a value instead will first delete the existing Entry (and all its children), and then replace it with the new value.\n\n### Concurrency\n\nThe library API should be safe to be called by different goroutines.  \nRegarding the usage of the same DB from different processes, it should be safe too, but more details will be added in the future (TBD).\n\n## Types\n\nThe internal data format for `Entries`' values is `string`. For this reason, the library API offers a set of methods that accept a type parameter and automatically serializes/deserializes values to/from `string`. Example:\n\n```go\n// Gets the value at `path` and converts it to T\nfunc Get[T Stringable](path string) (T, error)\n\n// Converts `value` from T to `string` and sets it at `path`\nfunc Set[T Stringable](path string, value T) error\n```\n\nThe constraint of the type parameter is the `Stringable` `interface`:\n\n```go\ntype Stringable interface {\n\tBaseType\n}\n```\n\nthat in turn is composed by the `BaseType` `interface`, the collection of almost all Go supported base types.  \nData satisfying the `BaseType` interface is serialized using `fmt.Sprint()` and deserialized using `fmt.Scan`.\n\n### Note on custom types\n\nThe library defines an additional `interface` for serialization:\n\n```go\ntype CustomStringable interface {\n\tString() string\n\tFromString(s string) error\n}\n```\n\nintended to be used as a base for user-defined serializable types.  \nUnfortunately, support to custom types is not implemented at the moment, since go 1.18 does not allow to define `Stringable` in this way:\n\n```go\ntype Stringable interface {\n  BaseType | CustomStringable\n}\n```\n\nsince unions of interfaces defining methods are not supported for now.\n\nPlease refer to this [comment](https://github.com/golang/go/issues/45346#issuecomment-862505803) for more details.\n\n## JSON import/export\n\n### Formats\n\nEntries can be imported/exported from/to JSON.  \nTwo different formats are supported:\n\n- **Default**: meant to represent just the hierarchical relationship of Entries and their values. This will be the format used in most cases:\n\n```json\n{\n  \"status\": {\n    \"userIdentifier\": \"ABCDEF123456\",\n    \"system\": {\n      \"areWeOk\": \"true\"\n    }\n  },\n  \"sensors\": {\n    \"temperature\": {\n      \"lastValue\": \"-48.0\"\n    },\n    \"saturation\": {\n      \"lastValue\": \"99\"\n    }\n  }\n}\n```\n\nThis format is used by the following methods:\n\n```go\nfunc SetValuesFromJSON(reader io.Reader, onlyMerge bool) error\nfunc ValuesToJSON(path string) (string, error)\n```\n\n- **Extended**: carrying the all the properties of each Entry. The format was created to accommodate any future addition of useful metadata:\n\n```json\n{\n  \"status\": {\n    \"last_update_ms\": \"1641488635512\",\n    \"children\": {\n      \"userIdentifier\": {\n        \"last_update_ms\": \"1641488675539\",\n        \"value\": \"ABCDEF123456\"\n      },\n      \"system\": {\n        \"last_update_ms\": \"1641453675583\",\n        \"children\": {\n          \"areWeOk\": {\n            \"last_update_ms\": \"1641488659275\",\n            \"value\": \"true\"\n          }\n        }\n      }\n    }\n  },\n  \"sensors\": {\n    \"last_update_ms\": \"1641453582957\",\n    \"children\": {\n      \"temperature\": {\n        \"last_update_ms\": \"1641453582957\",\n        \"children\": {\n          \"lastValue\": {\n            \"last_update_ms\": \"1641453582957\",\n            \"value\": \"-48.0\"\n          }\n        }\n      },\n      \"saturation\": {\n        \"last_update_ms\": \"1641453582957\",\n        \"children\": {\n          \"lastValue\": {\n            \"last_update_ms\": \"1641453582957\",\n            \"value\": \"99\"\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nThis format is used by the following methods:\n\n```go\nfunc SetEntriesFromJSON(reader io.Reader, onlyMerge bool) error\nfunc EntriesToJSON(path string) (string, error)\n```\n\nA note on `last_update_ms`: this property will be put in the JSON when exporting, but ignored when importing. The value of this property will be set to the timestamp of the actual moment of setting the Entry.\n\n### Import and merge\n\nWhen importing from JSON, two distinct modes of operation are supported:\n\n- **Import**: the default operation. Overwrites any existing value with the one found in the input JSON. When overwriting, it forces values instead of just attempting to set them.\n- **Merge**: like import, but does not overwrite existing values with the ones found in the input JSON\n\n## Hooks\n\nHooks are callback methods that can be registered to run before (pre) and after (post) the setting of a certain value:\n\n```go\n// Register a pre set hook to check the value before it is set\ncml.SetPreSetHook(\"sensors/temperature/saturation\", func(path, value string) error {\n    saturation, err := strconv.Atoi(value)\n    if err != nil {\n        return fmt.Errorf(\"invalid saturation value\")\n    }\n\n    // Block the setting of the value if it's out of range\n    if saturation \u003c 0 || saturation \u003e 100 {\n        return fmt.Errorf(\"invalid saturation value. Must be a percentage value\")\n    }\n\n    return nil\n})\n\n// Register an async post set hook and react to changes\ncml.SetPostSetHook(\"status/system/areWeOk\", func(path, value string) error {\n    if value == \"true\" {\n        fmt.Printf(\"System went back to normal\")\n    } else {\n        fmt.Printf(\"Something bad happened\")\n    }\n\n    return nil\n}, true)\n```\n\nHooks can be synchronous or asynchronous:\n\n- Synchronous hooks are run on the same thread calling the `Set()` method. They can block the setting of a value by returning a non-`nil` error.\n- Asynchronous hooks are run on a new goroutine, and their return value is ignored (so the can't block the setting). Only post set hooks can be asynchronous.\n\n---\n\n## `cml` command\n\n## Command line at a glance\n\n```sh\n# Set some values\ncml set status/userIdentifier \"ABCDEF123456\"\ncml set /status/system/areWeOk \"true\"\ncml set \"sensors/saturation/latestValue\" 99\ncml set sensors/temperature/latestValue \"-48.0\"\n\n# Get a value\ncml get sensors/temperature/latestValue\n# -48.0\n\n# Get some values\ncml get sensors\n\n# {\n#   \"saturation\": {\n#       \"latestValue\": \"99\"\n#   },\n#   \"temperature\": {\n#       \"latestValue\": \"-48.0\"\n#   }\n# }\n\n# Get Entries in the extended format\ncml get -e sensors/temperature\n\n# {\n#    \"last_update_ms\": \"1641453582957\",\n#    \"children\": {\n#      \"lastValue\": {\n#        \"last_update_ms\": \"1641453582957\",\n#        \"value\": \"-48.0\"\n#      }\n#    }\n# }\n\n# Try to get a value, fail if it's a non-value\ncml get -v sensors\n# Error getting value - path is not a value\n\n# Merge values from JSON file\ncml merge /path/to/file.json\n```\n\n## Installation\n\nInstall `cml` globally with:\n\n```\ngo install github.com/debevv/camellia/cml@latest\n```\n\n## Output of `cml help`\n\n```\ncml - The camellia hierarchical key-value store utility\nUsage:\ncfg get [-e] [-v] \u003cpath\u003e        Displays the configuration entry (and its children) at \u003cpath\u003e in JSON format\n                                -e        Displays entries in the extended JSON format\n                                -v        Fails (returns nonzero) if the entry is not a value\ncfg set [-f] \u003cpath\u003e \u003cvalue\u003e     Sets the configuration entry at \u003cpath\u003e to \u003cvalue\u003e\n                                -f        Forces overwrite of non-value entries\ncfg delete \u003cpath\u003e               Deletes a configuration entry (and its children)\ncfg import [-e] \u003cfile\u003e          Imports config entries from JSON \u003cfile\u003e\n                                -e        Use the extended JSON format\ncfg merge [-e] \u003cfile\u003e           Imports only non-existing config entries from JSON \u003cfile\u003e\n                                -e        Use the extended JSON format\ncfg migrate                     Migrates the DB to the current supported version\ncfg wipe [-y]                   Wipes the DB\n                                -y        Does not ask for confirmation\ncfg help                        Displays this help message\n```\n\n## Database path\n\n`cml` attempts to automatically determine the path of the SQLite database by reading it from different sources, in the following order:\n\n- From the `CAMELLIA_DB_PATH` environment variable, then\n- From the file `/tmp/camellia.db.path`, then\n- If the steps above fail, the path used is `./camellia.db`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdebevv%2Fcamellia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdebevv%2Fcamellia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdebevv%2Fcamellia/lists"}