diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..0c9aec6f --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,30 @@ +name: documentation + +on: + push: + branches: [master] + +permissions: + contents: write + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - name: Install dependencies + run: | + pip install sphinx sphinx-book-theme + - name: Sphinx build + run: | + cd docs/ + make html + - name: Deploy to Github Pages + uses: peaceiris/actions-gh-pages@v4 + with: + public_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/_build/ + force_orphan: true + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..9c5f5782 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..41c270bb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..fba1cedb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +CouchbaseOrm Documentation +================================= + +This subdirectory contains the high-level driver documentation, including +tutorials and the reference. + +To build the documentation locally for review, install `sphinx` and +`sphinx-book-theme`, then execute `make html` in this directory: + + pip install 'sphinx' sphinx-book-theme + make html \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..1989d8b1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,57 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'CouchbaseOrm' +copyright = '2024, Woop' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +source_suffix = { + '.txt': 'restructuredtext', +} + +html_theme = 'sphinx_book_theme' \ No newline at end of file diff --git a/docs/includes/unicode-ballot-x.rst b/docs/includes/unicode-ballot-x.rst new file mode 100644 index 00000000..98dbf1e2 --- /dev/null +++ b/docs/includes/unicode-ballot-x.rst @@ -0,0 +1 @@ +.. |x| unicode:: U+2717 \ No newline at end of file diff --git a/docs/includes/unicode-checkmark.rst b/docs/includes/unicode-checkmark.rst new file mode 100644 index 00000000..f4ebe7ab --- /dev/null +++ b/docs/includes/unicode-checkmark.rst @@ -0,0 +1 @@ +.. |checkmark| unicode:: U+2713 \ No newline at end of file diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 00000000..19ae9a69 --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,17 @@ +.. _couchbaseorm: + +************ +CouchbaseOrm +************ + +.. default-domain:: woop + +CouchbaseOrm is the non-official supported object-document record (ORM) for Couchbase in +Ruby. + +.. toctree:: + :titlesonly: + + installation-configuration + schema-configuration + working-with-data \ No newline at end of file diff --git a/docs/installation-configuration.txt b/docs/installation-configuration.txt new file mode 100644 index 00000000..adc3d6d3 --- /dev/null +++ b/docs/installation-configuration.txt @@ -0,0 +1,25 @@ +.. _installation-configuration: + +**************************** +Installation & Configuration +**************************** + +.. default-domain:: woop + +.. toctree:: + :titlesonly: + + installations + reference/compatibility + reference/configuration + reference/rails-integration + +Overview +-------- + +Learn how to install and configure CouchbaseOrm in the following sections: + +- :ref:`Installation ` +- :ref:`Compatibility ` +- :ref:`Configuration ` +- :ref:`Rails Integration ` \ No newline at end of file diff --git a/docs/installations.txt b/docs/installations.txt new file mode 100644 index 00000000..1dd00385 --- /dev/null +++ b/docs/installations.txt @@ -0,0 +1,31 @@ +.. _installation: + +************ +Installation +************ + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Install the Gem +=============== + +CouchbaseOrm is hosted on github https://github.com/Mapotempo/couchbase-orm. +It can be installed manually or with bundler. + +To install the gem with bundler, include the following in your ``Gemfile``: + +.. code-block:: ruby + + git_source(:github) do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/') + "https://github.com/#{repo_name}.git" + end + + gem 'couchbase-orm', github: 'Mapotempo/couchbase-orm' + gem 'couchbase', '~> 3.3.0' \ No newline at end of file diff --git a/docs/reference/associations.txt b/docs/reference/associations.txt new file mode 100644 index 00000000..7f6ad45e --- /dev/null +++ b/docs/reference/associations.txt @@ -0,0 +1,247 @@ +.. _associations: + +************ +Associations +************ + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Referenced Associations +======================= + +CouchbaseOrm supports the ``has_many``, ``belongs_to`` and +``has_and_belongs_to_many`` associations familiar to ActiveRecord users. + +Has Many +-------- + +Use the ``has_many`` association to declare that the parent has zero or more +children stored in a separate collection: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + + has_many :members + end + +The child model must use ``belongs_to`` to declare the +association with the parent: + +.. code-block:: ruby + + class Member < CouchbaseOrm::Base + + belongs_to :band + end + +The child documents contain references to their +respective parents: + +.. code-block:: ruby + + band = Band.create!(members: [Member.new]) + # => # + + band.members + # => [#] + +Use validations to require that at least one child is present: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + + has_many :members + + validates_presence_of :members + end + +Belongs To +---------- + +Use the ``belongs_to`` macro to associate a child with a parent stored in a +separate collection. The ``_id`` of the parent (if a parent is associated) +is stored in the child. + +.. code-block:: ruby + + class Studio < CouchbaseOrm::Base + + belongs_to :band + end + + studio = Studio.create! + # => # + +Although ``has_many`` associations require the +corresponding ``belongs_to`` association to be defined on the child, +``belongs_to`` may also be used ``has_many`` macro. +In this case the child is not accessible from the parent +but the parent is accessible from the child: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + end + + class Studio < CouchbaseOrm::Base + belongs_to :band + end + +Has And Belongs To Many +----------------------- + +Use the ``has_and_belongs_to_many`` macro to declare a many-to-many +association: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + has_and_belongs_to_many :tags + end + + class Tag < CouchbaseOrm::Base + has_and_belongs_to_many :bands + end + +Both model instances store a list of ids of the associated models, if any: + +.. code-block:: ruby + + band = Band.create!(tags: [Tag.create!]) + # => # + + band.tags + # => [#] + +Custom Association Names +------------------------ + +You can name your associations whatever you like, but if the class cannot be inferred by +CouchbaseOrm from the name, and neither can the opposite side you'll want to provide the +macro with some additional options to tell CouchbaseOrm how to hook them up. + +.. code-block:: ruby + + class Car + include CouchbaseOrm::Document + belongs_to :engine, class_name: "Motor", inverse_of: :machine + end + + class Motor + include CouchbaseOrm::Document + has_many :machine, class_name: "Car", inverse_of: :engine + end + +Custom Foreign Keys +----------------------------- + +The attributes used when looking up associations can be explicitly specified. +The default is to use ``id`` on the "parent" association and ``#{association_name}_id`` +on the "child" association, for example with a has_many/belongs_to: + +.. code-block:: ruby + + class Company < CouchbaseOrm::Base + has_many :emails + end + + class Email < CouchbaseOrm::Base + belongs_to :company + end + + company = Company.find(id) + # looks up emails where emails.company_id == company.id + company.emails + +Specify a different ``foreign_key`` to change the attribute name on the "child" +association: + +.. code-block:: ruby + + class Company < CouchbaseOrm::Base + attribute :c, type: String + has_many :emails, foreign_key: 'c_ref' + end + + class Email < CouchbaseOrm::Base + # This definition of c_ref is automatically generated by CouchbaseOrm: + # attribute :c_ref, type: Object + # But the type can also be specified: + attribute :c_ref, type: String + belongs_to :company, foreign_key: 'c_ref' + end + + company = Company.find(id) + # looks up emails where emails.c_ref == company.c + company.emails + +Polymorphism +------------ + +``has_and_belongs_to_many`` associations support polymorphism, which is +having a single association potentially contain objects of different classes. +For example, we could model an organization in which departments and teams +have managers as follows: + +.. code-block:: ruby + + class Department < CouchbaseOrm::Base + has_and_belongs_to_many :unit, class_name "Manager" + end + + class Team < CouchbaseOrm::Base + has_and_belongs_to_many :unit, class_name "Manager" + end + + class Manager < CouchbaseOrm::Base + belongs_to :unit, polymorphic: true + end + + dept = Department.create! + team = Team.create! + + alice = Manager.create!(unit: dept) + alice.unit == dept + # => true + dept.manager == alice + # => true + +.. _dependent-behavior: + +Dependent Behavior +------------------ + +You can provide dependent options to referenced associations to instruct CouchbaseOrm +how to handle situations where one side of the association is deleted, or is attempted +to be deleted. The options are as follows: + +- ``:destroy``: Destroy the child document(s) and run all of the model callbacks. + +If no ``:dependent`` option is provided, deleting the parent document leaves the child document unmodified +(in other words, the child document continues to reference the now deleted parent document via the foreign key attribute). +The child may become orphaned if it is ordinarily only referenced via the parent. + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + has_many :albums, dependent: :destroy + belongs_to :label + end + + class Album < CouchbaseOrm::Base + belongs_to :band + end + + class Label < CouchbaseOrm::Base + has_many :bands + end + + Band.first.destroy # Will delete all associated albums. diff --git a/docs/reference/attributes.txt b/docs/reference/attributes.txt new file mode 100644 index 00000000..cbd85ea1 --- /dev/null +++ b/docs/reference/attributes.txt @@ -0,0 +1,627 @@ +.. _attributes: + +******************** +Attribute Definition +******************** + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + + +.. _attribute-types: + +Attribute Types +=============== + +Couchbase stores underlying document data using +`json types, and +CouchbaseOrm converts Json types to Ruby types at runtime in your application. +For example, a attribute defined with ``type: :float`` will use the Ruby ``Float`` +class in-memory and will persist in the database as the the JSON ``double`` type. + +Attribute type definitions determine how CouchbaseOrm behaves when constructing queries +and retrieving/writing attributes from/to the database. Specifically: + +1. When assigning values to attributes at runtime, the values are converted to the + specified type. +2. When persisting data to Couchbase, the data is sent in an appropriate + type, permitting richer data manipulation within Couchbase or by other + tools. +3. When querying documents, query parameters are converted to the specified + type before being sent to Couchbase. +4. When retrieving documents from the database, attribute values are converted + to the specified type. + +Changing the attribute definitions in a model class does not alter data already stored in +Couchbase. To update type or contents of attributes of existing documents, +the attribute must be re-saved to the database. Note that, due to CouchbaseOrm +tracking which attributes on a model change and only saving the changed ones, +it may be necessary to explicitly write a attribute value when changing the +type of an existing attribute without changing the stored values. + +Consider a simple class for modeling a person in an application. A person may +have a name, date_of_birth, and weight. We can define these attributes +on a person by using the ``attribute`` macro. + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + attribute :name, type: String + attribute :date_of_birth, type: Date + attribute :weight, type: Float + end + +The valid types for attributes are as follows: + +- ``Array`` +- ``Boolean`` +- :ref:`Date ` +- :ref:`DateTime ` +- ``Float`` +- :ref:`Hash ` +- ``Integer`` +- :ref:`Object ` +- ``String`` +- :ref:`Time ` + +To define custom attribute types, refer to :ref:`Custom Attribute Types ` below. + +.. _untyped-attributes: + +Untyped Attributes +------------------ + +.. code-block:: ruby + + class Product < CouchbaseOrm::Base + attribute :properties + end + +An untyped attribute can store values of any type which is directly serializable +to JSON. This is useful when a attribute may contain values of different types +(i.e. it is a variant type attribute), or when the type of values is not known +ahead of time: + +.. code-block:: ruby + + product = Product.new(properties: "color=white,size=large") + product.properties + # => "color=white,size=large" + + product = Product.new(properties: {color: "white", size: "large"}) + product.properties + # => {:color=>"white", :size=>"large"} + +.. _attribute-type-hash: + +Attribute Type: Hash +-------------------- + +When using a attribute of type Hash, be wary of adhering to the +legal key names for Couchabse or else the values will not store properly. + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + attribute :first_name + attribute :url, type: Hash + + # will update the attributes properly and save the values + def set_vals + self.first_name = 'Daniel' + self.url = {'home_page' => 'http://www.homepage.com'} + save + end + + # all data will fail to save due to the illegal hash key + def set_vals_fail + self.first_name = 'Daniel' + self.url = {'home.page' => 'http://www.homepage.com'} + save + end + end + + +.. _attribute-type-time: + +Attribute Type: Time +-------------------- + +``Time`` attributes store values as ``Time`` instances in the configured +time zone. + +``Date`` and ``DateTime`` instances are converted to ``Time`` instances upon +assignment to a ``Time`` attribute: + +.. code-block:: ruby + + class Voter < CouchbaseOrm::Base + + attribute :registered_at, type: Time + end + + Voter.new(registered_at: Date.today) + # => # + +In the above example, the value was interpreted as the beginning of today in +local time, because the application was not configured to use UTC times. + +.. note:: + + When the database contains a string value for a ``Time`` attribute, CouchbaseOrm + parses the string value using ``Time.parse`` which considers values without + time zones to be in local time. + + +.. _attribute-type-date: + +Attribute Type: Date +-------------------- + +CouchbaseOrm allows assignment of values of several types to ``Date`` attributes: + +- ``Date`` - the provided date is stored as is. +- ``Time``, ``DateTime``, - the date component + of the value is taken in the value's time zone. +- ``String`` - the date specified in the string is used. + +In other words, if a date is specified in the value, that date is used without +first converting the value to the configured time zone. + +As a date & time to date conversion is lossy (it discards the time component), +especially if an application operates with times in different time zones it is +recommended to explicitly convert ``String``, ``Time`` and ``DateTime`` +objects to ``Date`` objects before assigning the values to attributes of type +``Date``. + +.. note:: + + When the database contains a string value for a ``Date`` attribute, CouchbaseOrm + parses the string value using ``Time.parse``, discards the time portion of + the resulting ``Time`` object and uses the date portion. ``Time.parse`` + considers values without time zones to be in local time. + + +.. _attribute-type-date-time: + +Attribute Type: DateTime +------------------------ + +Couchbase stores all times as UTC timestamps. When assigning a value to a +``DateTime`` attribute, or when querying a ``DateTime`` attribute, CouchbaseOrm +converts the passed in value to a UTC ``Time`` before sending it to the +Couchbase server. + +``Time``, ``DateTime`` objects embed +time zone information, and the value persisted is the specified moment in +time, in UTC. + +.. code-block:: ruby + + class Ticket < Couchabse::Base + attribute :opened_at, type: DateTime + end + + Time.zone = 'Berlin' + + ticket = Ticket.create!(opened_at: '2018-02-18 07:00:08 -0500') + + ticket.opened_at + # => Sun, 18 Feb 2018 13:00:08 +0100 + ticket + # => # + + Time.zone = 'America/New_York' + ticket.opened_at + # => Sun, 18 Feb 2018 07:00:08 -0500 + +If a string is used as a ``DateTime`` attribute value, the behavior depends on +whether the string includes a time zone: + +.. code-block:: ruby + + Time.zone = 'America/New_York' + ticket.opened_at = 'Mar 4, 2018 10:00:00' + ticket.opened_at + # => Sun, 04 Mar 2018 15:00:00 +0000 + +.. note:: + + When the database contains a string value for a ``DateTime`` attribute, CouchbaseOrm + parses the string value using ``Time.parse`` which considers values without + time zones to be in local time. + +Using Symbols Or Strings Instead Of Classes +------------------------------------------- + +CouchbaseOrm permits using symbols or strings instead of classes to specify the +type of attributes, for example: + +.. code-block:: ruby + + class Order < CouchbaseOrm::Base + + attribute :state, type: :integer + # Equivalent to: + attribute :state, type: "integer" + # Equivalent to: + attribute :state, type: Integer + end + +Only standard attribute types as listed below can be specified using symbols or +strings in this manner. CouchbaseOrm recognizes the following expansions: + +- ``:array`` => ``Array`` +- ``:boolean`` => ``Boolean`` +- ``:date`` => ``Date`` +- ``:date_time`` => ``DateTime`` +- ``:float`` => ``Float`` +- ``:hash`` => ``Hash`` +- ``:integer`` => ``Integer`` +- ``:string`` => ``String`` +- ``:time`` => ``Time`` + +.. _attribute-default-values: + +Specifying Attribute Default Values +----------------------------------- + +A attribute can be configured to have a default value. The default value can be +fixed, as in the following example: + +.. code-block:: ruby + + class Order < CouchbaseOrm::Base + + attribute :state, type: String, default: 'created' + end + +The default value can also be specified as a ``Proc``: + +.. code-block:: ruby + + class Order < CouchbaseOrm::Base + + attribute :fulfill_by, type: Time, default: ->{ Time.now + 3.days } + end + +.. note:: + + Default values that are not ``Proc`` instances are evaluated at class load + time, meaning the following two definitions are not equivalent: + + .. code-block:: ruby + + attribute :submitted_at, type: Time, default: Time.now + attribute :submitted_at, type: Time, default: ->{ Time.now } + + The second definition is most likely the desired one, which causes the + time of submission to be set to the current time at the moment of + document instantiation. + +To set a default which depends on the document's state, use ``self`` +inside the ``Proc`` instance which would evaluate to the document instance +being operated on: + +.. code-block:: ruby + + attribute :fulfill_by, type: Time, default: ->{ + # Order should be fulfilled in 2 business hours. + if (7..8).include?(self.submitted_at.hour) + self.submitted_at + 4.hours + elsif (9..3).include?(self.submitted_at.hour) + self.submitted_at + 2.hours + else + (self.submitted_at + 1.day).change(hour: 11) + end + } + +When defining a default value as a ``Proc``, CouchbaseOrm will apply the default +after all other attributes are set and associations are initialized. +To have the default be applied before the other attributes are set, +use the ``pre_processed: true`` attribute option: + +.. code-block:: ruby + + attribute :fulfill_by, type: Time, default: ->{ Time.now + 3.days }, + pre_processed: true + +The ``pre_processed: true`` option is also necessary when specifying a custom +default value via a ``Proc`` for the ``_id`` attribute, to ensure the ``_id`` +is set correctly via associations: + +.. code-block:: ruby + + attribute :_id, type: String, default: -> { 'hello' }, pre_processed: true + +.. _attribute-aliases: + +Attribute Aliases +----------------- + +It is possible to define attribute aliases. The value will be stored in the +destination attribute but can be accessed from either the destination attribute or +from the aliased attribute: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + + attribute :name, type: String + alias_attribute :n, :name + end + + band = Band.new(n: 'Astral Projection') + # => # + + band.attributes + # => {"_id"=>'5fc1c1ee2c97a64accbeb5e1', "name"=>"Astral Projection"} + + band.n + # => "Astral Projection" + +Aliases can be removed from model classes using the ``unalias_attribute`` +method. + +.. code-block:: ruby + + class Band + unalias_attribute :n + end + +.. _customizing-attribute-behavior: + +Customizing Attribute Behavior +============================== + +CouchbaseOrm offers several ways to customize the behavior of attributes. + + +.. _custom-getters-and-setters: + +Custom Getters And Setters +-------------------------- + +You may override getters and setters for attributes to modify the values +when they are being accessed or written. The getters and setters use the +same name as the attribute. Use ``read_attribute`` and ``write_attribute`` +methods inside the getters and setters to operate on the raw attribute +values. + +For example, CouchbaseOrm provides the ``:default`` attribute option to write a +default value into the attribute. If you wish to have a attribute default value +in your application but do not wish to persist it, you can override the +getter as follows: + +.. code-block:: ruby + + class DistanceMeasurement < CouchbaseOrm::Base + attribute :value, type: Float + attribute :unit, type: String + + def unit + read_attribute(:unit) || "m" + end + + def to_s + "#{value} #{unit}" + end + end + + measurement = DistanceMeasurement.new(value: 2) + measurement.to_s + # => "2.0 m" + measurement.attributes + # => {"_id"=>'613fa0b0a15d5d61502f3447', "value"=>2.0} + +To give another example, a attribute which converts empty strings to nil values +may be implemented as follows: + +.. code-block:: ruby + + class DistanceMeasurement < CouchbaseOrm::Base + + attribute :value, type: Float + attribute :unit, type: String + + def unit=(value) + if value.blank? + value = nil + end + write_attribute(:unit, value) + end + end + + measurement = DistanceMeasurement.new(value: 2, unit: "") + measurement.attributes + # => {"_id"=>'613fa15aa15d5d617216104c', "value"=>2.0, "unit"=>nil} + + +.. _custom-attribute-types: + +Custom Attribute Types +---------------------- + +You can define custom types in CouchbaseOrm and determine how they are serialized +and deserialized. In this example, we define a new attribute type ``Point``, which we +can use in our model class as follows: + +.. code-block:: ruby + + class Venue < CouchbaseOrm::Base + attribute :location, :nested, type: Point + end + +Then make a Ruby class to represent the type. This class must define methods +used for Couchbase serialization and deserialization as follows: + +.. code-block:: ruby + + class Point < CouchbaseOrm::NestedDocument + attribute :x, type: :float + attribute :y, type: :float + + validates :x, :y, presence: true + end + +.. code-block:: ruby + + point = Point.new(x: 12, y: 24) + venue = Venue.new(location: point) # This uses the Point#mongoize instance method. + venue = Venue.new(location: {x: 12, y: 24 }) # This uses the Point.mongoize class method. + +.. _dynamic-attributes: + +Dynamic Attributes +================== + +By default, CouchbaseOrm requires all attributes that may be set on a document to +be explicitly defined using ``attribute`` declarations. CouchbaseOrm also supports +creating attributes on the fly from an arbitrary hash or documents stored in +the database. When a model uses attributes not explicitly defined, such attributes +are called *dynamic attributes*. + +To enable dynamic attributes, include ``CouchbaseOrm::Attributes::Dynamic`` module +in the model: + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + include CouchbaseOrm::Attributes::Dynamic + end + + bob = Person.new(name: 'Bob', age: 42) + bob.name + # => "Bob" + +It is possible to use ``attribute`` declarations and dynamic attributes in the same +model class. Attributes for which there is a ``attribute`` declaration will be +treated according to the ``attribute`` declaration, with remaining attributes +being treated as dynamic attributes. + +Attribute values in the dynamic attributes must initially be set by either +passing the attribute hash to the constructor, mass assignment via +``attributes=``, mass assignment via ``[]=``, using ``write_attribute``, +or they must already be present in the database. + +.. code-block:: ruby + + # OK + bob = Person.new(name: 'Bob') + + # OK + bob = Person.new + bob.attributes = {age: 42} + + # OK + bob = Person.new + bob['age'] = 42 + + # Raises NoMethodError: undefined method age= + bob = Person.new + bob.age = 42 + + # OK + bob = Person.new + # OK - string access + bob.write_attribute('age', 42) + # OK - symbol access + bob.write_attribute(:name, 'Bob') + + # OK, initializes attributes from whatever is in the database + bob = Person.find('123') + +If an attribute is not present in a particular model instance's attributes +hash, both the reader and the writer for the corresponding attribute are not +defined, and invoking them raises ``NoMethodError``: + +.. code-block:: ruby + + bob = Person.new + bob.attributes = {age: 42} + + bob.age + # => 42 + + # raises NoMethodError + bob.name + + # raises NoMethodError + bob.name = 'Bob' + + # OK + bob['name'] = 'Bob' + + bob.name + # => "Bob" + +Attributes can always be read using mass attribute access or ``read_attribute`` +(this applies to models not using dynamic attributes as well): + +.. code-block:: ruby + + bob = Person.new(age: 42) + + # OK - string access + bob['name'] + # => nil + + # OK - symbol access + bob[:name] + # => nil + + # OK - string access + bob['age'] + # => 42 + + # OK - symbol access + bob[:age] + # => 42 + + # OK + bob.attributes['name'] + # => nil + + # OK + bob.attributes['age'] + # => 42 + + # Returns nil - keys are always strings + bob.attributes[:age] + # => nil + + # OK + bob.read_attribute('name') + # => nil + + # OK + bob.read_attribute(:name) + # => nil + + # OK - string access + bob.read_attribute('age') + # => 42 + + # OK - symbol access + bob.read_attribute(:age) + # => 42 + +Special Characters in Attribute Names +------------------------------------- + +CouchbaseOrm permits dynamic attribute names to include spaces and punctuation: + +.. code-block:: ruby + + bob = Person.new('hello world' => 'MDB') + bob.send('hello world') + # => "MDB" + + bob.write_attribute("hello%world", 'MDB') + bob[:"hello%world"] + # => "MDB" \ No newline at end of file diff --git a/docs/reference/callbacks.txt b/docs/reference/callbacks.txt new file mode 100644 index 00000000..21e3697a --- /dev/null +++ b/docs/reference/callbacks.txt @@ -0,0 +1,89 @@ +.. _callbacks: + +********* +Callbacks +********* + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +CouchbaseOrm implements many of the `ActiveRecord callbacks +`_. + + +Document Callbacks +================== + +CouchbaseOrm supports the following callbacks for documents: + +- ``after_initialize`` +- ``after_build`` +- ``before_validation`` +- ``after_validation`` +- ``before_create`` +- ``around_create`` +- ``after_create`` +- ``after_find`` +- ``before_update`` +- ``around_update`` +- ``after_update`` +- ``before_upsert`` +- ``around_upsert`` +- ``after_upsert`` +- ``before_save`` +- ``around_save`` +- ``after_save`` +- ``before_destroy`` +- ``around_destroy`` +- ``after_destroy`` + +Callbacks are available on any document, whether it is embedded within +another document or not. Note that to be efficient, CouchbaseOrm only invokes +the callback on the document that the persistence action was executed on. +This enables CouchbaseOrm to support large hierarchies and to handle optimized +atomic updates efficiently (without invoking callbacks throughout the document +hierarchy). + +Note that using callbacks for domain logic is a bad design practice, and can +lead to unexpected errors that are hard to debug when callbacks in +the chain halt execution. It is our recommendation to only use them +for cross-cutting concerns, like queueing up background jobs. + +.. code-block:: ruby + + class Article + include CouchbaseOrm::Document + attribute :name, type: String + attribute :body, type: String + attribute :slug, type: String + + before_create :send_message + + after_save do |document| + # Handle callback here. + end + + protected + def send_message + # Message sending code here. + end + end + +Callbacks are coming from Active Support, so you can use the new +syntax as well: + +.. code-block:: ruby + + class Article + include CouchbaseOrm::Document + attribute :name, type: String + + set_callback(:create, :before) do |document| + # Message sending code here. + end + end diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt new file mode 100644 index 00000000..8105d6f7 --- /dev/null +++ b/docs/reference/compatibility.txt @@ -0,0 +1,160 @@ + +.. _compatibility: + +************* +Compatibility +************* + +.. default-domain:: mongodb + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Ruby Couchbase Driver Compatibility +=================================== + +The following compatibility table specifies the versions of `Ruby driver for +Couchbase Ruby Client `_ +(the ``couchbase`` gem) supported by the most recent patch releases of the +specified Mongoid versions. + +.. note:: + + Older versions of CouchbaseOrm within the same minor release may support older + driver versions. + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + :class: compatibility-large no-padding + + * - CouchbaseOrm + - Couchbase SDK 3.3 + + * - last + - |checkmark| + + +Ruby Compatibility +================== + +The following compatibility table specifies the versions of Ruby interpreters +supported by CouchbaseOrm. "D" in a column means support for that Ruby version +is deprecated. + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + :class: compatibility-large no-padding + + * - CouchbaseOrm + - Ruby 3.2 + - Ruby 3.1 + - Ruby 3.0 + - Ruby 2.7 + - Ruby 2.6 + - Ruby 2.5 + - Ruby 2.4 + - Ruby 2.3 + - Ruby 2.2 + - JRuby 9.4 + - JRuby 9.3 + - JRuby 9.2 + + * - last + - + - |checkmark| + - |checkmark| + - |checkmark| + - + - + - + - + - + - + - + - + +CouchbaseOrm Server Compatibility +================================= + +The following compatibility table specifies the recommended +version(s) of CouchbaseOrm for use with a specific version of Couchbase server. + +Note that in order to use features of a particular Couchbase server version, +both the driver and CouchbaseOrm must support that server version. +Please refer to `the driver compatibility page +`_ +for driver compatibility matrices. + +The following compatibility table specifies the versions of Ruby interpreters +supported by CouchbaseOrm. "D" in a column means support for that Ruby version +is deprecated. + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + :class: compatibility-large no-padding + + * - CouchbaseOrm + - Couchbase 7.6.0 + - Couchbase 7.2.0 + - Couchbase 7.2.0 + - Couchbase 7.1.0 + - Couchbase 7.0.0 + - Couchbase 6.5.5 + + * - last + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + +.. _rails-compatibility: + +Rails Compatibility +=================== + +The following compatibility table specifies which versions of Ruby on Rails +are supported by CouchbaseOrm. + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + :class: compatibility-large no-padding + + * - CouchbaseOrm + - Rails 7.1 + - Rails 7.0 + - Rails 6.1 + - Rails 6.0 + - Rails 5.2.7 + + * - last + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + +.. include:: /includes/unicode-checkmark.rst +.. include:: /includes/unicode-ballot-x.rst + +Rails Frameworks Support +------------------------ + +Ruby on Rails is comprised of a number of frameworks, which CouchbaseOrm attempts to +provide compatibility with wherever possible. + +Though CouchbaseOrm attempts to offer API compatibility with `Active Record `_, +libraries that depend directly on Active Record may not work as expected when +CouchabseOrm is used as a drop-in replacement. + +.. note:: + + CouchbaseOrm can be used alongside Active Record within the same application without issue. diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt new file mode 100644 index 00000000..97704488 --- /dev/null +++ b/docs/reference/configuration.txt @@ -0,0 +1,107 @@ +.. _configuration: + +************* +Configuration +************* + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +CouchbaseOrm is customarily configured through a ``couchbase.yml`` file that specifies +options and clients. The simplest configuration is as follows, which configures +CouchbaseOrm to talk to a Couchbase server and use the database +named "dev_bucket". + +.. code-block:: yaml + + common: &common + connection_string: couchbase://localhost + username: dev_user + password: dev_password + + development: + <<: *common + bucket: dev_bucket + + test: + <<: *common + bucket: dev_bucket_test + + # set these environment variables on your production server + production: + connection_string: <%= ENV['COUCHBASE_CONNECTION_STRING'] %> + bucket: <%= ENV['COUCHBASE_BUCKET'] %> + username: <%= ENV['COUCHBASE_USER'] %> + password: <%= ENV['COUCHBASE_PASSWORD'] %> + +The top level key in the configuration file, ``development`` in the above +example, refers to the environment name which the application is executing in, +i.e. ``development``, ``test`` or ``production``. + +Generating Default Configuration +================================ + +If you are using Ruby on Rails, you can have CouchbaseOrm generate a default +configuration file for you by running the following command: + +.. code-block:: bash + + rails g couchbase_orm:config + +The configuration file will be placed in ``config/couchbase.yml``. An +It is recommended that all configuration +be specified in ``config/couchbase.yml``, but if you prefer, the ``couchbase_orm.rb`` +initializer may also be used to set configuration options. Note, though, that +settings in ``couchbase.yml`` always take precedence over settings in the +initializer. + +If you are not using Rails, you can configure couchbase-orm with an initializer: + +.. code-block:: ruby + + # config/initializers/couchbase_orm.rb + CouchbaseOrm::Connection.config = { + connection_string: "couchbase://localhost" + username: "dev_user" + password: "dev_password" + bucket: "dev_bucket" + } + + +Loading CouchbaseOrm Configuration +================================== + +If you are using Ruby on Rails, CouchbaseOrm configuration is automatically loaded +for the current environment as stored in ``Rails.env`` when the application +loads. + +ERb Preprocessing +================= + +When loading a configuration file, Mongoid processes it with ERb before +parsing it as YAML. This allows, for example, constructing the contents of +the configuration file at runtime based on environment variables: + +.. code-block:: yaml + + production: + connection_string: <%= ENV['COUCHBASE_CONNECTION_STRING'] %> + bucket: <%= ENV['COUCHBASE_BUCKET'] %> + username: <%= ENV['COUCHBASE_USER'] %> + password: <%= ENV['COUCHBASE_PASSWORD'] %> + +.. note:: + + When outputting values from ERb, ensure the values are valid YAML and + escape them as needed. + +.. note:: + + Since ERb rendering is performed prior to YAML parsing, all ERb directives + in the configuration file are evaluated, including those occurring in YAML + comments. diff --git a/docs/reference/crud.txt b/docs/reference/crud.txt new file mode 100644 index 00000000..07e69cec --- /dev/null +++ b/docs/reference/crud.txt @@ -0,0 +1,614 @@ + +.. _crud: + +*************** +CRUD Operations +*************** + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + + +Saving Documents +================ + +CouchbaseOrm supports all expected CRUD operations for those familiar with other +Ruby mappers like Active Record or Data Mapper. What distinguishes CouchbaseOrm +from other mappers for Couchbase is that the general persistence operations +perform atomic updates on only the attributes that have changed instead of +writing the entire document to the database each time. + +The persistence sections will provide examples on what database operation is +performed when executing the documented command. + +Standard +-------- + +CouchbaseOrm's standard persistence methods come in the form of common methods you +would find in other mapping frameworks. The following table shows all standard +operations with examples. + +.. list-table:: + :header-rows: 1 + :widths: 30 60 + + * - Operation + - Example + + * - ``Model#attributes`` + + *Returns the document's attributes as a* ``ActiveSupport::HashWithIndifferentAccess`` *, and its values in Ruby form.* + - + .. code-block:: ruby + + person = Person.new(first_name: "Heinrich", last_name: "Heine") + + person.attributes + # => { "_id" => '633467d03282a43784c2d56e', "first_name" => "Heinrich", "last_name" => "Heine" } + + * - ``Model.create!`` + + *Insert a document, raising an + error if a validation or server error occurs.* + + *Pass a hash of attributes to create one document with the specified + attributes. + If a single hash is passed, the corresponding document is returned.* + + *If a block is given to* ``create!`` *, it will be invoked with each + document as the argument in turn prior to attempting to save that + document.* + + *If there is a problem saving any of the documents, such as + a validation error or a server error, an exception is raised + and, consequently, none of the documents are returned.* + - + .. code-block:: ruby + + Person.create!( + first_name: "Heinrich", + last_name: "Heine" + ) # => Person instance + + Person.create!([ + { first_name: "Heinrich", last_name: "Heine" }, + { first_name: "Willy", last_name: "Brandt" } + ]) # => Array of two Person instances + + Person.create!(first_name: "Heinrich") do |doc| + doc.last_name = "Heine" + end # => Person instance + + * - ``Model.create`` + + *Instantiate a document and, if validations pass, + insert them into the database.* + + ``create`` *is similar to* ``create!`` *but does not raise + exceptions on validation errors. It still raises errors on server + errors, such as trying to insert a document with an* ``_id`` *that + already exists in the collection.* + + *If any validation errors are encountered, the respective document + is not inserted but is returned along with documents that were inserted. + Use* ``persisted?`` *,* ``new_record?`` *or* ``errors`` *methods + to check which of the returned documents were inserted into the + database.* + - + .. code-block:: ruby + + Person.create( + first_name: "Heinrich", + last_name: "Heine" + ) # => Person instance + + Person.create([ + { first_name: "Heinrich", last_name: "Heine" }, + { first_name: "Willy", last_name: "Brandt" } + ]) # => Array of two Person instances + + Person.create(first_name: "Heinrich") do |doc| + doc.last_name = "Heine" + end # => Person instance + + class Post < CouchbaseOrm::Base + validates_uniqueness_of :title + end + + posts = Post.create([{title: "test"}, {title: "test"}]) + # => array of two Post instances + posts.map { |post| post.persisted? } # => [true, false] + + * - ``Model#save!`` + + *Save the changed attributes to the database atomically, or insert the document if + new. Raises an exception if validations fail or there is a server error.* + + *Returns true if the changed attributes were saved, raises an exception otherwise.* + - + .. code-block:: ruby + + person = Person.new( + first_name: "Heinrich", + last_name: "Heine" + ) + person.save! + + person.first_name = "Christian Johan" + person.save! + + * - ``Model#save`` + + *Save the changed attributes to the database atomically, or insert the document + if new.* + + *Returns true if the changed attributes were saved. Returns false + if there were any validation errors. Raises an exception if + the document passed validation but there was a server error during + the save.* + + *Pass* ``validate: false`` *option to bypass validations.* + + - + .. code-block:: ruby + + person = Person.new( + first_name: "Heinrich", + last_name: "Heine" + ) + person.save + person.save(validate: false) + + person.first_name = "Christian Johan" + person.save + + * - ``Model#update_attributes`` + + *Update the document attributes in the database. Will return true if validation passed, + false if not.* + - + .. code-block:: ruby + + person.update_attributes( + first_name: "Jean", + last_name: "Zorg" + ) + + * - ``Model#update_attributes!`` + + *Update the document attributes in the database and raise an error if validation failed.* + - + .. code-block:: ruby + + person.update_attributes!( + first_name: "Leo", + last_name: "Tolstoy" + ) + + * - ``Model#update_attribute`` + + *Update a single attribute, bypassing validations.* + - + .. code-block:: ruby + + person.update_attribute(:first_name, "Jean") + + * - ``Model#touch`` + + *Update the document's updated_at timestamp* + + *Attempting to touch a destroyed document will raise* ``FrozenError``, + *same as if attempting to update an attribute on a destroyed + document.* + - + .. code-block:: ruby + + person.touch + + * - ``Model#delete`` + + *Deletes the document from the database without running callbacks.* + + *If the document is not persisted, CouchbaseOrm will attempt to delete from + the database any document with the same* ``_id``. + - + .. code-block:: ruby + + person.delete + + person = Person.create!(...) + unsaved_person = Person.new(id: person.id) + unsaved_person.delete + person.reload + # raises CouchbaseOrm::Errors::DocumentNotFound because the person was deleted + + * - ``Model#destroy`` + + *Deletes the document from the database while running destroy callbacks.* + + *If the document is not persisted, CouchbaseOrm will attempt to delete from + the database any document with the same* ``_id``. + - + .. code-block:: ruby + + person.destroy + + person = Person.create!(...) + unsaved_person = Person.new(id: person.id) + unsaved_person.destroy + person.reload + # raises CouchbaseOrm::Errors::DocumentNotFound because the person was deleted + + * - ``Model.delete_all`` + + *Deletes all documents from the database without running any callbacks.* + - + .. code-block:: ruby + + Person.delete_all + +CouchbaseOrm provides the following persistence-related attributes: + +.. list-table:: + :header-rows: 1 + :widths: 30 60 + + * - Attribute + - Example + + + * - ``Model#new_record?`` + + *Returns* ``true`` *if the model instance has not yet been saved + to the database. Opposite of* ``persisted?`` + - + .. code-block:: ruby + + person = Person.new( + first_name: "Heinrich", + last_name: "Heine" + ) + person.new_record? # => true + person.save! + person.new_record? # => false + + * - ``Model#persisted?`` + + *Returns* ``true`` *if the model instance has been saved + to the database. Opposite of* ``new_record?`` + - + .. code-block:: ruby + + person = Person.new( + first_name: "Heinrich", + last_name: "Heine" + ) + person.persisted? # => false + person.save! + person.persisted? # => true + +Reloading +========= + +Use the ``reload`` method to fetch the most recent version of a document from +the database. Any unsaved modifications to the document's attributes are lost: + +.. code-block:: ruby + + band = Band.create!(name: 'foo') + # => # + + band.name = 'bar' + band + # => # + + band.reload + # => # + +If a document has referenced associations, the loaded +associations' are not reloaded but their values are cleared, such that these +associations would be loaded from the database at the next access. + +.. note:: + + Some operations on associations, for example assignment, persists the new + document. In these cases there may not be any unsaved modifications to + revert by reloading. In the following example, the assignment of the + empty array to the association is immediately persisted and reloading + does not make any changes to the document: + + .. code-block:: ruby + + # Assuming band has many tours, which could be referenced: + band = Band.create!(tours: [Tour.create!]) + # ... or embedded: + band = Band.create!(tours: [Tour.new]) + + # This writes the empty tour list into the database. + band.tours = [] + + # There are no unsaved modifications in band at this point to be reverted. + band.reload + + # Returns the empty array since this is what is in the database. + band.tours + # => [] + +Getters & Setters +----------------- + +The recommended way is to use the getter and setter methods generated for +each declared attribute: + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + attribute :first_name + end + + person = Person.new + + person.first_name = "Artem" + person.first_name + # => "Artem" + +To use this mechanism, each attribute must be explicitly declared, or the +model class must enable :ref:`dynamic attributes `. + + +Custom Getters & Setters +------------------------ + +It is possible to explicitly define the getter and setter methods to provide +custom behavior when reading or writing attributes, for example value +transformations or storing values under different attribute names. In this case +``read_attribute`` and ``write_attribute`` methods can be used to read and +write the values directly into the attributes hash: + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + def first_name + read_attribute(:fn) + end + + def first_name=(value) + write_attribute(:fn, value) + end + end + + person = Person.new + + person.first_name = "Artem" + person.first_name + # => "Artem" + + person.attributes + # => {"_id"=> '606477dc2c97a628cf47075b', "fn"=>"Artem"} + +.. _read-write-attribute: + +``read_attribute`` & ``write_attribute`` +---------------------------------------- + +The ``read_attribute`` and ``write_attribute`` methods can be used explicitly +as well. + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + attribute :first_name, as: :fn + attribute :last_name, as: :ln + end + + person = Person.new(first_name: "Artem") + # => # + + person.read_attribute(:first_name) + # => "Artem" + + person.read_attribute(:fn) + # => "Artem" + + person.write_attribute(:last_name, "Pushkin") + person + # => # + + person.write_attribute(:ln, "Medvedev") + person + # => # + +``read_attribute`` and ``write_attribute`` do not require that a attribute with +the used name is defined, but writing attribute values with ``write_attribute`` +does not cause the respective attribute to be defined either: + +.. code-block:: ruby + + person.write_attribute(:undefined, "Hello") + person + # => # + person.attributes + # => {"_id"=> '60647b212c97a6292c195b4c', "first_name"=>"Artem", "last_name"=>"Medvedev", "undefined"=>"Hello"} + + person.read_attribute(:undefined) + # => "Hello" + person.undefined + # raises NoMethodError + +When ``read_attribute`` is used to access a missing attribute, it returns ``nil``. + + +Hash Access +----------- + +CouchbaseOrm model instances define the ``[]`` and ``[]=`` methods to provide +``ActiveSupport::HashWithIndifferentAccess `` style access to the attributes. ``[]`` is an alias for +``read_attribute`` and ``[]=`` is an alias for ``write_attribute``; see +the section on :ref:`read_attribute and write_attribute ` +for the detailed description of their behavior. + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + attribute :first_name, as: :fn + attribute :last_name, as: :ln + end + + person = Person.new(first_name: "Artem") + + person["fn"] + # => "Artem" + + person[:first_name] + # => "Artem" + + person[:ln] = "Medvedev" + person + # => # + + person["last_name"] = "Pushkin" + person + # => # + +Dirty Tracking +============== + +CouchbaseOrm supports tracking of changed or "dirty" attributes with an API that mirrors that of +Active Model. If a defined attribute has been modified in a model the model will be marked as +dirty and some additional behavior comes into play. + + +Viewing Changes +--------------- + +There are various ways to view what has been altered on a model. Changes are recorded +from the time a document is instantiated, either as a new document or via loading from +the database up to the time it is saved. Any persistence operation clears the changes. + +.. code-block:: ruby + + class Person < CouchbaseOrm::Base + attribute :name, type: String + end + + person = Person.first + person.name = "Alan Garner" + + # Check to see if the document has changed. + person.changed? # true + + # Get an array of the names of the changed attributes. + person.changed # [ :name ] + + # Get a hash of the old and changed values for each attribute. + person.changes # { "name" => [ "Alan Parsons", "Alan Garner" ] } + + # Check if a specific attribute has changed. + person.name_changed? # true + + # Get the changes for a specific attribute. + person.name_change # [ "Alan Parsons", "Alan Garner" ] + + # Get the previous value for a attribute. + person.name_was # "Alan Parsons" + +.. note:: + + Setting the associations on a document does not cause the ``changes`` or + ``changed_attributes`` hashes to be modified. This is true for all associations + whether referenced or embedded. Note that changing the _id(s) attribute on + referenced associations does cause the changes to show up in the ``changes`` + and the ``changed_attributes`` hashes. + + +Resetting Changes +----------------- + +You can reset changes of a attribute to its previous value by calling the reset method. + +.. code-block:: ruby + + person = Person.first + person.name = "Alan Garner" + + # Reset the changed name back to the original + person.reset_name! + person.name # "Alan Parsons" + + +Persistence +----------- + +CouchbaseOrm uses dirty tracking as the core of its persistence operations. It looks at the +changes on a document and atomically updates only what has changed, unlike other frameworks +that write the entire document on each save. If no changes have been made, CouchbaseOrm will +not hit the database on a call to ``Model#save``. + + +Viewing Previous Changes +------------------------ + +After a document has been persisted, you can see what the changes were previously by +calling ``Model#previous_changes``. + +.. code-block:: ruby + + person = Person.first + person.name = "Alan Garner" + person.save # Clears out current changes. + + # View the previous changes. + person.previous_changes # { "name" => [ "Alan Parsons", "Alan Garner" ] } + + +Updating Container Fields +========================= + +Be aware that, until +is resolved, all attributes including container ones must be assigned to for +their values to be persisted to the database. + +For example, adding to a set like this does not work: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + attribute :tours, type: Set + end + + band = Band.new + band.tours + # => # + + band.tours << 'London' + # => # + band.tours + # => # + +Instead, the attribute value must be modified outside of the model and assigned +back to the model as follows: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + attribute :tours, type: Set + end + + band = Band.new + + tours = band.tours + # => # + + tours << 'London' + # => # + + band.tours = tours + # => # + + band.tours + # => # diff --git a/docs/reference/queries.txt b/docs/reference/queries.txt new file mode 100644 index 00000000..77a81b12 --- /dev/null +++ b/docs/reference/queries.txt @@ -0,0 +1,355 @@ +.. _queries: + +******* +Queries +******* + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + + +CouchbaseOrm provides a rich query DSL inspired by ActiveRecord. A trivial query +looks as follows: + +.. code-block:: ruby + + Band.where(name: "Depeche Mode") + +A more complex query utilizing various CouchbaseOrm features could be as follows: + +.. code-block:: ruby + + Band. + where(:founded.gte => "1980-01-01"). + where(name: [ "Tool", "Deftones" ]). + +The query methods return ``CouchbaseOrm::Relation::CouchbaseOrm_Relation`` objects, which are chainable +and lazily evaluated wrappers for Couchbase query language (SQL). +The queries are executed when their result sets are iterated. For example: + +.. code-block:: ruby + + # Construct a CouchbaseOrm_Relation object: + + Band.where(name: 'Deftones') + # => #"Deftones"}] + # model: Band> + + # Evaluate the query and get matching documents: + + Band.where(name: 'Deftones').to_a + # => [#] + +Methods like ``first`` and ``last`` return the individual documents immediately. +Otherwise, iterating a CouchbaseOrm_Relation object with methods like ``each`` or ``map`` +retrieves the documents from the server. ``to_a`` can be used to force +execution of a query that returns an array of documents, literally converting +a CouchbaseOrm_Relation object to an Array. + +When a query method is called on a CouchbaseOrm_Relation instance, the method returns a new +CouchbaseOrm_Relation instance with the new conditions added to the existing conditions: + +.. code-block:: ruby + + scope = Band.where(:founded.gte => "1980-01-01") + # => #{"$gte"=>"1980-01-01"}}] + # model: Band> + + scope.where(:founded.lte => "2020-01-01") + # => #{"$gte"=>"1980-01-01", "$lte"=>"2020-01-01"}}] + # model: Band> + + scope + # => #{"$gte"=>"1980-01-01"}}] + # class: Band> + + +Condition Syntax +================ + +CouchbaseOrm supports three ways of specifying individual conditions: + +1. Attribute syntax. +2. SQL syntax. +3. Symbol operator syntax. + +All syntaxes support querying embedded documents using the dot notation. +All syntaxes respect attribute types, if the attribute being queried is defined in the +model class, and attribute aliases. + +The examples in this section use the following model definition: + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + + attribute :name, type: String + attribute :founded, type: Integer + attribute :m, as: :member_count, type: Integer + + belongs_to :manager + end + + class Manager < CouchbaseOrm::Base + + has_many :band + + attribute :name, type: String + end + +Attribute Syntax +---------------- + +The simplest querying syntax utilizes the basic Ruby hashes. Keys can be +symbols or strings, and correspond to attribute names in Couchbase documents: + +.. code-block:: ruby + + Band.where(name: "Depeche Mode") + # => #"Depeche Mode"}] + # model: Band> + + # Equivalent to: + + Band.where("name" => "Depeche Mode") + +SQL Syntax +---------- + +An SQL operator may be specified on any attribute using the string syntax: + +.. code-block:: ruby + + Band.where(founded: {'$gt' => 1980}) + # => #{"$gt"=>1980}}] + # model: Band> + + # Equivalent to: + + Band.where('founded > 1980') + +.. _logical-operations: + +Logical Operations +================== + +CouchbaseOrm supports ``where`` and ``not`` logical operations on +``CouchbaseOrm_Relation`` objects. These methods take one hash of conditions. + +.. code-block:: ruby + + # and with conditions + Band.where(label: 'Trust in Trance').where(name: 'Astral Projection') + +``not`` Behavior +---------------- + +``not`` method can be called without arguments, in which case it will negate +the next condition that is specified. ``not`` can also be called with one +or more hash conditions or ``CouchbaseOrm_Relation`` objects, which will all be negated and +added to the criteria. + +.. code-block:: ruby + + # not negates subsequent where + Band.not.where(name: 'Best') + # => {"name"=>{"$ne"=>"Best"}} + + # not negates its argument + Band.not(name: 'Best') + # => {"name"=>{"$ne"=>"Best"}} + +.. _ordering: + +Ordering +======== + +CouchbaseOrm provides the ``order`` method on ``CouchbaseOrm_Relation`` objects and its alias, +``order_by``, to specify the ordering of documents. These methods take a +hash indicating which attributes to order the documents by, and whether to use +ascending or descending order for each attribute. + +.. code-block:: ruby + + Band.order(name: asc) + # => #asc}] + # model: Band> + + Band.order_by(name: :desc, description: :asc) + # => #:desc}, {"description"=>:asc}] + # class: Band + # embedded: false> + +The direction may be specified with Symbol ``:asc`` and ``:desc`` for ascending +and descending, respectively + +Finding By ``_id`` +================== + +CouchbaseOrm provides the ``find`` method on ``CouchbaseOrm_Relation`` objects to find documents +by their ``_id`` values: + +.. code-block:: ruby + + Band.find('5f0e41d92c97a64a26aabd10') + # => # + +The ``find`` method can accept an array of arguments. +In either case each of the arguments or array elements is taken to be an ``_id`` +value, and documents with all of the specified ``_id`` values are returned in +an array: + +.. code-block:: ruby + + Band.find('5f0e41d92c97a64a26aabd10', '5f0e41b02c97a64a26aabd0e') + # => [#, + #] + + Band.find(['5f0e41d92c97a64a26aabd10', '5f0e41b02c97a64a26aabd0e']) + # => [#, + #] + +If any of the ``_id`` values are not found in the database, the behavior of +``find`` depends on the value of the ``quiet`` configuration +option. If the option is set to ``false``, ``find`` raises +``CouchbaseOrm::Errors::DocumentNotFound`` if any of the ``_id``\s are not found. +If the option is set to ``true`` and ``find`` is given a single ``_id`` to +find and there is no matching document, ``find`` returns ``nil``. If the +option is set to ``false`` and ``find`` is given an array of ids to find +and some are not found, the return value is an array of documents that were +found (which could be empty if no documents were found at all). + +.. _additional-query-methods: + +Additional Query Methods +======================== + +CouchbaseOrm also has some helpful methods on criteria. + +.. list-table:: + :header-rows: 1 + :widths: 30 60 + + * - Operation + - Example + + * - ``CouchbaseOrm_Relation#count`` + + *Get the total number of documents matching a filter, or the total + number of documents in a collection. Note this will always hit + the database for the count.* + + - + .. code-block:: ruby + + Band.count + Band.where(name: "Photek").count + + * - ``CouchbaseOrm_Relation#each`` + + *Iterate over all matching documents in the criteria.* + + - + .. code-block:: ruby + + Band.where(members: 1).each do |band| + p band.name + end + + * - ``CouchbaseOrm_Relation#find_by`` + + *Find a document by the provided attributes. If not found, + raise an error or return nil depending on the value of the* + ``raise_not_found_error`` *configuration option.* + + - + .. code-block:: ruby + + Band.find_by(name: "Photek") + + Band.find_by(name: "Tool") do |band| + band.impressions += 1 + end + + * - ``CouchbaseOrm_Relation#first|last`` + + *Finds a single document given the provided criteria. Get a list of + documents by passing in a limit argument. This method automatically adds + a sort on _id. This can cause performance issues, so if the sort is + undesirable, CouchbaseOrm_Relation#take can be used instead.* + + - + .. code-block:: ruby + + Band.first + Band.where(:members.with_size => 3).first + Band.where(:members.with_size => 3).last + Band.first(2) + + * - ``CouchbaseOrm_Relation#length|size`` + + *Same as count but caches subsequent calls to the database* + + - + .. code-block:: ruby + + Band.length + Band.where(name: "FKA Twigs").size + + * - ``CouchbaseOrm_Relation#pluck`` + + *Get all the values for the provided attribute. + Returns nil for unset attributes and for non-existent attributes.* + + - + .. code-block:: ruby + + Band.all.pluck(:name) + #=> ["Daft Punk", "Aphex Twin", "Ween"] + + Band.all.pluck('address.city') + #=> ["Paris", "Limerick", "New Hope"] + + # Using the earlier definition of Manager, + # expands out to "managers.name" in the query: + Band.all.pluck('managers.n') + #=> [ ["Berry Gordy", "Tommy Mottola"], [], ["Quincy Jones"] ] + + # Accepts multiple attribute arguments, in which case + # the result will be returned as an Array of Arrays. + Band.all.pluck(:name, :likes) + #=> [ ["Daft Punk", 342], ["Aphex Twin", 98], ["Ween", 227] ] + +Class Methods +------------- + +Class methods on models that return criteria objects are also +treated like scopes, and can be chained as well. + +.. code-block:: ruby + + class Band < CouchbaseOrm::Base + attribute :name, type: String + attribute :active, type: Boolean, default: true + + def self.active + where(active: true) + end + end + + Band.active diff --git a/docs/reference/rails-integration.txt b/docs/reference/rails-integration.txt new file mode 100644 index 00000000..08456eff --- /dev/null +++ b/docs/reference/rails-integration.txt @@ -0,0 +1,29 @@ + +.. _rails-integration: + +***************** +Rails Integration +***************** + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +CouchbaseOrm seamlessly integrates into Ruby on Rails applications. +This page describes features that are automatically enabled in the context +of a Rails application and Rails-related functionality which can be +manually enabled. + +Model Preloading +================ + +Views are generated on application load if they don't exist or mismatch. +This works fine in production however by default in development models are lazy loaded. + +.. code-block:: ruby + + config.eager_load = true \ No newline at end of file diff --git a/docs/reference/validations.txt b/docs/reference/validations.txt new file mode 100644 index 00000000..ab28f49b --- /dev/null +++ b/docs/reference/validations.txt @@ -0,0 +1,22 @@ +.. _validations: + +********** +Validation +********** + +.. default-domain:: woop + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +CouchbaseOrm includes ``ActiveModel::Validations`` to supply the basic +validation plus an additional associated and uniqueness validator. + +See the `Active Record Validations +`_ +Rails guide and `ActiveModel::Validations +`_ +documentation for more information. diff --git a/docs/schema-configuration.txt b/docs/schema-configuration.txt new file mode 100644 index 00000000..0c65b5c3 --- /dev/null +++ b/docs/schema-configuration.txt @@ -0,0 +1,23 @@ +.. _schema-configuration: + +******************** +Schema Configuration +******************** + +.. default-domain:: woop + +.. toctree:: + :titlesonly: + + reference/attributes + reference/associations + reference/validations + +Overview +-------- + +See the following sections to learn how to configure a schema with Mongoid: + +- :ref:`Attributes Definition ` +- :ref:`Associations ` +- :ref:`Validation ` diff --git a/docs/working-with-data.txt b/docs/working-with-data.txt new file mode 100644 index 00000000..a491ff39 --- /dev/null +++ b/docs/working-with-data.txt @@ -0,0 +1,23 @@ +.. _working-with-data: + +***************** +Working With Data +***************** + +.. default-domain:: woop + +.. toctree:: + :titlesonly: + + reference/crud + reference/queries + reference/callbacks + +Overview +-------- + +See the following sections to learn more about working with data in CouchbaseOrm: + +- :ref:`CRUD Operations ` +- :ref:`Queries ` +- :ref:`Callbacks `