# Basic Usage ## Overview What is a document? For the purposes of this library, documents can be objects or associative arrays. Most of the functions are geared toward classes, but arrays can be handy for patching documents. There are several categories of operations that can be accomplished against documents. - **Count** returns the number of documents matching some criteria - **Exists** returns true if any documents match the given criteria - **Insert** adds a new document, failing if the ID field is not unique - **Save** adds a new document, updating an existing one if the ID is already present ("upsert") - **Update** updates an existing document, doing nothing if no documents satisfy the criteria - **Patch** updates a portion of an existing document, doing nothing if no documents satisfy the criteria - **Find** returns the documents matching some criteria - **RemoveFields** removes fields from documents matching some criteria - **Delete** removes documents matching some criteria `Insert` and `Save` were the only two that don't mention criteria. For the others, "some criteria" can be defined a few different ways: - **all** references all documents in the table; applies to Count and Find - **byId** looks for a single document on which to operate; applies to all but Count - **byFields** uses a JSON field comparison to select documents (note that PostgreSQL will always use a text comparison, while SQLite will do its usual [best-guess on types][]); applies to all but Update - **byContains** (PostgreSQL only) uses a JSON containment query (the `@>` operator) to find documents where the given sub-document occurs (think of this as an `=` comparison based on one or more properties in the document; looking for hotels with `{ "country": "USA", "rating": 4 }` would find all hotels with a rating of 4 in the United States); applies to all but Update - **byJsonPath** (PostgreSQL only) uses a JSON patch match query (the `@?` operator) to make specific queries against a document's structure (it also supports more operators than a containment query; to find all hotels rated 4 _or higher_ in the United States, we could query for `"$ ? (@.country == \"USA\" && @.rating > 4)"`); applies to all but Update Finally, `Find` also has `firstBy*` implementations for all supported criteria types. ## Saving Documents The library provides three different ways to save data. The first equates to a SQL `INSERT` statement, and adds a single document to the repository. If automatic IDs are enabled, the document will receive one. ```php $room = new Room(/* ... */); // Parameters are table name and document Document::insert('room', $room); ``` The second is `Document::save`; and inserts the data it if does not exist and replaces the document if it does exist (what some call an "upsert"). It utilizes the `ON CONFLICT` syntax to ensure an atomic statement. Its parameters are the same as those for `Insert`. Note that this does _not_ consider automatic IDs; using this will allow you to bypass that generation for documents you know are new. The third equates to a SQL `UPDATE` statement. `Update` applies to a full document and is usually used by ID, while `Patch` is used for partial updates and may be done by field comparison, JSON containment, or JSON Path match. For a few examples, let's begin with a query that may back the "edit hotel" page. This page lets the user update nearly all the details for the hotel, so updating the entire document would be appropriate. ```php $hotel = Find::byId('hotel', $hotelId, Hotel::class); if ($hotel->isDefined()) { // update hotel properties from the posted form Document::update('hotel', $hotel->id, $hotel); } ``` For the next example, suppose we are upgrading our hotel, and need to take rooms 221-240 out of service*. For PostgreSQL, we can utilize a patch via JSON Path. ```php // PostgreSQL only Patch::byJsonPath('room', "$ ? (@.hotelId == \"abc\" && (@.roomNumber >= 221 && @.roomNumber <= 240)", ['inService' => false]); ``` _* - we are ignoring the current reservations, end date, etc. This is very naïve example!_ For SQLite, we can utilize a `Field` query with a between operator. (This will also work in PostgreSQL.) ```php // SQLite Patch::byFields('room', [Field::between('roomNumber', 221, 240)], ['inService' => false]); ``` ## Finding Documents Functions to find documents start with `Find::`. There are variants to find all documents in a table, find by ID, find by JSON field comparison, find by JSON containment, or find by JSON Path. The hotel update example above utilizes an ID lookup; the descriptions of JSON containment and JSON Path show examples of the criteria used to retrieve using those techniques. `Find` functions take a class name, and will attempt to map the document returned to the class specified. Queries which can return zero-or-one results, like `byId` and the `firstBy*` functions, will return `BitBadger\InspiredByFSharp\Option` representing the possibly-single result. All functions other than `byId` also take an optional list of fields by which the results should be ordered. There is a `Field` function `::named` to support creating a field with no comparison criteria. When specifying this name, one can include direction information like `DESC` and `NULLS FIRST`; additionally, there are two prefixes that will affect the sort order: - `n:` will treat the field as a number. In PostgreSQL, this casts the value to a number; if a value cannot be cast, the database will return an error. For SQLite, this has no effect; it automatically guesses about types. - `i:` will use case-insensitive ordering. In most PostgreSQL implementations, this is the default, but only due to the operating system implementation; Mac hosts are case-sensitive by default. SQLite defaults to a case-sensitive ordering. This flag will normalize this to case-insensitive sorting regardless of the host operating system or libraries. A couple of quick examples: ```php // Sorts "field1" ascending, using default case-sensitivity Field::named('field1') // Sorts "field2" descending, treating it as a number Field::named('n:field2 DESC'); // Sorts "field3" ascending case-insensitively with NULLs last Field::named('i:field3 NULLS LAST'); ``` ### Results and the `DocumentList` `Find::all` and `Find::by*` will return a `DocumentList` instance. This is a lazy iterator over these results, and has several ways to interact with the results, none of which involve loading the entire result set into memory. It is a consumable iteration; once it is read, the results are no longer available. * **`hasItems()`** (v1) / **`hasItems`** (v2) will return `true` if there are items in the list, and will return `false` if there are no items, whether from the initial query returning 0 results or from the generator being consumed. * **`items()`** (v1) / **`items`** (v2) returns a generator that will return each result in turn. Using it in a `foreach` loop will iterate each result; passing it into `iterator_to_array` will load the result into an in-memory array. With the `foreach` loop, only the current result is loaded into memory; it is the default way to process results. * **`map()`** returns a generator that will map the results from the query; as it reads each result, it will transform it, returning that as the value rather than the original document. It takes a `callable` which expects a parameter of the document type in the list and returns something other than `void`. * **`iter()`** takes a `callable` which expects a parameter of the document type in the list, and executes that function against each item as it goes. It can be used for `void` iterations, as well as iterations that may need to capture some outer state and manipulate it as the generator is iterated. All that said, the `foreach` on `items()`/`items` is quite straightforward. ```php // use ->items() for PHP 8.2/8.3 foreach ($result->items as $item) { // Do something amazing with $item } ``` ## Deleting Documents Functions to delete documents start with `Delete::`. Document deletion is supported by ID, JSON field comparison, JSON containment, or JSON Path match. The pattern is the same as for finding or partially updating. _(There is no library method provided to delete all documents, though deleting by JSON field comparison where a non-existent field is null would accomplish this.)_ ## Counting Documents Functions to count documents start with `Count::`. Documents may be counted by a table in its entirety, by JSON field comparison, by JSON containment, or by JSON Path match. _(Counting by ID is an existence check!)_ ## Document Existence Functions to check for existence start with `Exists::`. Documents may be checked for existence by ID, JSON field comparison, JSON containment, or JSON Path match. ## What / How Cross-Reference The table below shows which commands are available for each access method. (X = supported for both, P = PostgreSQL only) | Operation | `all` | `byId` | `byFields` | `byContains` | `byJsonPath` | `firstByFields` | `firstByContains` | `firstByJsonPath` | |----------------|:-----:|:------:|:----------:|:------------:|:------------:|:---------------:|:-----------------:|:-----------------:| | `Count` | X | | X | P | P | | | | | `Exists` | | X | X | P | P | | | | | `Find` | X | X | X | P | P | X | P | P | | `Patch` | | X | X | P | P | | | | | `RemoveFields` | | X | X | P | P | | | | | `Delete` | | X | X | P | P | | | | `Document::insert`, `Document::save`, and `Document::update` operate on single documents. [best-guess on types]: https://sqlite.org/datatype3.html "Datatypes in SQLite • SQLite" [JSON Path]: https://www.postgresql.org/docs/15/functions-json.html#FUNCTIONS-SQLJSON-PATH "JSON Functions and Operators • PostgreSQL Documentation"