diff --git a/doc/_static/ingest-options-pull.png b/doc/_static/ingest-options-pull.png
new file mode 100755
index 0000000000..1d25db0f3a
Binary files /dev/null and b/doc/_static/ingest-options-pull.png differ
diff --git a/doc/_static/ingest-options-push.png b/doc/_static/ingest-options-push.png
new file mode 100755
index 0000000000..db9aa5338d
Binary files /dev/null and b/doc/_static/ingest-options-push.png differ
diff --git a/doc/_static/ingest-options-read.png b/doc/_static/ingest-options-read.png
new file mode 100755
index 0000000000..6c2b96bb81
Binary files /dev/null and b/doc/_static/ingest-options-read.png differ
diff --git a/doc/_static/ingest-options.pptx b/doc/_static/ingest-options.pptx
new file mode 100644
index 0000000000..5017c02936
Binary files /dev/null and b/doc/_static/ingest-options.pptx differ
diff --git a/doc/_static/ingest-table-types-dependent.png b/doc/_static/ingest-table-types-dependent.png
new file mode 100755
index 0000000000..f58dbc3c57
Binary files /dev/null and b/doc/_static/ingest-table-types-dependent.png differ
diff --git a/doc/_static/ingest-table-types-partitioned.png b/doc/_static/ingest-table-types-partitioned.png
new file mode 100755
index 0000000000..0e5227da3c
Binary files /dev/null and b/doc/_static/ingest-table-types-partitioned.png differ
diff --git a/doc/_static/ingest-table-types-regular.png b/doc/_static/ingest-table-types-regular.png
new file mode 100755
index 0000000000..307ef1fe48
Binary files /dev/null and b/doc/_static/ingest-table-types-regular.png differ
diff --git a/doc/_static/ingest-table-types.pptx b/doc/_static/ingest-table-types.pptx
new file mode 100644
index 0000000000..147cc20647
Binary files /dev/null and b/doc/_static/ingest-table-types.pptx differ
diff --git a/doc/_static/ingest-trans-multiple-chunks.png b/doc/_static/ingest-trans-multiple-chunks.png
new file mode 100755
index 0000000000..82b077c859
Binary files /dev/null and b/doc/_static/ingest-trans-multiple-chunks.png differ
diff --git a/doc/_static/ingest-trans-multiple-one.png b/doc/_static/ingest-trans-multiple-one.png
new file mode 100755
index 0000000000..27ea3c378c
Binary files /dev/null and b/doc/_static/ingest-trans-multiple-one.png differ
diff --git a/doc/_static/ingest-trans-multiple-scattered.png b/doc/_static/ingest-trans-multiple-scattered.png
new file mode 100755
index 0000000000..5ca2698033
Binary files /dev/null and b/doc/_static/ingest-trans-multiple-scattered.png differ
diff --git a/doc/_static/ingest-trans-multiple.pptx b/doc/_static/ingest-trans-multiple.pptx
new file mode 100644
index 0000000000..f10512be8a
Binary files /dev/null and b/doc/_static/ingest-trans-multiple.pptx differ
diff --git a/doc/_static/ingest-transaction-fsm.png b/doc/_static/ingest-transaction-fsm.png
new file mode 100755
index 0000000000..29d4a076b5
Binary files /dev/null and b/doc/_static/ingest-transaction-fsm.png differ
diff --git a/doc/_static/ingest-transaction-fsm.pptx b/doc/_static/ingest-transaction-fsm.pptx
new file mode 100644
index 0000000000..7f2c2bbf0c
Binary files /dev/null and b/doc/_static/ingest-transaction-fsm.pptx differ
diff --git a/doc/_static/ingest-transactions-aborted.png b/doc/_static/ingest-transactions-aborted.png
new file mode 100755
index 0000000000..f5e7bcf81c
Binary files /dev/null and b/doc/_static/ingest-transactions-aborted.png differ
diff --git a/doc/_static/ingest-transactions-aborted.pptx b/doc/_static/ingest-transactions-aborted.pptx
new file mode 100644
index 0000000000..adfae1ad62
Binary files /dev/null and b/doc/_static/ingest-transactions-aborted.pptx differ
diff --git a/doc/_static/ingest-transactions-failed.png b/doc/_static/ingest-transactions-failed.png
new file mode 100755
index 0000000000..d2a0d60d5f
Binary files /dev/null and b/doc/_static/ingest-transactions-failed.png differ
diff --git a/doc/_static/ingest-transactions-failed.pptx b/doc/_static/ingest-transactions-failed.pptx
new file mode 100644
index 0000000000..1ffe243f9f
Binary files /dev/null and b/doc/_static/ingest-transactions-failed.pptx differ
diff --git a/doc/_static/ingest-transactions-resolved.png b/doc/_static/ingest-transactions-resolved.png
new file mode 100755
index 0000000000..a321adbd1c
Binary files /dev/null and b/doc/_static/ingest-transactions-resolved.png differ
diff --git a/doc/_static/ingest-transactions-resolved.pptx b/doc/_static/ingest-transactions-resolved.pptx
new file mode 100644
index 0000000000..932f344e0a
Binary files /dev/null and b/doc/_static/ingest-transactions-resolved.pptx differ
diff --git a/doc/_static/subchunks.png b/doc/_static/subchunks.png
new file mode 100644
index 0000000000..c2fe77ff0d
Binary files /dev/null and b/doc/_static/subchunks.png differ
diff --git a/doc/admin/data-table-indexes.rst b/doc/admin/data-table-indexes.rst
new file mode 100644
index 0000000000..b6108d206e
--- /dev/null
+++ b/doc/admin/data-table-indexes.rst
@@ -0,0 +1,608 @@
+
+.. _admin-data-table-index:
+
+==================
+Data Table Indexes
+==================
+
+.. note::
+
+ All operations on indexes are only allowed on databases that are in the published state. The system will
+ refuse all requests on databases that are in a process of being ingested. This is done for three reasons:
+
+ #. To avoid interfering with the catalog ingest operations.
+ #. To prevent returning an inconsistent (or wrong) state of the indexes
+ #. To prevent transient errors due to potential race conditions since the overall state of the distributed catalogs
+ might be being changed by the Ingest system.
+
+.. _admin-data-table-index-intro:
+
+Introduction
+------------
+
+This document explains how to use the index management API of the Qserv Replication System. Services provided by the system
+are related to the following index management operations in MySQL:
+
+.. code-block:: sql
+
+ SHOW INDEX FROM
...
+ CREATE ... INDEX ON
...
+ DROP INDEX ON
...
+
+However, unlike the single table operations listed above, the services allow managing groups of related tables distributed
+across Qserv worker nodes. Hence, each request for any service refers to a single such group. In particular:
+
+- if a request refers to the *regular* (fully replicated) table then all instances of the table will be affected by the request.
+- otherwise (in the case of the *partitioned* tables) each replica of every chunk of the table will be included in an operation.
+ Note that the index management services differentiate between the *chunk* tables themselves and the corresponding *full overlap*
+ tables. When submitting a request, a user will have to choose which of those tables will be included in the operation.
+
+The latter would also be reflected in the result and error reports made by the services. The JSON objects returned by
+the services would return the names of the *final* tables affected by the operations, not the names of the corresponding
+*base* name of a table specified in service requests. This is done to facilitate investigating problems with Qserv should
+they occur. See more on the *base* and *final* table names in the section:
+
+- :ref:`ingest-general-base-table-names` (REST)
+
+Things to consider before creating indexes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The recommendations mentioned in this section are not complete. Please, get yourself familiarized with
+the MySQL documentation on indexes before attempting the operation. Read the following instructions on
+the index creation command:
+
+- https://dev.mysql.com/doc/refman/8.4/en/create-index.html
+
+Always keep in mind that there is the MySQL (or alike) machinery behind Qserv and that the data tables
+in Qserv are presently based on the MyISAM storage engine:
+
+- https://dev.mysql.com/doc/refman/8.4/en/myisam-storage-engine.html
+
+These are a few points to consider.
+
+General:
+
+- Indexes are created on tables, and not on views.
+- Indexes are created in the scope of the table, and not databases.
+- In the case of the *partitioned* tables, the *chunk* and *full* overlap may not need to have the same set of indexes.
+ In some cases, it may not be even possible, for instance, due to different ``UNIQUE`` constraints requirements.
+- Please, provide a reasonable comment for an index, even though, comments aren't mandatory. Your comments could be useful
+ for bookkeeping purposes to know why and what the index was created for.
+- Be aware that indexes take additional space on the Qserv workers' filesystems where the database tables are residing.
+ Potentially, if too many indexes were created, MySQL may run out of disk space and stop working. The rule of thumb for
+ estimating the amount of space to be taken by an index is based on the sizes of columns mentioned in an index
+ specification (explained in the next section) multiplied by the total number of rows in a table (on which the index
+ is being created). There are other factors here, such as the type of a table (regular or partitioned) as well as
+ the number of replicas for the partitioned tables. The index management service which is used for creating indexes
+ will also make the best attempt to prevent creating indexes if it will detect that the amount of available disk space
+ is falling too low. In this case, the service will refuse to create an index and an error will be reported back.
+
+When choosing a name for an index:
+
+- The name should be unique (don't confuse this with the UNIQUE keyword used in the index specifications) in the scope
+ of the table. It means it should not be already taken by some other index. Always check which indexes already exist
+ in a table before creating a new one.
+- Generally, the name must adhere to the MySQL requirements for identifiers as explained
+ in:
+
+ - https://dev.mysql.com/doc/refman/8.4/en/identifier-qualifiers.html
+
+- Keep in mind that names of identifiers (including indexes) in Qserv are case-insensitive. This is not the general
+ requirement in MySQL, where the case sensitivity of identifiers is configurable one way or another. It's because
+ of a design decision the original Qserv developers made to configure the underlying MySQL machinery.
+- To make things simple, restrain from using special characters (all but the underscore one ``_``).
+- The length of the name should not exceed 64 characters:
+
+ - https://dev.mysql.com/doc/refman/8.4/en/identifier-length.html
+
+Error reporting
+^^^^^^^^^^^^^^^^
+
+All services mentioned in the document are the Replication system's services, and they adhere to the same error reporting
+schema. The schema is explained in the document:
+
+- :ref:`ingest-general-error-reporting` (REST)
+
+In addition, the index managemnt services may return the service-specific error conditions in the ``error_ext`` attribute:
+
+.. code-block:
+
+ "error_ext" : {
+ "job_state" : ,
+ "workers" : {
+ : {
+
: {
+ "request_status" : ,
+ "request_error" :
+ },
+ ...
+ },
+ ...
+ }
+ }
+
+Where:
+
+``job_state`` : *string*
+ The completion status (state) of the corresponding C++ job classes:
+
+ - ``lsst::qserv::replica::SqlGetIndexesJob``
+ - ``lsst::qserv::replica::SqlCreateIndexesJob``
+ - ``lsst::qserv::replica::SqlDropIndexesJob``
+
+ The status will be serialized into a string. The explanation of the possible values could be found in
+ the following C++ header:
+
+ - https://github.com/lsst/qserv/blob/master/src/replica/jobs/Job.h
+
+ Look for this enum type:
+
+ .. code-block::
+
+ enum ExtendedState {
+ NONE, ///< No extended state exists at this time.
+ SUCCESS, ///< The job has been fully implemented.
+ CONFIG_ERROR, ///< Problems with job configuration found.
+ FAILED, ///< The job has failed.
+ ...
+ }
+
+ Also look for worker/table-specific errors in a JSON object explained below.
+
+``workers`` : *object*
+ The JSON object which has unique identifiers of workers (attribute ``worker``) as the keys, where the corresponding
+ value for the worker is another JSON object which has names of worker-side tables as the next-level keys for
+ descriptions of problems with managing indexes for the corresponding tables.
+
+ .. note:
+
+ Only the problematic tables (if any) would be mentioned in the report. If no problems were seen during
+ the index management operations then a collection of workers and tables will be empty.
+
+``request_status`` : *string*
+ The completion status (state) of the index creation C++ request classes:
+
+ - ``SqlGetIndexesRequest``
+ - ``SqlCreateIndexesRequest``
+ - ``SqlDropIndexesRequest``
+
+ The status will be serialized into a string. More information on possible values could be found in the
+ following C++ header:
+
+ - https://github.com/lsst/qserv/blob/main/src/replica/requests/Request.h
+
+ Look for this enum type:
+
+ .. code-block::
+
+ enum ExtendedState {
+ NONE, /// No extended state exists at this time
+ SUCCESS, /// The request has been fully implemented
+ CLIENT_ERROR, /// The request could not be implemented due to
+ /// an unrecoverable client-side error.
+ SERVER_BAD, /// Server reports that the request can not be implemented
+ /// due to incorrect parameters, etc.
+ SERVER_ERROR, /// The request could not be implemented due to
+ /// an unrecoverable server-side error.
+ ...
+ };
+
+``request_error`` : *string*
+ This string provides an expanded explanation of an error reported by the Replication system's worker (in case if the
+ request failed on the worker's side and is reported to the service).
+
+.. note::
+
+ **Reporting partial successes or failures**
+
+ Since the index management requests may (will) involve multiple tables, the corresponding operations may be potentially
+ partially successful and partially not successful. All failures for specific indexes which couldn't be managed (created,
+ queried, or deleted) would be reported as explained in the previous section. For example, that would be a case if a request
+ was made to drop a known to-exist index, and if no such index existed for some final tables. There may be various reasons
+ why this might happen. An explanation of the reasons is beyond a scope of this document. The best way a user should treat
+ this situation is to expect that the service would do the "best effort" of removing the index. It's also allowed to run
+ the index removal request multiple times. This won't make any harm. All subsequent requests will report failures for all
+ final tables in the specified group of tables.
+
+.. _admin-data-table-index-create:
+
+Creating
+--------
+
+To create a new index, a user must submit a request to the service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``POST``
+ - ``/replication/sql/index``
+
+Where the request object has the following schema:
+
+.. code-block::
+
+ { "database" : ,
+ "table" : ,
+ "overlap" : ,
+ "index" : ,
+ "spec" : ,
+ "comment" : ,
+ "columns" : [
+ { "column" : ,
+ "length " :,
+ "ascending" :
+ },
+ ..
+ ],
+ "auth_key" :
+ }
+
+Where:
+
+``database`` : *string*
+ The required name of the database where the table resides.
+
+``table`` : *string*
+ The required *base* name of the table where the index will be created.
+
+``overlap`` : *number* := ``0``
+ The optional *overlap* flagg indicating a sub-type of the *chunk* table. The value should be one of the following:
+
+ - ``1`` : *full overlap*
+ - ``0`` : *chunk*
+
+``index`` : *string*
+ The required name of the index to be created.
+
+``spec`` : *string*
+ The required index specification. The specification should adhere to the MySQL requirements for the index creation command:
+
+ - https://dev.mysql.com/doc/refman/8.4/en/create-index.html
+
+ Where a value of the parameter should be one of the following keywords: ``DEFAULT``, ``UNIQUE``, ``FULLTEXT``, or ``SPATIAL``.
+ All but the first one (``DEFAULT``) are mentioned in the MySQL documentation. The keywords map to the indfex
+ creation command:
+
+ .. code-block:: sql
+
+ CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name ...
+
+ The REST service expects ``DEFAULT`` in those cases when none of the other three specifications are provided.
+ Any other value or a lack of ant will be considered as an error.
+
+``comment`` : *string* := ``""``
+ The optional comment for the index. The value will be passed via the ``COMMENT`` parameter of the MySQL index
+ creation command:
+
+ .. code-block:: sql
+
+ CREATE ... INDEX ... COMMENT 'string' ...
+
+``columns`` : *array*
+ The required non-empty array of JSON objects keys mentioned in the key_part of the index creation statements:
+
+ .. code-block:: sql
+
+ CREATE ... INDEX index_name ON tbl_name (key_part,...) ...
+
+ .. note::
+
+ The current implementation of the service doesn't support the extended-expression syntax of key_part introduced
+ in MySQL version 8.
+
+ These are the mandatory attributes of each key-specific object:
+
+ ``column`` : *string*
+ The required name of a column.
+ ``length`` : *number*
+ The required length of a substring used for the index. It only has a meaning for columns of
+ types: ``TEXT``, ``CHAR(N)``, ``VARCHAR(N)``, ``BLOB`` , etc. And it must be always 0 for other column
+ types (numeric, etc.). Otherwise, an index creation request will fail.
+
+ ``ascending`` : *number*
+ The required sorting order of the column in the index. It translates into ``ASC`` or ``DESC`` optiona
+ in the key definition in ``key_part``. A value of ``0`` will be interpreted as ``DESC``. Any other positive number
+ will be imterpreted as to ``ASC``.
+
+``auth_key`` : *string*
+ The required zauthorization key.
+
+Here is an example of the index creation request. Let's supposed we have the *regular* (fully replicated)
+table that has the following schema:
+
+.. code-block:: sql
+
+ CREATE TABLE `sdss_stripe82_01`.`Science_Ccd_Exposure_NoFile` (
+ `scienceCcdExposureId` BIGINT(20) NOT NULL,
+ `run` INT(11) NOT NULL,
+ `filterId` TINYINT(4) NOT NULL,
+ `camcol` TINYINT(4) NOT NULL,
+ `field` INT(11) NOT NULL,
+ `path` VARCHAR(255) NOT NULL
+ );
+
+And, suppose we are going to create the ``PRIMARY`` key index based on the very first column ``scienceCcdExposureId``.
+In this case the request object will look like this:
+
+.. code-block:: json
+
+ { "database" : "sdss_stripe82_01",
+ "table" : "Science_Ccd_Exposure_NoFile",
+ "index" : "PRIMARY",
+ "spec" : "UNIQUE",
+ "comment" : "This is the primary key index",
+ "columns" : [
+ { "column" : "scienceCcdExposureId",
+ "length" : 0,
+ "ascending" : 1
+ }
+ ],
+ "auth_key" : ""
+ }
+
+The request deliberately misses the optional ``overlap`` attribute since it won't apply to the regular tables.
+
+Here is how the request could be submitted to the service using ``curl``:
+
+.. code-block:: bash
+
+ curl 'http://localhost:25081/replication/sql/index' \
+ -X POST -H "Content-Type: application/json" \
+ -d '{"database":"sdss_stripe82_01","table":"Science_Ccd_Exposure_NoFile",
+ "index":"PRIMARY","spec":"UNIQUE","comment":"This is the primary key index",
+ "columns":[{"column":"scienceCcdExposureId","length":0,"ascending":1}],
+ "auth_key":""}'
+
+.. _admin-data-table-index-delete:
+
+Deleting
+--------
+
+To delete an existing index, a user must submit a request to the service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``DELETE``
+ - ``/replication/sql/index``
+
+Where the request object has the following schema:
+
+.. code-block::
+
+ { "database" : ,
+ "table" : ,
+ "overlap" : ,
+ "index" : ,
+ "auth_key" :
+ }
+
+Where:
+
+``database`` : *string*
+ The required name of the database where the table resides.
+
+``table`` : *string*
+ The required *base* name of the table where the index will be created.
+
+``overlap`` : *number* := ``0``
+ The optional *overlap* flagg indicating a sub-type of the *chunk* table. The value should be one of the following:
+
+ - ``1`` : *full overlap*
+ - ``0`` : *chunk*
+
+``index`` : *string*
+ The required name of the index to be dropped.
+
+Here is an example of the index deletion request. It's based on the same table that was mentioned in the previous section.
+The request object will look like this:
+
+.. code-block:: json
+
+ { "database" : "sdss_stripe82_01",
+ "table" : "Science_Ccd_Exposure_NoFile",
+ "index" : "PRIMARY",
+ "auth_key" : ""
+ }
+
+Here is how the request could be submitted to the service using ``curl``:
+
+.. code-block:: bash
+
+ curl 'http://localhost:25081/replication/sql/index' \
+ -X DELETE -H "Content-Type: application/json" \
+ -d '{"database":"sdss_stripe82_01","table":"Science_Ccd_Exposure_NoFile",
+ "index":"PRIMARY",
+ "auth_key":""}'
+
+.. _admin-data-table-index-inspect:
+
+Inspecting
+----------
+
+To inspect the existing indexes, a user must submit a request to the service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``GET``
+ - ``/replication/sql/index/:database/:table[?overlap={0|1}]``
+
+Where the service path has the following parameters:
+
+``database`` : *string*
+ The name of a database affected by the operation.
+
+``table`` : *string*
+ The name of the table for which the indexes are required to be collected.
+
+The optional query parameyter is
+
+``overlap`` : *number* := ``0``
+ The optional *overlap* flagg indicating a sub-type of the *chunk* table. The value should be one of the following:
+
+ - ``1`` : *full overlap*
+ - ``0`` : *chunk*
+
+In case of successful completion of the request the JSON object returned by the service will have the following schema:
+
+.. code-block::
+
+ { "status": {
+ "database" : ,
+ "table" : ,
+ "overlap" : ,
+ "indexes" : [
+ { "name" : ,
+ "unique" : ,
+ "type" : ,
+ "comment" : ,
+ "status" : ,
+ "num_replicas_total" : ,
+ "num_replicas" : ,
+ "columns" : [
+ { "name" : ,
+ "seq" : ,
+ "sub_part" : ,
+ "collation" :
+ },
+ ...
+ ]
+ },
+ ...
+ ]
+ }
+ }
+
+Where:
+
+``name`` : *string*
+ The name of the index (the key).
+
+``unique`` : *number*
+ The numeric flag indicates of the index's keys are unique, where a value of ``0`` means they're unique. Any other
+ value would mean the opposite.
+
+``type`` : *string*
+ The type of index, such as ``BTREE``, ``SPATIAL``, ``PRIMARY``, ``FULLTEXT``, etc.
+
+``comment`` : *string*
+ An optional explanation for the index passed to the index creation statement:
+
+ .. code-block:: sql
+
+ CREATE ... INDEX ... COMMENT 'string' ...
+
+``status`` : *string*
+ The status of the index. This parameter considers the aggregate status of the index across all replicas of the table.
+ Possible values here are:
+
+ - ``COMPLETE`` : the same index (same type, columns) is present in all replicas of the table (or its chunks in the case
+ of the partitioned table)
+ - ``INCOMPLETE`` : the same index is present in a subset of replicas of the table, where all indexes have the same
+ definition.
+ - ``INCONSISTENT`` : instances of the index that have the same name have different definitions in some replicas
+
+ .. warning::
+
+ The result object reported by the service will not provide any further details on the last status ``INCONSISTENT``
+ apart from indicating the inconsistency. It will be up to the data administrator to investigate which replicas have
+ unexpected index definitions.
+
+``num_replicas_total`` : *number*
+ The total number of replicas that exist for the table. This is the target number of replicas where the index is expected
+ to be present.
+
+``num_replicas`` : *number*
+ The number of replicas where the index was found to be present. If this number is not the same as the one reported
+ in the attribute
+ ``num_replicas_total`` then the index will be ``INCOMPLETE``.
+
+``columns`` : *array*
+ The collection of columns that were included in the index definition. Each entry of the collection has:
+
+ ``name`` : *string*
+ The name of the column
+ ``seq`` : *number*
+ The 1-based position of the column in the index.
+ ``sub_part`` : *number*
+ The index prefix. That is, the number of indexed characters if the column is only partly indexed 0 if
+ the entire column is indexed.
+ ``collation`` : *string*
+ How the column is sorted in the index. This can have values ``ASC``, ``DESC``, or ``NOT_SORTED``.
+
+The following request will report indexes from the fully-replicated table ``ivoa.ObsCore``:
+
+.. code-block:: bash
+
+ curl http://localhost:25081/replication/sql/index/ivoa/ObsCore -X GET
+
+The (truncated and formatted for readability) result of an operation performed in a Qserv deployment with 6-workers may
+look like this:
+
+.. code-block:: json
+
+ { "status" : {
+ "database" : "ivoa"
+ "indexes" : [
+ { "name" : "idx_dataproduct_subtype",
+ "type" : "BTREE",
+ "unique" : 0,
+ "status" : "COMPLETE",
+ "num_replicas" : 6,
+ "num_replicas_total" : 6,
+ "comment" : "The regular index on the column dataproduct_subtype",
+ "columns" : [
+ { "collation" : "ASC",
+ "name" : "dataproduct_subtype",
+ "seq" : 1,
+ "sub_part" : 0
+ }
+ ]
+ },
+ { "name" : "idx_s_region_bounds",
+ "type" : "SPATIAL",
+ "unique" : 0,
+ "status" : "COMPLETE",
+ "num_replicas" : 6,
+ "num_replicas_total" : 6,
+ "comment" : "The spatial index on the geometric region s_region_bounds",
+ "columns" : [
+ { "collation" : "ASC",
+ "name" : "s_region_bounds",
+ "seq" : 1,
+ "sub_part" : 32
+ }
+ ]
+ },
+ { "name" : "idx_lsst_tract_patch",
+ "type" : "BTREE",
+ "unique" : 0,
+ "status" : "COMPLETE",
+ "num_replicas" : 6,
+ "num_replicas_total" : 6,
+ "comment" : "The composite index on the columns lsst_tract and lsst_patch",
+ "columns" : [
+ { "collation" : "ASC",
+ "name" : "lsst_tract",
+ "seq" : 1,
+ "sub_part" : 0
+ },
+ { "collation" : "ASC",
+ "name" : "lsst_patch",
+ "seq" : 2,
+ "sub_part" : 0
+ }
+ ]
+ },
+ }
+
+.. note::
+
+ - The second index ``idx_s_region_bounds`` is spatial. It's based on the binary column of which only
+ the first 32 bytes are indexed.
+
+ - The third index ``idx_lsst_tract_patch`` is defined over two columns.
diff --git a/doc/admin/director-index.rst b/doc/admin/director-index.rst
new file mode 100644
index 0000000000..cc8c9a21a5
--- /dev/null
+++ b/doc/admin/director-index.rst
@@ -0,0 +1,108 @@
+.. _admin-director-index:
+
+Director Index
+==============
+
+The *director* indexes in Qserv are optional metadata tables associated with the *director* tables, which are explained in:
+
+- :ref:`ingest-api-concepts-table-types` (CONCEPTS)
+
+Each row in the index table refers to the corresponding row in the related *director* table. The association is done via
+the unique identifier of rows in the *director* table. In additon to the unique identifier, the index table also contains
+the number of a chunk (column ``chunkId``) which contains the row in the *director* table. The index table is used to speed up the queries that
+use the primary keys of *director* tables to reference rows.
+
+Here is an example of the index table schema and the schema of the corresponding *director* table ``Object``:
+
+.. code-block:: sql
+
+ CREATE TABLE qservMeta.test101__Object (
+ objectId BIGINT NOT NULL,
+ chunkId INT NOT NULL,
+ subChunkId INT NOT NULL,
+ PRIMARY KEY (objectId)
+ );
+
+ CREATE TABLE test101.Object (
+ objectId BIGINT NOT NULL,
+ ..
+ );
+
+The index allows to speed up the following types of queries:
+
+- point queries (when an identifier is known)
+- ``JOIN`` queries (when the *director* table is used as a reference table by the dependent tables)
+
+Point queries can be executed without scanning all chunk tables of the *director* table. Once the chunk number is known,
+the query will be sent to the corresponding chunk table at a worker node where the table resides. For example,
+the following query can be several orders of magnitude faster with the index:
+
+.. code-block:: sql
+
+ SELECT * FROM test101.Object WHERE objectId = 123456;
+
+The index is optional. If the index table is not found in the Qserv Czar's database, queries will be executed
+by scanning all chunk tables of the *director* table.
+
+The index table can be built in two ways:
+
+- Automatically by the Qserv Replication/Ingest system during transaction commit time if the corresponding flag
+ was set as ``auto_build_secondary_index=1`` when calling the database registration service:
+
+ - :ref:`ingest-db-table-management-register-db` (REST)
+
+ .. note::
+
+ The index tables that are built automatically will be MySQL-partitioned. The partitioning is done
+ to speed up the index construction process and to benefit from using the distributed transactions
+ mechanism implemented in the Qserv Ingest system:
+
+ - :ref:`ingest-api-concepts-transactions` (CONCEPTS)
+
+ Having too many partitions in the index table can slow down user queries that use the index. Another side
+ effect of the partitions is an increased size of the table. The partitions can be consolidated at the database
+ *publishing* stage as described in the following section:
+
+ - :ref:`ingest-api-concepts-publishing-data` (CONCEPTS)
+
+- Manually, on the *published* databases using the following service:
+
+ - :ref:`ingest-director-index-build` (REST)
+
+ Note that the index tables built by this service will not be partitioned.
+
+The following example illustrates rebuilding the index of the *director* table ``Object`` that resides in
+the *published* database ``test101``:
+
+.. code-block:: bash
+
+ curl localhost:25081/ingest/index/secondary \
+ -X POST -H "Content-Type: application/json" \
+ -d '{"database":"test101", "director_table":"Object","rebuild":1,"local":1}'
+
+.. warning::
+
+ The index rebuilding process can be time-consuming and potentially affect the performance of user query processing
+ in Qserv. Depending on the size of the *director* table, the process can take from several minutes to several hours.
+ For *director* tables exceeding 1 billion rows, the process can be particularly lengthy.
+ It's recommended to perform the index rebuilding during a maintenance window or when the system load is low.
+
+Notes on the MySQL table engine configuration for the index
+-----------------------------------------------------------
+
+The current implementation of the Replication/Ingest system offers the following options for the implementation
+of index table:
+
+- ``innodb``: https://mariadb.com/kb/en/innodb/
+- ``myisam``: https://mariadb.com/kb/en/myisam-storage-engine/
+
+Each engine has its own pros and cons.
+
+The ``innodb`` engine is the default choice. The option is controlled by the following configuration parameter of the Master
+Replication Controller:
+
+- ``(controller,director-index-engine)``
+
+The parameter can be set via the command line when starting the controller:
+
+- ``--controller-director-index-engine=``
diff --git a/doc/admin/index.rst b/doc/admin/index.rst
index d9f6e3f44b..24bb3322b6 100644
--- a/doc/admin/index.rst
+++ b/doc/admin/index.rst
@@ -1,15 +1,14 @@
-.. warning::
- **Information in this guide is known to be outdated.** A documentation sprint is underway which will
- include updates and revisions to this guide.
+.. _admin:
-####################
-Administration Guide
-####################
+#####################
+Administrator's Guide
+#####################
.. toctree::
:maxdepth: 4
k8s
- qserv-ingest/index
- test-set
+ row-counters
+ data-table-indexes
+ director-index
diff --git a/doc/admin/qserv-ingest/index.rst b/doc/admin/qserv-ingest/index.rst
deleted file mode 100644
index 38cddd282e..0000000000
--- a/doc/admin/qserv-ingest/index.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-.. _installation-label:
-
-#########################
-The Qserv Ingest Workflow
-#########################
-
-.. toctree::
- :maxdepth: 2
-
- input-data
- version
- run
- repcli
- itest
-
diff --git a/doc/admin/row-counters.rst b/doc/admin/row-counters.rst
new file mode 100644
index 0000000000..04d9c975e7
--- /dev/null
+++ b/doc/admin/row-counters.rst
@@ -0,0 +1,176 @@
+
+.. _admin-row-counters:
+
+=========================
+Row counters optimization
+=========================
+
+.. _admin-row-counters-intro:
+
+Introduction
+------------
+
+Soon after the initial public deployment of Qserv, it was noticed that numerous users were executing the following query:
+
+.. code-block:: sql
+
+ SELECT COUNT(*) FROM .
+
+Typically, Qserv handles this query by distributing it to all workers, which then count the rows in each chunk table and aggregate the results
+at the Czar. This process is akin to the one used for *shared scan* (or simply *scan*) queries. The performance of these *scan* queries can
+fluctuate based on several factors:
+
+- The number of chunks in the target table
+- The number of workers available
+- The presence of other concurrent queries (particularly slower ones)
+
+In the best-case scenario, such a scan would take seconds; in the worst case, it could take many minutes or even hours.
+This has led to frustration among users, as this query appears to be (and indeed is) a very trivial non-scan query.
+
+To address this situation, Qserv includes a built-in optimization specifically for this type of query.
+Here's how it works: Qserv Czar maintains an optional metadata table for each data table, which stores the row count for each
+chunk. This metadata table is populated and managed by the Qserv Replication system. If the table is found, the query
+optimizer will use it to determine the number of rows in the table without the need to scan all the chunks.
+
+Note that this optimization is currently optional for the following reasons:
+
+- Collecting counters requires scanning all chunk tables, which can be time-consuming. Performing this during
+ the catalog *publishing* phase would extend the ingest time and increase the likelihood of workflow instabilities
+ (generally, the longer an operation takes, the higher the probability of encountering infrastructure-related failures).
+- The counters are not necessary for the data ingest process itself. They are merely optimizations for query performance.
+- Building the counters before the ingested data have been quality assured (Q&A-ed) may not be advisable.
+- The counters may need to be rebuilt if the data are modified (e.g., after making corrections to the ingested catalogs).
+
+The following sections provide detailed instructions on building, managing, and utilizing the row counters, along with formal
+descriptions of the corresponding REST services.
+
+.. note::
+
+ In the future, the per-chunk counters will be used for optimizing another class of unconditional queries
+ presented below:
+
+ .. code-block:: sql
+
+ SELECT * FROM .
LIMIT
+ SELECT `col`,`col2` FROM .
LIMIT
+
+ For these "indiscriminate" data probes, Qserv would dispatch chunk queries to a subset of random chunks that have enough
+ rows to satisfy the requirements specified in ``LIMIT ``.
+
+.. _admin-row-counters-build:
+
+Building and deploying
+----------------------
+
+.. warning::
+
+ Depending on the scale of a catalog (data size of the affected table), it may take a while before this operation
+ will be complete.
+
+.. note::
+
+ Please, be advised that the very same operation could be performed at the catalog publishing time as explained in:
+
+ - :ref:`ingest-db-table-management-publish-db` (REST)
+
+ The decision to perform this operation during catalog publishing or as a separate step, as described in this document,
+ is left to the discretion of Qserv administrators or developers of the ingest workflows. It is generally recommended
+ to make it a separate stage in the ingest workflow. This approach can expedite the overall transition time of a catalog
+ to its final published state. Ultimately, row counters optimization is optional and does not impact the core functionality
+ of Qserv or the query results presented to users.
+
+To build and deploy the counters, use the following REST service:
+
+- :ref:`ingest-row-counters-deploy` (REST)
+
+The service needs to be invoked for every table in the ingested catalog. Here is a typical example of using this service,
+which will work even if the same operation was performed previously:
+
+.. code-block:: bash
+
+ curl http://localhost:25080/ingest/table-stats \
+ -X POST -H "Content-Type: application/json" \
+ -d '{"database":"test101",
+ "table":"Object",
+ "overlap_selector":"CHUNK_AND_OVERLAP",
+ "force_rescan":1,
+ "row_counters_state_update_policy":"ENABLED",
+ "row_counters_deploy_at_qserv":1,
+ "auth_key":""}'
+
+This method is applicable to all table types: *director*, *dependent*, *ref-match*, or *regular* (fully replicated).
+If the counters already exist in the Replication system's database, they will be rescanned and redeployed.
+
+It is advisable to compare Qserv's performance for executing the aforementioned queries before and after running this operation.
+Typically, if the table statistics are available in Qserv, the result should be returned in a small fraction of
+a second (approximately 10 milliseconds) on a lightly loaded Qserv.
+
+.. _admin-row-counters-delete:
+
+Deleting
+--------
+
+In certain situations, such as when there is suspicion that the row counters were inaccurately scanned or during the quality
+assurance (Q&A) process of the ingested catalog, a data administrator might need to remove the counters and allow Qserv
+to perform a full table scan. This can be achieved using the following REST service:
+
+- :ref:`ingest-row-counters-delete` (REST)
+
+Similarly to the previously mentioned service, this one should also be invoked for each table requiring attention. Here is
+an example:
+
+.. code-block:: bash
+
+ curl http://localhost:25080/ingest/table-stats/test101/Object \
+ -X DELETE -H "Content-Type: application/json" \
+ -d '{"overlap_selector":"CHUNK_AND_OVERLAP","qserv_only":1,"auth_key":""}'
+
+Note that with the parameters shown above, the statistics will be removed from Qserv only.
+This means the system would not need to rescan the tables again if the statistics need to be rebuilt. The counters could simply
+be redeployed later at Qserv. To remove the counters from the Replication system's persistent state as well,
+the request should have ``qserv_only=0``.
+
+An alternative approach, detailed in the next section, is to instruct Qserv to bypass the counters for query optimization.
+
+
+.. _admin-row-counters-disable:
+
+Disabling the optimization at run-time
+---------------------------------------
+
+.. warning::
+
+ This is a global setting that affects all users of Qserv. All new queries will be run without the optimization.
+ It should be used with caution. Typically, it is intended for use by the Qserv data administrator to investigate
+ suspected issues with Qserv or the catalogs it serves.
+
+To complement the previously explained methods for scanning, deploying, or deleting row counters for query optimization,
+Qserv also supports a run-time switch. This switch can be turned on or off by submitting the following statements via
+the Qserv front-ends:
+
+.. code-block:: sql
+
+ SET GLOBAL QSERV_ROW_COUNTER_OPTIMIZATION = 1
+ SET GLOBAL QSERV_ROW_COUNTER_OPTIMIZATION = 0
+
+The default behavior of Qserv, when the variable is not set, is to enable the optimization for tables where the counters
+are available.
+
+.. _admin-row-counters-retrieve:
+
+Inspecting
+----------
+
+It's also possible to retrieve the counters from the Replication system's state using the following REST service:
+
+.. code-block:: bash
+
+ curl http://localhost:25080/ingest/table-stats/test101/Object \
+ -X GET -H "Content-Type: application/json" \
+ -d '{"auth_key":""}'
+
+- :ref:`ingest-row-counters-inspect` (REST)
+
+The retrieved information can be utilized for multiple purposes, including investigating potential issues with the counters,
+monitoring data distribution across chunks, or creating visual representations of chunk density maps. Refer to the REST service
+documentation for more details on this topic.
diff --git a/doc/admin/test-set.rst b/doc/admin/test-set.rst
deleted file mode 100644
index 8d8a31136a..0000000000
--- a/doc/admin/test-set.rst
+++ /dev/null
@@ -1,60 +0,0 @@
-*********
-Test sets
-*********
-
-.. warning::
- DEPRECATED
-
-Integration tests
-=================
-
-Once a Qserv mono-node instance is running, you can run advanced integration test on one dataset by using:
-
-.. code-block:: bash
-
- qserv-check-integration.py --load --case=01
- # See advanced options with --help option
-
-You can also run the whole integration test suite, with fixed parameters by using :
-
-.. code-block:: bash
-
- qserv-test-integration.py
- # See advanced options with --help option
-
-Results are stored in ``$QSERV_RUN_DIR/tmp/qservTest_case/outputs/``, and are erased before each run.
-
-Input data sets
----------------
-
-Directory structure
-^^^^^^^^^^^^^^^^^^^
-
-::
- case/
- README.txt - contains info about data
- queries/
- data/
-
.schema - contains schema info per table
-
.csv.gz - contains data``
-
-Database
-^^^^^^^^
-
-data from case will be loaded into databases called
- - ``qservTest_case_mysql``, for mysql
- - and ``LSST``, for qserv
-
-Query file format
-^^^^^^^^^^^^^^^^^
-
-Query file are named ``_.sql`` where ```` means :
- - ``0xxx`` supported, trivial (single object)
- - ``1xxx`` supported, simple (small area)
- - ``2xxx`` supported, medium difficulty (full scan)
- - ``3xxx`` supported, difficult /expensive (e.g. full sky joins)
- - ``4xxx`` supported, very difficult (eg near neighbor for large area)
- - ``8xxx`` queries with bad syntax. They can fail, but should not crash the server
- - ``9xxx`` unknown support
-
-Files that are not yet supported should have extension ``.FIXME``.
diff --git a/doc/conf.py b/doc/conf.py
index 13dcb17f0b..aded147ad6 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -42,6 +42,7 @@
r"^https://rubinobs.atlassian.net/wiki/",
r"^https://rubinobs.atlassian.net/browse/",
r"^https://www.slac.stanford.edu/",
+ r".*/_images/",
]
html_additional_pages = {
diff --git a/doc/dev/css.rst b/doc/dev/css.rst
deleted file mode 100644
index c8bf1a19c0..0000000000
--- a/doc/dev/css.rst
+++ /dev/null
@@ -1,68 +0,0 @@
-###################
-CSS metadata layout
-###################
-
-The Central State System (CSS) maintains information about the data stored in
-Qserv. Currently, this consists of the data schema: the names of databases and
-what tables they contain, the partitioning parameters used by the database, etc.
-
-**************
-Implementation
-**************
-
-CSS content is stored in a hierarchical key-value structure, with keys
-structured much like filename paths. In standard Qserv installations, CSS data
-is kept in a MySQL database, although parts of qserv may be tested with CSS
-content stored in a more feature-light form, e.g., a file.
-
-***********
-Packed keys
-***********
-
-In order to reduce the number of key-value updates when manipulating CSS
-content, some portions of the content tree are stored packed in JSON format.
-This is signified by presence of ".packed.json" key. The content of this key is
-a structure (JSON object) which contains some of the keys appearing at the same
-level as ".packed.json" key. For example, suppose CSS content includes
-(specified in python dict syntax):
-
-.. code-block:: python
-
- { "/foo/bar" : "12345",
- "/foo/bar/baz" : "regular data",
- "/foo/bar/.packed.json" : '{"a":"aa", "b":"bb"}' }
-
-The key "/foo/bar/.packed.json" is unpacked, thus the above content will be
-interpreted as:
-
-.. code-block:: python
-
- { "/foo/bar" : "12345",
- "/foo/bar/baz" : "regular data",
- "/foo/bar/a" : "aa",
- "/foo/bar/b" : "bb" }
-
-Note that the presence of "/foo/bar/.packed.json" does not prevent the presence
-of "/foo/bar/baz". It is not specified what happens if the same key appears in
-both regular CSS structure and in packed key value like in this structure:
-
-.. code-block:: python
-
- # it is unspecified which value /foo/bar/a has when unpacked
- { "/foo/bar" : "12345",
- "/foo/bar/a" : "regular key data",
- "/foo/bar/.packed.json" : '{"a":"aa", "b":"bb"}' }
-
-The values in JSON object can be simple types like strings or numbers, complex
-objects are not supported there. All values in JSON object are transformed to
-strings when unpacked, so the packed object ``"{"key": 1}"`` is treated
-identically to ``"{"key": "1"}"``.
-
-*************
-CSS interface
-*************
-
-CSS key-value storage is hidden completely behind CSS interface
-(``css/CssAccess`` class). This interface defines all logical operations on
-databases, tables, nodes, etc. and translates those operations into key-value
-structure manipulatons, including packing and upacking of the keys.
diff --git a/doc/dev/index.rst b/doc/dev/index.rst
index 1b5134fefd..0fc6b0cc50 100644
--- a/doc/dev/index.rst
+++ b/doc/dev/index.rst
@@ -1,18 +1,12 @@
-.. warning::
-
- **Information in this guide is known to be outdated.** A documentation sprint is underway which will
- include updates and revisions to this guide.
-
.. highlight:: sql
-###############
-Developer Guide
-###############
+#################
+Developer's Guide
+#################
.. toctree::
:maxdepth: 2
quick-start-devel
doc
- css
- wmgr-api
+ scisql
diff --git a/doc/dev/quick-start-devel.rst b/doc/dev/quick-start-devel.rst
index 4b0cd7b345..1bfdaabbe6 100644
--- a/doc/dev/quick-start-devel.rst
+++ b/doc/dev/quick-start-devel.rst
@@ -1,3 +1,7 @@
+.. warning::
+
+ Information in this guide is known to be outdated. The updated version will be available in the near future.
+
.. _quick-start-devel:
################################
diff --git a/doc/dev/scisql.rst b/doc/dev/scisql.rst
new file mode 100644
index 0000000000..d533bd2c43
--- /dev/null
+++ b/doc/dev/scisql.rst
@@ -0,0 +1,113 @@
+.. note::
+ The oficial documentation for the ``sciSQL`` project can be found at https://smonkewitz.github.io/scisql/
+
+======================
+sciSQL Developer Notes
+======================
+
+The document presents the development methodology for ``sciSQL`` in the context of the Qserv container environment.
+
+Making a build container
+------------------------
+
+The most straight-forward way to do this involves starting with a generic MariaDB distribution container,
+then layering in the development toolchain. Build and test then take place within this customized container.
+Here is an example of a small ``Dockerfile`` for this:
+
+.. code-block:: dockerfile
+
+ FROM mariadb:10.6
+ RUN apt-get update \
+ && apt-get install -y g++ git make libmariadb-dev python3 python3-pip vim \
+ && pip3 install future mako mysqlclient \
+ && update-alternatives --install /usr/bin/python python /usr/bin/python3 0
+ ENV MARIADB_ROOT_PASSWORD=CHANGEME
+ VOLUME /root
+
+With that file the desired image is built by:
+
+.. code-block:: bash
+
+ docker build -t scisql-dev - < Dockerfile
+
+The ``ENV`` and ``VOLUME`` lines of the ``Dockerfile`` file are for convenience when running the resulting container.
+The stock MariaDB container already has a ``VOLUME`` for ``/var/lib/sql``. Passing this and the additional ``VOLUME``
+for ``/root`` conveniently captures your development state in case the container crashes or you otherwise wish to restart it.
+
+Running the container
+---------------------
+
+To run the container built as above:
+
+.. code-block:: bash
+
+ docker run --name scisql-dev --init -d \
+ -v scisql-dev-data:/var/lib/mysql \
+ -v scisql-dev-home:/root scisql-dev
+
+You need to provide names for volumes holding MariaDB state and the root user home directory. This exact same command can be
+repeated to re-launch with preserved state (providing, e.g., you check out and build under ``/root`` within the container).
+
+Now the container will start. If it is a first run on a given data volume, it will take some tens of seconds for MariaDB
+to initialize. You can monitor docker logs on the container. When it is ready to go you will see "ready for connections"
+near the end of the log. There will be a running MariaDB server within this container, into which scisql can be installed
+and tested.
+
+At this point, it's recommended using a tool like ``VSCode``'s ``"connect to running container"`` to attach
+the IDE to the container. It can take ``VSCode`` a little while to download and install its server-side support within
+the container (another nice reason to have this persisted in the ``/root`` volume). You may wish to install a few niceties
+like your favorite ``.profile``, ssh keys for GitHub, etc. now in ``/root``.
+
+Building and testing sciSQL
+---------------------------
+
+Now, inside the container, clone out from ``github.com/smonkewitz/scisql``. Configure and build with:
+
+.. code-block:: bash
+
+ git clone https://github.com/smonkewitz/scisql.git
+ cd scisql
+ ./configure --mysql-includes=/usr/include/mariadb
+ make
+
+From here, the somewhat non-obvious iterated incantation to rebuild, deploy into the local MariaDB, and run the test
+suite is:
+
+.. code-block:: bash
+
+ make install && echo $MARIADB_ROOT_PASSWORD | PYTHONPATH=/usr/local/python \
+ scisql-deploy.py --mysql-dir=/usr \
+ --mysql-socket=/run/mysqld/mysqld.sock \
+ --mysql-plugin-dir=/lib/mysql/plugin
+
+If you don't want to undeploy/redeploy the UDFs and plugin, but are just iterating on the unit tests themselves,
+the following shortcut version is also useful:
+
+.. code-block:: bash
+
+ make install && echo $MARIADB_ROOT_PASSWORD | PYTHONPATH=/usr/local/python \
+ scisql-deploy.py --mysql-dir=/usr \
+ --mysql-socket=/run/mysqld/mysqld.sock \
+ --mysql-plugin-dir=/lib/mysql/plugin \
+ --test
+
+Updating the HTML documentation
+-------------------------------
+
+The HTML documentation is rendered by the Mako template library for Python: https://www.makotemplates.org from comments
+embedded in the sources and templates in ``tools/templates/``. Incantation to build is just:
+
+.. code-block:: bash
+
+ make html_docs
+
+If you are using ``VSCode``, you can get a tunneled live view on the documentation in your working tree by popping
+an additional terminal, and incanting:
+
+.. code-block:: bash
+
+ cd doc
+ exec python -m http.server
+
+Don't forget to add and commit the re-rendered documentation (under ``doc/``) on your PR. After a PR is merged to master,
+the documentation will automatically update on github pages at https://smonkewitz.github.io/scisql/
diff --git a/doc/dev/wmgr-api.rst b/doc/dev/wmgr-api.rst
deleted file mode 100644
index 27e6cbab62..0000000000
--- a/doc/dev/wmgr-api.rst
+++ /dev/null
@@ -1,1092 +0,0 @@
-Worker Manager API
-##################
-
-Overview
-********
-
-Worker manager is a service which runs alongside every qserv worker or czar and
-manages many common operations:
-
-* database operations (table creating, data loading, etc.)
-* certain xrootd (plug-in) operations
-* management of other services' lifetime
-
-Primary motivation for implementing wmgr service was to facilitate data loading
-in integration tests. It is likely that some of the wmgr functions may be
-changed in the future once we implement production-level services for data
-loading and distribution. For more details on wmgr design consult pages:
-
-* https://jira.lsstcorp.org/browse/DM-1900
-* https://dev.lsstcorp.org/trac/wiki/db/Qserv/WMGRDesign
-
-This document describes wmgr HTTP-basedAPI in details. Note that there is also a
-Python client library implemented on top of HTTP API.
-
-
-General API description
-***********************
-
-Wmgr API is implemented on top of HTTP protocol following RESTful principles
-(where possible). All communication with wmgr is performed over HTTP without
-encryption (implementing SSL/TLS is feasible but needs solution for certificate
-management).
-
-Wmgr can be configured to require one of few authentication mechanisms - none,
-basic, or digest. Lack of SSL/TLS dictates use digest authentication for
-production services.
-
-Whole wmgr API is split into three groups - database, xrootd, and process API.
-Below is description of individual API methods (actions).
-
-Return codes and content types
-==============================
-
-Responses generated by wmgr service use regular HTTP status codes to indicate
-success or error. Codes in range 200-299 are used for success, 400-499 for
-errors (range 500-599 typically means service failure).
-
-Normally responses generated by wmgr service include data in JSON format and
-have their ``Content-Type`` header set to ``application/json``. This applies to
-both successful completion and error conditions. In some cases when error
-condition is returned by the transport/framework layer it will have different
-content type.
-
-See also document which defines general structure of JSON objects for returned
-response data:
-https://confluence.lsstcorp.org/display/DM/REST+API+General+Guidelines
-
-
-Database API
-************
-
-This section contains description of actions related to database operations -
-creating/deleting databases and tables, loading table data, etc.
-
-
-``GET /dbs``
-============
-
- Return the list of all existing database names.
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for successful return
- * 500 - for database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a list of database objects with keys:
- * name: database name
- * uri: URL for database operations
-
- Request/response example::
-
- GET /dbs HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "results": [
- {
- "name": "qservMeta",
- "uri": "/dbs/qservMeta"
- },
- {
- "name": "qservResult",
- "uri": "/dbs/qservResult"
- }
- ]
- }
-
-``POST /dbs``
-=============
-
- Create new database. In addition to creating database itself this method
- also grants all privileges on this database to regular non-privileged
- account.
-
- Request headers:
- * Content-Type: required as ``multipart/form-data``
-
- Form Parameters:
- * db: Database name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 201 - if database was successfully created
- * 400 - if database name is missing
- * 409 - if database already exists
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a database object with keys:
- * name: database name
- * uri: URL for database operations
-
- Request/response example::
-
- POST /dbs HTTP/1.0
- Content-Type: multipart/form-data; boundary=------------------------bb306714c15713c2
-
- --------------------------bb306714c15713c2
- Content-Disposition: form-data; name="db"
-
- newDB
- --------------------------bb306714c15713c2--
-
- ::
-
- HTTP/1.0 201 CREATED
- Content-Type: application/json
-
- {
- "result": {
- "name": "newDB",
- "uri": "/dbs/newDB"
- }
- }
-
-``DELETE /dbs/``
-========================
-
- Deletes database.
-
- Parameters:
- * dbName: database name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - if database was successfully deleted
- * 400 - if parameters have invalid format
- * 404 - if database does not exist
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a database object with keys:
- * name: database name
- * uri: URL for database operations
-
- Request/response example::
-
- DELETE /dbs/newDB HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "name": "newDB",
- "uri": "/dbs/newDB"
- }
- }
-
-``GET /dbs//tables``
-============================
-
- Returns the list of tables in a database.
-
- Parameters:
- * dbName: database name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for successful return
- * 400 - if parameters have invalid format
- * 404 - if database does not exist
- * 500 - for database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a list of table objects with keys:
- * name: table name
- * uri: URL for database operations
-
- Request/response example::
-
- GET /dbs/qservMeta/tables HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "results": [
- {
- "name": "QCzar",
- "uri": "/dbs/qservMeta/tables/QCzar"
- },
- {
- "name": "QInfo",
- "uri": "/dbs/qservMeta/tables/QInfo"
- },
- ...
- ]
- }
-
-``POST /dbs//tables``
-=============================
-
- Create new table.
-
- If ``schemaSource`` (see below) is "request" then request must include
- ``schema`` parameter which is an SQL DDL statement starting with 'CREATE
- TABLE TableName ...'.
-
- If ``schemaSource`` is "css" then ``table`` parameter must be specified.
- Table schema will be extracted from CSS in this case, ``schemaSource`` must
- not be given.
-
- Parameters:
- * dbName: database name
-
- Request headers:
- * Content-Type: required as ``multipart/form-data``
-
- Form Parameters:
- * table: Table name
- * schemaSource: source for schema name, possible
- values: "request", "css", (default: "request")
- * schema: complete "CREATE TABLE ..." statement
- (optional)
- * chunkColumns: boolean flag, false by default,
- accepted values: '0', '1', 'yes', 'no', 'false', 'true'. If set
- to true then delete columns "_chunkId", "_subChunkId" from table
- (if they exist) and add columns "chunkId", "subChunkId" (if they
- don't exist)
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 201 - if table was successfully created
- * 400 - if parameters have invalid format or if form
- parameters are missing or conflicting
- * 409 - if table already exists
- * 500 - if table is not defined in CSS or other
- database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a table object with keys:
- * name: database name
- * uri: URL for database operations
-
- Request/response example::
-
- POST /dbs/newDB/tables HTTP/1.0
- Content-Type: multipart/form-data; boundary=------------------------c5c44964f0f9add0
-
- --------------------------c5c44964f0f9add0
- Content-Disposition: form-data; name="schema"
-
- CREATE TABLE newTable (I INT)
- --------------------------c5c44964f0f9add0
- Content-Disposition: form-data; name="table"
-
- newTable
- --------------------------c5c44964f0f9add0--
-
- ::
-
- HTTP/1.0 201 CREATED
- Content-Type: application/json
-
- {
- "result": {
- "name": "newTable",
- "uri": "/dbs/newDB/tables/newTable"
- }
- }
-
-``DELETE /dbs//tables/``
-=========================================
-
- Drop a table and optionally all chunk/overlap tables.
-
- Parameters:
- * dbName: database name
- * tblName: table name
-
- Query Parameters:
- * dropChunks: boolean flag, false by default, accepted
- values: '0', '1', 'yes', 'no', 'false', 'true'
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - if table was successfully deleted
- * 400 - if parameters have invalid format
- * 404 - if table does not exist
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a table object with keys:
- * name: database name
- * uri: URL for database operations
-
- Request/response example::
-
- DELETE /dbs/newDB/tables/newTable HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "name": "newTable",
- "uri": "/dbs/newDB/tables/newTable"
- }
- }
-
-
-``GET /dbs//tables//schema``
-=============================================
-
- Return result of SHOW CREATE TABLE statement for given table.
-
- Parameters:
- * dbName: database name
- * tblName: table name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for successful return
- * 400 - if parameters have invalid format
- * 404 - if table does not exist
- * 500 - for database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a string with resulting schema.
- * name: table name
- * uri: URL for database operations
-
- Request/response example::
-
- GET /dbs/newDB/tables/newTable/schema HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": "CREATE TABLE `newTable` (\n `I` int(11) DEFAULT NULL\n) ENGINE=MyISAM DEFAULT CHARSET=latin1"
- }
-
-``GET /dbs//tables//columns``
-==============================================
-
- Return result of SHOW COLUMNS statement for given table.
-
- Parameters:
- * dbName: database name
- * tblName: table name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for successful return
- * 400 - if parameters have invalid format
- * 404 - if table does not exist
- * 500 - for database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a list of column
- objects with keys: name, type, key, default, null
-
- Request/response example::
-
- GET /dbs/newDB/tables/newTable/columns HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "results": [
- {
- "default": null,
- "key": "",
- "name": "I",
- "null": "YES",
- "type": "int(11)"
- }
- ]
- }
-
-``GET /dbs//tables//chunks``
-=============================================
-
- Return the list of chunks in a table. For non-chunked table empty list
- is returned.
-
- Parameters:
- * dbName: database name
- * tblName: table name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for successful return
- * 400 - if parameters have invalid format
- * 404 - if table does not exist
- * 500 - for database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a list of chunk objects with keys:
- * chunkId: chunk number (integer)
- * chunkTable: true if chunk has regular chunk
- table (boolean)
- * overlapTable: true if chunk has overlap
- table (boolean)
- * uri: URL for chunk operations
-
- Request/response example::
-
- GET /dbs/qservTest_case01_qserv/tables/Object/chunks HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "results": [
- {
- "chunkId": 7648,
- "chunkTable": true,
- "overlapTable": true,
- "uri": "/dbs/qservTest_case01_qserv/tables/Object/chunks/7648"
- },
- ...
- ]
- }
-
-``POST /dbs//tables//chunks``
-==============================================
-
- Create new chunk.
-
- Parameters:
- * dbName: database name
- * tblName: table name
-
- Request headers:
- * Content-Type: required as ``multipart/form-data``
-
- Form Parameters:
- * chunkId: chunk ID, non-negative integer
- * overlapFlag: if true then create overlap table too
- (default is true), accepted values: '0', '1', 'yes', 'no',
- 'false', 'true'
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 201 - if chunk tables were successfully created
- * 400 - if parameters have invalid format or if form
- parameters are missing or conflicting
- * 404 - if table is missing
- * 409 - if chunk table already exists
- * 500 - if table is not defined in CSS or other
- database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a chunk object with keys:
- * chunkId: chunk number (integer)
- * chunkTable: true if chunk has regular chunk
- table (boolean)
- * overlapTable: true if chunk has overlap
- table (boolean)
- * uri: URL for chunk operations
-
- Request/response example::
-
- POST /dbs/newDB/tables/newTable/chunks HTTP/1.0
- Content-Type: multipart/form-data; boundary=------------------------df029da2ec8387ce
-
- --------------------------df029da2ec8387ce
- Content-Disposition: form-data; name="chunkId"
-
- 1000
- --------------------------df029da2ec8387ce--
-
- ::
-
- HTTP/1.0 201 CREATED
- Content-Type: application/json
-
- {
- "result": {
- "chunkId": 1000,
- "chunkTable": true,
- "overlapTable": true,
- "uri": "/dbs/newDB/tables/newTable/chunks/1000"
- }
- }
-
-``DELETE /dbs//tables//chunks/``
-==========================================================
-
- Delete chunk from a table, both chunk data and overlap data is dropped.
-
- Parameters:
- * dbName: database name
- * tblName: table name
- * chunkId: chunk number, non-negative integer
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - if table was successfully deleted
- * 400 - if parameters have invalid format
- * 404 - if table does not exist
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "result" property is a chunk object with keys:
- * chunkId: chunk number (integer)
- * chunkTable: true if chunk has regular chunk
- table (boolean)
- * overlapTable: true if chunk has overlap
- table (boolean)
- * uri: URL for chunk operations
-
- Request/response example::
-
- DELETE /dbs/newDB/tables/newTable/chunks/1000 HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "chunkId": 1000,
- "chunkTable": true,
- "overlapTable": true,
- "uri": "/dbs/newDB/tables/newTable/chunks/1000"
- }
- }
-
-``POST //tables//data``
-========================================
-
- Upload data into a table using file format supported by mysql command
- LOAD DATA [LOCAL] INFILE.
-
- Parameters:
- * dbName: database name
- * tblName: table name
-
- Request headers:
- * Content-Type: required as ``multipart/form-data``
-
- Form Parameters:
- * table-data: the data come in original LOAD DATA
- format with ``binary/octet-stream`` content type and binary
- encoding, and it may be compressed with gzip.
- * load-options: set of options encoded with usual
- ``application/x-www-form-urlencoded`` content type, options are:
- - delimiter - defaults to TAB
- - enclose - defaults to empty string (strings are not enclosed)
- - escape - defaults to backslash
- - terminate - defaults to newline
- - compressed - "0" or "1", by default is guessed from file extension (.gz)
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 201 - if chunk tables were successfully created
- * 400 - if parameters have invalid format or if form
- parameters are missing or conflicting
- * 404 - if table is missing
- * 409 - if chunk table already exists
- * 500 - if table is not defined in CSS or other
- database errors
-
- Response body (for successful completion):
- JSON object, "result" property is an object with keys:
- * status: string "OK"
- * count: count of rows added to a table
-
- Request/response example::
-
- POST /dbs/newDB/tables/newTable/data HTTP/1.0
- Content-Type: multipart/form-data; boundary=------------------------345ad77805210ac6
-
- --------------------------345ad77805210ac6
- Content-Disposition: form-data; name="table-data"; filename="table.dat.gz"
- Content-Type: application/octet-stream
-
- .....<.U..table.dat.3.2400.2.bS..;.......
-
- --------------------------345ad77805210ac6
- Content-Disposition: form-data; name="load-options"
-
- compressed=1&delimiter=%2C
- --------------------------345ad77805210ac6--
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "count": 4,
- "status": "OK"
- }
- }
-
-``POST //tables//chunks//data``
-=========================================================
-
- Upload data into a chunk table using file format supported by mysql
- command LOAD DATA [LOCAL] INFILE.
-
- This method works exactly as previous one taking the same form parameter
- but it loads data into a chunk and has additional URL parameter specifying
- chunk number.
-
-``POST //tables//chunks//overlap``
-============================================================
-
- Upload data into overlap table using file format supported by mysql
- command LOAD DATA [LOCAL] INFILE.
-
- This method works exactly as previous one taking the same form parameter
- but it loads data into an overlap table and has additional URL parameter
- specifying chunk number.
-
-``GET /dbs//tables//index``
-============================================
-
- Return index data (array of (objectId, chunkId, subChunkId) triplets).
-
- Parameters:
- * dbName: database name
- * tblName: table name
- * chunkId: chunk number (non-negative integer)
-
- Query Parameters:
- * columns: specifies comma-separated list of three
- column names. Default column names are "objectId", "chunkId",
- "subChunkId". Result returns columns in the same order as they
- are specified in 'columns' argument.
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for successful return
- * 400 - if parameters have invalid format
- * 404 - if table does not exist
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "result" property is an object with keys:
- * description: array of three objects
- describing columns, each with keys "name" (column name) and
- "type" (MySQL type name)
- * rows: array of arrays of integers
-
- Request/response example::
-
- GET /dbs/qservTest_case01_qserv/tables/Object/index HTTP/1.1
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "description": [
- {
- "name": "objectId",
- "type": "LONGLONG"
- },
- {
- "name": "chunkId",
- "type": "LONG"
- },
- {
- "name": "subChunkId",
- "type": "LONG"
- }
- ],
- "rows": [
- [
- 386937898687249,
- 6630,
- 897
- ],
- [
- 386942193651348,
- 6630,
- 660
- ],
- ...
- ]
- }
- }
-
-``GET /dbs//tables//chunks//index``
-=============================================================
-
- Return index data (array of (objectId, chunkId, subChunkId) triplets)
- for single chunk.
-
- Does the same as previous method but for one chunk from partitioned
- table. Useful when index for whole table may be too big.
-
- Request/response example::
-
- GET /dbs/qservTest_case01_qserv/tables/Object/chunks/7648/index HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "description": [
- {
- "name": "objectId",
- "type": "LONGLONG"
- },
- {
- "name": "chunkId",
- "type": "LONG"
- },
- {
- "name": "subChunkId",
- "type": "LONG"
- }
- ],
- "rows": [
- [
- 433306365599363,
- 7648,
- 5
- ],
- [
- 433314955527561,
- 7648,
- 10
- ],
- ...
- ]
- }
- }
-
-
-Xrootd API
-**********
-
-This section contains description of actions related to xrootd operations - e.g.
-publishing database via xrootd.
-
-
-``GET /xrootd/dbs``
-===================
-
- Return the list of databases known to xrootd.
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for success
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a list of database objects with keys:
- * name: database name
- * uri: URL for *xrootd* database operations
-
- Request/response example::
-
- GET /xrootd/dbs HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "results": [
- {
- "name": "qservTest_case01_qserv",
- "uri": "/xrootd/dbs/qservTest_case01_qserv"
- }
- ]
- }
-
-``POST /xrootd/dbs``
-====================
-
- Register new database in xrootd chunk inventory.
-
- Request headers:
- * Content-Type: required as ``multipart/form-data``
-
- Form Parameters:
- * db: database name (required)
- * xrootdRestart: if set to 'no' then do not restart
- xrootd (defaults to yes)
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 201 - if database was successfully registered
- * 400 - if parameters are missing or have invalid
- format
- * 409 - if database is already registered
- * 500 - on other database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a database object with keys:
- * name: database name
- * uri: URL for *xrootd* database operations
-
- Request/response example::
-
- POST /xrootd/dbs HTTP/1.0
- Content-Type: multipart/form-data; boundary=------------------------370e6e4d60b7499e
-
- --------------------------370e6e4d60b7499e
- Content-Disposition: form-data; name="db"
-
- newDB
- --------------------------370e6e4d60b7499e
- Content-Disposition: form-data; name="xrootdRestart"
-
- no
- --------------------------370e6e4d60b7499e--
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "name": "newDB",
- "uri": "/xrootd/dbs/newDB"
- }
- }
-
-``DELETE /xrootd/dbs/``
-===============================
-
- Unregister database from xrootd chunk inventory.
-
- Parameters:
- * dbName: database name
-
- Query Parameters:
- * xrootdRestart: if set to 'no' then do not restart
- xrootd (defaults to yes)
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for success
- * 400 - if parameters have invalid format
- * 409 - if database is not registered
- * 500 - for other database errors
-
- Response body (for successful completion):
- JSON object, "results" property is a database object with keys:
- * name: database name
- * uri: URL for *xrootd* database operations
-
- Request/response example::
-
- DELETE /xrootd/dbs/newDB?xrootdRestart=no HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "name": "newDB",
- "uri": "/xrootd/dbs/newDB"
- }
- }
-
-``GET /xrootd/dbs/``
-============================
-
- Return the list of chunk IDs in a database as known to xrootd.
-
- .. note:: Not implemented yet.
-
-
-Services API
-************
-
-This section contains description of actions related to operations on services -
-e.g. stopping and starting processes.
-
-
-``GET /services``
-=================
-
- Return the list of services.
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for success
-
- Response body (for successful completion):
- JSON object, "results" property is a list of service objects with keys:
- * name: service name
- * uri: URL for service operations
-
- Request/response example::
-
- GET /services HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "results": [
- {
- "name": "xrootd",
- "uri": "/services/xrootd"
- },
- {
- "name": "mysqld",
- "uri": "/services/mysqld"
- }
- ]
- }
-
-``GET /services/``
-===========================
-
- Return service state.
-
- Parameters:
- * service: service name
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for success
- * 404 - for invalid service name
-
- Response body (for successful completion):
- JSON object, "result" property is a service object with keys:
- * name: service name
- * state: one of "active" or "stopped"
- * uri: URL for service operations
-
- Request/response example::
-
- GET /services/mysqld HTTP/1.0
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "name": "mysqld",
- "state": "active",
- "uri": "/services/mysqld"
- }
- }
-
-``PUT /services/``
-===========================
-
- Execute some action on service, like "stop" or "restart".
-
- Parameters:
- * service: service name
-
- Request headers:
- * Content-Type: required as ``multipart/form-data``
-
- Form Parameters:
- * action: action: one of 'stop', 'start', 'restart'
- (required)
-
- Response headers:
- * Content-Type: ``application/json``
-
- Status Codes:
- * 200 - for success
- * 400 - if parameters are missing or have invalid
- format
- * 409 - if action has failed
-
- Response body (for successful completion):
- JSON object, "result" property is a service object with keys:
- * name: service name
- * state: state of service after action, one of
- "active" or "stopped"
- * uri: URL for service operations
-
- Request/response example::
-
- PUT /services/mysqld HTTP/1.0
- Content-Type: multipart/form-data; boundary=------------------------48169e483bc7d12e
-
- --------------------------48169e483bc7d12e
- Content-Disposition: form-data; name="action"
-
- restart
- --------------------------48169e483bc7d12e--
-
- ::
-
- HTTP/1.0 200 OK
- Content-Type: application/json
-
- {
- "result": {
- "name": "mysqld",
- "state": "active",
- "uri": "/services/mysqld"
- }
- }
diff --git a/doc/documenteer.toml b/doc/documenteer.toml
index 7ae39c6f41..811b923f11 100644
--- a/doc/documenteer.toml
+++ b/doc/documenteer.toml
@@ -1,5 +1,5 @@
[project]
title = "Qserv"
-copyright = "2015-2023 Association of Universities for Research in Astronomy, Inc. (AURA)"
+copyright = "2015-2024 Association of Universities for Research in Astronomy, Inc. (AURA)"
base_url = "https://qserv.lsst.io"
github_url = "https://github.com/lsst/qserv"
diff --git a/doc/index.rst b/doc/index.rst
index e0922133e7..aec51063d6 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -3,4 +3,5 @@
/intro/index
/admin/index
/user/index
+ /ingest/index
/dev/index
diff --git a/doc/ingest/api/advanced/async-concurrency.rst b/doc/ingest/api/advanced/async-concurrency.rst
new file mode 100644
index 0000000000..ec9722e101
--- /dev/null
+++ b/doc/ingest/api/advanced/async-concurrency.rst
@@ -0,0 +1,75 @@
+.. _ingest-api-advanced-concurrency:
+
+Concurrency control when processing async requests
+--------------------------------------------------
+
+.. note::
+
+ This optional mechanism is designed specifically for handling contribution requests submitted asynchronously by reference.
+ Workflows using the synchronous interface should implement their own request load balancing strategies.
+
+The current implementation of the worker request processor for *asynchronously* submitted contribution requests
+(:ref:`ingest-worker-contrib-by-ref`) follows a straightforward model:
+
+- Incoming requests are managed by a single input queue.
+- Requests are queued based on their creation timestamp.
+- At server startup, a fixed-size pool of processing threads is initialized.
+- The pool size is configured by the worker's parameter ``(worker,num-async-loader-processing-threads)``, which can be set
+ at startup using the command line option ``--worker-num-async-loader-processing-threads=``. By default, this value
+ is twice the number of hardware threads on the worker host.
+- Worker threads process requests sequentially from the queue's head.
+
+This model can cause issues in some deployments where resource availability is limited, such as:
+
+- Remote data sources (e.g., HTTP servers, object stores) may become overloaded if the total number of parallel requests from all
+ workers exceeds the service capacity. For instance, disk I/O performance may degrade when services read too many files simultaneously.
+- The performance of locally mounted distributed filesystems at workers may degrade if there are too many simultaneous file
+ reads, especially when input data is located on such filesystems.
+- Ongoing ingest activities can significantly degrade Qserv performance for user queries due to resource contention (memory, disk I/O, network I/O, CPU).
+- The timing of ingests can be problematic. For instance, massive ingests might be scheduled at night, while less intensive
+ activities occur during the day when user activity is higher.
+
+Adjusting the number of processing threads in the service configuration is not an optimal solution because it requires restarting
+all worker servers (or the entire Qserv in Kubernetes-based deployments) whenever the ingest workflow needs to manage resource usage.
+Additionally, the constraints can vary based on the specific context in both "space" (ingesting particular databases from specific sources)
+and "time" (when Qserv is under heavy load from user queries).
+
+To mitigate these issues, the API provides a feature to control the concurrency level of processed requests. Limits can be configured
+at the database level. Workflows can query or set these limits using the existing REST services, as detailed in the following section:
+
+- :ref:`ingest-config` (REST)
+
+Here is an example of how to configure all workers to limit concurrency to a maximum of 4 requests per worker for
+the database ``test101``:
+
+.. code-block:: bash
+
+ curl http://localhost:25081/ingest/config \
+ -X PUT -H 'Content-Type: application/json' \
+ -d'{"database":"test101","ASYNC_PROC_LIMIT":4,"auth_key":""}'
+
+Specifying a value of ``0`` will remove the concurrency limit, causing the system to revert to using the default number of processing threads.
+
+Workflows can modify the limit at any time, and changes will take effect immediately. However, the new limit will only
+apply to requests that are pulled from the queue after the change. Existing requests in progress will not be interrupted,
+even if the limit is reduced.
+
+The following example demonstrates how to use the related service to retrieve the current concurrency limit for a specific database:
+
+.. code-block:: bash
+
+ curl 'http://localhost:25081/ingest/config?database=test101' -X GET
+
+This would return:
+
+.. code-block:: json
+
+ { "config": {
+ "ASYNC_PROC_LIMIT": 4,
+ "database": "test101"
+ },
+ "error": "",
+ "error_ext": {},
+ "success": 1,
+ "warning": "No version number was provided in the request's query."
+ }
diff --git a/doc/ingest/api/advanced/charset.rst b/doc/ingest/api/advanced/charset.rst
new file mode 100644
index 0000000000..f0dacccef7
--- /dev/null
+++ b/doc/ingest/api/advanced/charset.rst
@@ -0,0 +1,93 @@
+.. _ingest-api-advanced-charset:
+
+Character sets in contributions
+-------------------------------
+
+.. note::
+
+ This feature was added in the REST API version ``15``.
+
+Background
+^^^^^^^^^^
+
+The current implementation of the Qserv Replication/Ingest system relies on the following SQL statement for ingesting
+table data:
+
+.. code-block:: sql
+
+ LOAD DATA [LOCAL] INFILE ...
+
+According to the MySQL/MariaDB documentation https://mariadb.com/kb/en/load-data-infile/#character-sets), the database
+server may interpret or transform the input data (CSV) differently depending on the character set assumed by the operation.
+Incorrect data transformation can result in distorted data being stored in the target table. This issue is most likely
+to occur when the input data were produced from another MySQL server using the following SQL statement:
+
+.. code-block:: sql
+
+ SELECT INTO OUTFILE ...
+
+The right approach to address this situation is twofold:
+
+- Know (or configure) a specific character set when producing the data files (CSV).
+- Use the same character set when loading these files into Qserv.
+
+Before version ``15`` of the API, the Qserv Ingest System did not provide any control for the latter. The system relied on
+the default character set in the database service configuration. This could lead to the following problems:
+
+- The character set configured in this way may be random, as it may be set either by a database administrator who might not
+ be aware of the problem (or the origin and parameters of the input data to be loaded into Qserv). Besides, the default
+ character set may change between MySQL/MariaDB versions, or it could depend on the choice of the OS (Debian, CentOS, etc.)
+ for the base Docker image.
+
+- Different input data may be produced with different character sets. In this case, setting one globally at the database
+ level (even if this could be done deterministically) wouldn't work for all inputs.
+
+As of version ``15``:
+
+- The implementation is reinforced to assume a specific default character set for the data loading operation. The default
+ is presently set to ``latin1``.
+- The ingest API is extended to allow overriding the default character set when loading contributions into Qserv.
+
+Global configuration
+^^^^^^^^^^^^^^^^^^^^
+
+.. warning::
+
+ Setting the server-side configuration parameters of the Ingest system is not supposed to be under the direct control of
+ the workflow developers. Managing Qserv deployments and the configuration of the Ingest system is the responsibility of
+ the Qserv administrators. Therefore, the workflow developers are advised to set the name of the desired character set in
+ each contribution request as explained in the subsections below.
+
+The configuration of the workers now includes the following parameter ``(worker,ingest-charset-name)``. The parameter can
+be set at the server startup via the command line option ``--worker-ingest-charset-name=``. The default value is
+``latin1``.
+
+In the REST API
+^^^^^^^^^^^^^^^
+
+Overriding the default character set value is supported by all forms of contribution ingest services, whether contributions
+are submitted *by reference* or *by value*, and whether they are *synchronous* or *asynchronous* requests.
+
+The desired character set value is specified via the ``charset_name`` parameter. This parameter should be a string representing
+the name of the character set. It is optional; if not provided, the Ingest system will use its default character set value.
+
+All services that return the status of contribution requests will include the character set name used by the Ingest system when
+processing the contributions. This information is reported in the JSON response object as:
+
+.. code-block::
+
+ { "charset_name":
+ }
+
+
+qserv-replica-file
+^^^^^^^^^^^^^^^^^^
+
+The command line tool :ref:`ingest-tools-qserv-replica-file` allows ingesting contributions via the proprietary binary protocol
+and has an option ``--charset-name=``:
+
+.. code-block:: bash
+
+ qserv-replica-file INGEST {FILE|FILE-LIST|FILE-LIST-TRANS} \
+ ... \
+ --charset-name=
diff --git a/doc/ingest/api/advanced/config.rst b/doc/ingest/api/advanced/config.rst
new file mode 100644
index 0000000000..dbccf73099
--- /dev/null
+++ b/doc/ingest/api/advanced/config.rst
@@ -0,0 +1,105 @@
+.. _ingest-api-advanced-global-config:
+
+Global configuration options
+----------------------------
+
+This section presents the configuration parameters of Qserv and the Replication/Ingest system which may
+affect the ingest activities. This section provides instructions for finding values of the parameters
+via the API, explains roles of the parameters, and outlines possble usage of the parameters by the workflow.
+
+The number of workers
+^^^^^^^^^^^^^^^^^^^^^
+
+The total number of Qserv workers is the main factor contributing to the performance of the catalog ingest operations.
+The number can be obtained using the following REST service:
+
+- :ref:`ingest-config-global-workers` (REST)
+
+The workflow needs to analyze a section ``config.workers`` to select workers in the following state (both apply):
+
+- ``is-enabled=1``
+- ``is-read-only=0``
+
+There are a few possibilities how the workflow could use this information. For example, the workflow
+could start a separate transaction (or a set of transactions) per worker.
+
+Worker-specific parameters
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The second group of parameters found in the section ``config.general.worker`` is related to resources which
+are available to the individual worker ingest services for processing contributions. These are instructions
+for some of the most relevant parameters:
+
+- ``num-loader-processing-threads``:
+
+ The parameter affects a flow of ingest requests made via the proprietary binary protocol using the command-line
+ tool :ref:`ingest-tools-qserv-replica-file`. To achieve the maximum throughput of the ingest the workflows
+ should aim at having each participated worker loaded with as many parallel requests as there are threads
+ reported by this parameter.
+
+ .. warning::
+
+ Exceeding the number of threads will result in having clients to wait for connections to be established
+ with the workers. In some cases this may lead to the performance degradation if the network connection
+ is unstable.
+
+- ``num-http-loader-processing-threads``:
+
+ The parameter affects a flow of ingest requests submitted via the HTTP-based ingest service. Note that
+ this service is used for processing *synchronous* contribution requests and for submitting the *asynchronous*
+ requests to the service.
+
+ The workflow may use a value of the parameter differently, depenidng on a type of the contribution request.
+ Requests which are *synchronous* should be submitted to the service in a way that the number of such requests
+ per worker was close to the number of threads reported by this parameter. In this case the workflow should
+ expect the maximum throughput of the ingest. The *asynchronous* requests aren't affected by the parameter,
+ unless another workflow is using the service in the *synchronous* mode at the same time.
+
+- ``num-async-loader-processing-threads``:
+
+ The parameter represents the number of the request processing threads in a thread pool allocated for
+ the *asynchronous* contribution requests. The workflow should aim at having the number of unfinished
+ *asynchronous* requests submitted to the service close to (or exceeding) the number of threads reported
+ by this parameter. To do so, the workflow should monitor the number and the status of the *asynchronous*
+ requests at each worker and submit new requests to the service when the number of the requests being processed
+ is less than the number of threads. The relevant services for monitoring the contribution requests at workers
+ are documented in:
+
+ - :ref:`ingest-worker-contrib-get` (WORKER)
+
+ .. note::
+
+ An alternative approach is to submit all *asynchronous* requests to the relevant worker services at once.
+ The services will take care of processing the requests in the same order they were submitted. This approach
+ may not work well where a specific order of the requests is important, or if all input data is not available
+ at the time of the submission.
+
+- ``ingest-charset-name``:
+
+ The name of a character set for parsing the payload of the contributions. The workflow may override the default
+ value of the parameter if the payload of the contributions is encoded in a different character set. See an
+ attrubute ``charset_name`` in:
+
+ - :ref:`ingest-worker-contrib-by-ref` (WORKER)
+ - :ref:`ingest-worker-contrib-by-val` (WORKER)
+
+- ``ingest-num-retries``, ``ingest-max-retries``:
+
+ These parameters are related to the number of the automatic retries of the failed *asynchronous* requests
+ specific in the parameter ``num_retries`` of the contribution request. The workflow may adjust the number
+ of such retries if needed. A good example is when the workflow knows that a connection to the data source
+ (a Web server or the object store) is unstable, or if the server might be overloaded. The workflow may increase
+ the number of retries to ensure that the data is ingested successfully.
+
+ .. note::
+
+ The parameter ``ingest-max-retries`` is a hard limit for the number of retries regardless of what's
+ specified in the request's attribute ``num_retries``.
+
+- ``loader-max-warnings``:
+
+ This parameter sets the default number for the number of warnings that the worker ingest service can
+ capture from MySQL after attempting to ingest a contribution. The workflow may adjust the parameter
+ for individual contributions by setting the desired limit in the request's attribute ``max_warnings``.
+ The main purpose for setting the limit higher than the default value is to debug problem with the
+ data of the contributions.
diff --git a/doc/ingest/api/advanced/contributions.rst b/doc/ingest/api/advanced/contributions.rst
new file mode 100644
index 0000000000..21fad852f2
--- /dev/null
+++ b/doc/ingest/api/advanced/contributions.rst
@@ -0,0 +1,51 @@
+.. _ingest-api-advanced-contributions:
+
+Options for making contribution requests
+----------------------------------------
+
+The API provides a variety of options for making contribution requests. The choice of the most suitable method depends on
+the specific needs of the client application. The following list outlines the available ingest modes:
+
+- **pull**: Tell the ingest service to pull the data from the Web server:
+
+ - :ref:`ingest-worker-contrib-by-ref` (WORKER)
+
+- **read**: Tell the ingest service to read the data directly from the locally mounted filesystem that is accessible
+ to the worker:
+
+ - :ref:`ingest-worker-contrib-by-ref` (WORKER)
+
+- **push**: Send the data over the proprietary binary protocol or ``http`` protocol to the ingest service:
+
+ - :ref:`ingest-tools-qserv-replica-file` (TOOLS)
+ - :ref:`ingest-worker-contrib-by-val` (WORKER)
+
+ The workflow can either read the data from the local filesystem or access the data directly from memory.
+
+All methods support the ``CSV`` data format. Additionally, the **push** mode over the ``http`` protocol also supports
+the ``JSON`` format. For more details, refer to the documentation of the respective service.
+
+The **pull** and **read** modes also support both *synchronous* and *asynchronous* data ingestion approaches.
+
+The following diagrams illustrate the three modes of making contribution requests:
+
+Pulling data from the Web server
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: /_static/ingest-options-pull.png
+ :target: ../../../_images/ingest-options-pull.png
+ :alt: Pull Mode
+
+Reading data from the local filesystem of the worker
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: /_static/ingest-options-read.png
+ :target: ../../../_images/ingest-options-read.png
+ :alt: Read Mode
+
+Pushing data from the workflow to the service
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. image:: /_static/ingest-options-push.png
+ :target: ../../../_images/ingest-options-push.png
+ :alt: Read Mode
diff --git a/doc/ingest/api/advanced/directors.rst b/doc/ingest/api/advanced/directors.rst
new file mode 100644
index 0000000000..0e0efc82bc
--- /dev/null
+++ b/doc/ingest/api/advanced/directors.rst
@@ -0,0 +1,162 @@
+
+.. _ingest-api-advanced-directors:
+
+Databases with many director tables
+===================================
+
+.. tip::
+
+ See the following document first:
+
+ - :ref:`ingest-api-concepts-table-types` (CONCEPTS)
+
+The API supports ingesting multiple *director* tables within a single catalog. Each *director* table can optionally have its
+own set of *dependent* tables. This section demonstrates the necessary configuration for the following table set:
+
+.. table::
+
+ +----------+-----------+
+ | director | dependent |
+ +==========+===========+
+ | dir_1 | dep_1_1 |
+ | +-----------+
+ | | dep_1_2 |
+ | +-----------+
+ | | dep_1_3 |
+ +----------+-----------+
+ | dir_2 | dep_2_1 |
+ | +-----------+
+ | | dep_2_2 |
+ +----------+-----------+
+ | dir_3 | |
+ +----------+-----------+
+
+In this example, there are 3 *director* tables. Each (except the third one ``dir_3``) has its own set of *dependent* tables.
+The key attributes that govern dependencies between the tables are specified in the section :ref:`ingest-db-table-management-register-table`
+(REST). The document mentions the following required attributes that need to be provided in the JSON specification of
+each table:
+
+.. table::
+
+ +--------------------+----------+-----------------------------------------------------------------------------------------------------+
+ | attribute | value | comment |
+ +====================+==========+=====================================================================================================+
+ | ``is_partitioned`` | ``1`` | Same value for both *director* and *dependent* tables. |
+ +--------------------+----------+-----------------------------------------------------------------------------------------------------+
+ | ``director_table`` | *string* | Where: |
+ | | | |
+ | | | - *director* tables should have the empty string here. |
+ | | | - *dependent* tables should have the name of the corresponding *director* table here. |
+ +--------------------+----------+-----------------------------------------------------------------------------------------------------+
+ | ``director_key`` | *string* | The non-empty string specifying the name of the corresponding column must be |
+ | | | provided here. Depending on the type of the table, this name corresponds to either: |
+ | | | |
+ | | | - The *primary key* in the *director* table, or |
+ | | | - The *foreign key* pointing to the corresponding director's primary key in the *dependent* tables. |
+ +--------------------+----------+-----------------------------------------------------------------------------------------------------+
+
+The *director* tables are **required** to have the following attributes:
+
+.. table::
+
+ +-------------------+----------+---------------------------------------------------------------------------+
+ | attribute | value | comment |
+ +===================+==========+===========================================================================+
+ | ``latitude_key`` | *string* | The names of the director table's columns that were used for partitioning |
+ | ``longitude_key`` | | the table data into chunks. |
+ +-------------------+----------+---------------------------------------------------------------------------+
+
+The *dependent* tables may also include the attributes ``latitude_key`` and ``longitude_key`` if their input data was partitioned using the columns specified
+by these attributes. If not, these attributes can be omitted from the table's JSON specification.
+
+The following table illustrates how JSON configurations for all the above-mentioned tables might look like
+(the examples were simplified for clarity):
+
+.. table::
+
+ +-----------------------------------+-------------------------------------------+
+ | director | dependents |
+ +===================================+===========================================+
+ | .. code-block:: json | .. code-block:: json |
+ | | |
+ | { "table" : "dir_1", | { "table" : "dep_1_1", |
+ | "is_partitioned" : 1, | "is_partitioned" : 1, |
+ | "director_table" : "", | "director_table" : "dir_1", |
+ | "director_key" : "objectId", | "director_key" : "dep_objectId" |
+ | "latitude_key" : "ra", | } |
+ | "longitude_key" : "dec" | |
+ | } | **Note**: Attributes ``latitude_key`` and |
+ | | ``longitude_key`` were not provided. |
+ | | is allowed for the dependent tables. |
+ | | |
+ | | .. code-block:: json |
+ | | |
+ | | { "table" : "dep_1_2", |
+ | | "is_partitioned" : 1, |
+ | | "director_table" : "dir_1", |
+ | | "director_key" : "dep_objectId" |
+ | | "latitude_key" : "", |
+ | | "longitude_key" : "" |
+ | | } |
+ | | |
+ | | **Note**: Attributes ``latitude_key`` and |
+ | | ``longitude_key`` were provided. However |
+ | | the values were empty strings, which is |
+ | | allowed for the dependent tables. |
+ | | |
+ | | .. code-block:: json |
+ | | |
+ | | { "table" : "dep_1_3", |
+ | | "is_partitioned" : 1, |
+ | | "director_table" : "dir_1", |
+ | | "director_key" : "dep_objectId" |
+ | | "latitude_key" : "dep_ra", |
+ | | "longitude_key" : "dep_dec" |
+ | | } |
+ +-----------------------------------+-------------------------------------------+
+ | .. code-block:: json | .. code-block:: json |
+ | | |
+ | { "table" : "dir_2", | { "table" : "dep_2_1", |
+ | "is_partitioned" : 1, | "is_partitioned" : 1, |
+ | "director_table" : "", | "director_table" : "dir_2", |
+ | "director_key" : "id", | "director_key" : "dep_id" |
+ | "latitude_key" : "coord_ra", | } |
+ | "longitude_key" : "coord_dec" | |
+ | } | .. code-block:: json |
+ | | |
+ | | { "table" : "dep_2_1", |
+ | | "is_partitioned" : 1, |
+ | | "director_table" : "dir_2", |
+ | | "director_key" : "dep_id" |
+ | | "latitude_key" : "dep_coord_ra", |
+ | | "longitude_key" : "dep_coord_dec" |
+ | | } |
+ +-----------------------------------+-------------------------------------------+
+ | .. code-block:: json | No dependents for the *director* table |
+ | | |
+ | { "table" : "dir_3", | |
+ | "is_partitioned" : 1, | |
+ | "director_table" : "", | |
+ | "director_key" : "objectId", | |
+ | "latitude_key" : "ra", | |
+ | "longitude_key" : "dec" | |
+ | } | |
+ +-----------------------------------+-------------------------------------------+
+
+.. note::
+
+ The attributes ``chunk_id_key`` and ``sub_chunk_id_key`` were required in older versions of the API and may still
+ be present in JSON configurations. However, they are no longer needed for registering tables during ingest.
+ The role-to-column mapping for these attributes is now predefined in the Ingest system implementation.
+ The mapping is presented below:
+
+ +----------------------+----------------+
+ | role | column |
+ +======================+================+
+ | ``chunk_id_key`` | ``chunkId`` |
+ +----------------------+----------------+
+ | ``sub_chunk_id_key`` | ``subChunkId`` |
+ +----------------------+----------------+
+
+ If any of these attributes are found in a configuration, their definitions will be ignored.
+
diff --git a/doc/ingest/api/advanced/index.rst b/doc/ingest/api/advanced/index.rst
new file mode 100644
index 0000000000..fca32c2e3d
--- /dev/null
+++ b/doc/ingest/api/advanced/index.rst
@@ -0,0 +1,26 @@
+
+.. _ingest-api-advanced:
+
+==================
+Advanced Scenarios
+==================
+
+.. hint::
+
+ Read the following document first:
+
+ - :ref:`ingest-api-concepts` (CONCEPTS)
+
+.. toctree::
+ :maxdepth: 4
+
+ config
+ charset
+ async-concurrency
+ unpublishing
+ transactions
+ optimisations
+ contributions
+ directors
+ ref-match
+ warnings
diff --git a/doc/ingest/api/advanced/optimisations.rst b/doc/ingest/api/advanced/optimisations.rst
new file mode 100644
index 0000000000..b803546e91
--- /dev/null
+++ b/doc/ingest/api/advanced/optimisations.rst
@@ -0,0 +1,59 @@
+
+.. _ingest-api-advanced-optimisations:
+
+Optimizations in using the REST services
+========================================
+
+When designing a workflow, it is crucial to avoid overloading the REST services with repeated or inefficient requests.
+Whenever possible, make certain requests once and reuse their results. This is particularly important for workflows
+designed for parallel ingests, where the results of some requests can be shared among parallel
+activities (processes, etc.) within the workflows.
+
+While this document does not cover all possible optimizations for interacting with the services, it is
+the responsibility of the workflow developer to determine what can be cached or shared based on
+the progressive state of the ingested catalog and the organization of the workflow
+Below are some of the most useful techniques.
+
+.. _ingest-api-advanced-optimisations-batch:
+
+Batch mode for allocating chunks
+--------------------------------
+
+.. note::
+
+ This optimization is feasible when all chunk numbers are known upfront.
+ A common scenario is when the workflow is ingesting a large dataset that has already been
+ *partitioned* into chunks. In such cases, the chunk numbers are known before the ingestion begins.
+
+In the example of the :ref:`ingest-api-simple` presented earlier, chunk allocations were made on a per-chunk basis
+(:ref:`table-location-chunks-one`). While this method works well for scenarios with a small number of chunks, it may
+slow down the performance of workflows ingesting large numbers of chunks or making numerous requests to the chunk
+allocation service. This is because chunk allocation operations can be expensive, especially in a Qserv setup with
+many pre-deployed chunks. In such cases, chunk allocation requests may take a significant amount of time.
+To address this issue, the system provides a service for allocating batches of chunks, as explained in:
+
+- :ref:`table-location-chunks-many` (REST)
+
+In the context of the earlier presented example of a simple workflow the chunk allocation request object would
+look like this:
+
+.. code-block:: json
+
+ { "transaction_id" : 123,
+ "chunks" : [ 187107, 187108, 187109, 187110 ]
+ }
+
+The result could be reported as:
+
+.. code-block:: json
+
+ { "location":[
+ { "chunk":187107, "worker":"db01", "host":"qserv-db01", "port":25002 },
+ { "chunk":187108, "worker":"db02", "host":"qserv-db02", "port":25002 },
+ { "chunk":187109, "worker":"db01", "host":"qserv-db01", "port":25002 },
+ { "chunk":187110, "worker":"db02", "host":"qserv-db02", "port":25002 }
+ ]
+ }
+
+The request can be made once, and its results can be distributed among parallel activities within the workflow
+to ingest the corresponding chunk contributions.
diff --git a/doc/ingest/api/advanced/ref-match.rst b/doc/ingest/api/advanced/ref-match.rst
new file mode 100644
index 0000000000..b5f5951e57
--- /dev/null
+++ b/doc/ingest/api/advanced/ref-match.rst
@@ -0,0 +1,94 @@
+.. _ingest-api-advanced-refmatch:
+
+Ingesting ref-match tables
+==========================
+
+.. tip::
+
+ See the following document first:
+
+ - :ref:`ingest-api-concepts-table-types` (CONCEPTS)
+
+.. note::
+
+ The input data for *ref-match* tables must be partitioned differently than other subtypes of *partitioned* tables.
+ Detailed instructions on this topic can be found in the section:
+
+ - :ref:`ingest-data-partitioning-ref-match` (DATA)
+
+The *ref-match* tables are a specialized class of *partitioned* tables that depend on (match rows of) two *director* tables.
+These referenced *director* tables can be located within the same catalog as the *ref-match* table or in any other catalog
+served by the same Qserv instance. The only additional requirement in the latter case is that all databases must belong to
+the same database *family* (partitioned using the same values for the parameters ``stripes``, ``sub-stripes``, and ``overlap``).
+This requirement is enforced by the table registration service of the Replication/Ingest system. From the system's perspective,
+these tables are not different from any other *partitioned* tables. The only changes made to the table registration interface
+to specifically support *ref-match* tables are redefining (extending) the syntax of the attribute ``director_table`` and adding
+four optional attributes allowed in the JSON configurations of the tables as presented below:
+
+``director_table`` : *string*
+ A table referenced here must be the **first** *director* table that must be registered in Qserv before the *ref-match* table.
+ The table registration service will refuse the operation if the *director* doesn't exist. The table name may also include
+ the name of a database where the table is located if this database differs from the one where the *ref-match* itself
+ will be placed. The syntax of the parameter's value:
+
+ .. code-block::
+
+ [.]
+
+ Note that the name cannot be empty, and the database (if specified) or table names should not be enclosed in quotes.
+
+ If the database name is provided, the database should already be known to the Replication/Ingest system.
+
+``director_key`` : *string*
+ A non-empty string specifying the name of the primary key column of the referenced *director* table must be provided here.
+ This column should also be present in the table schema.
+
+``director_table2`` : *string*
+ This is the **second** *director* table referenced by the *ref-match* table. The values for this attribute must adhere to the same
+ requirements and restrictions as those specified for the ``director_table`` attribute.
+
+``director_key2`` : *string*
+ A non-empty string specifying the name of the primary key column of the **second** referenced *director* table must be provided here.
+ This column should also be present in the table schema. Note that the name should be different from the one specified in
+ the ``director_key`` attribute.
+
+``flag`` : *string*
+ The name of a column that stores flags created by the special partitioning tool ``sph-partition-matches``. This column should
+ also be present in the table schema. Usually, the column has the SQL type ``UNSIGNED INT``.
+
+``ang_sep`` : *double*
+ The maximum angular separation (within the spatial coordinate system) between the matched objects. The value of this parameter
+ must be strictly greater than ``0`` and must not exceed the *overlap* value of the database *family*. The table registration service
+ will enforce this requirement and refuse to register the table if the condition is violated.
+
+.. note::
+
+ Spatial coordinate columns ``latitude_key`` and ``longitude_key`` are ignored for this class of tables.
+
+Here is an example of the JSON configuration for a *ref-match* table:
+
+.. code-block:: json
+
+ { "database" : "Catalog-A",
+ "table" : "RefMatch_A_Object_B_DeepSource",
+ "is_partitioned" : 1,
+ "director_table" : "Object",
+ "director_key" : "objectId",
+ "director_table2" : "Catalog-B.DeepSource",
+ "director_key2" : "deepSourceId",
+ "flag" : "flags",
+ "ang_sep" : 0.01667,
+ "schema": [
+ {"name" : "objectId", "type" : "BIGINT UNSIGNED"},
+ {"name" : "deepSourceId", "type" : "BIGINT UNSIGNED"},
+ {"name" : "flag", "type" : "INT UNSIGNED"},
+ {"name" : "chunkId", "type" : "INT UNSIGNED"},
+ {"name" : "subChunkId", "type" : "INT UNSIGNED"}
+ ],
+ "auth_key" : ""
+ }
+
+The configuration parameters of the *ref-match* tables can also be seen in the responses of the following REST services:
+
+- :ref:`ingest-db-table-management-register-table` (REST)
+- :ref:`ingest-db-table-management-config` (REST)
diff --git a/doc/ingest/api/advanced/transactions.rst b/doc/ingest/api/advanced/transactions.rst
new file mode 100644
index 0000000000..07291839c5
--- /dev/null
+++ b/doc/ingest/api/advanced/transactions.rst
@@ -0,0 +1,277 @@
+
+.. _ingest-api-advanced-transactions:
+
+Transaction management
+======================
+
+This document presents several advanced recipes related to transaction management in the Ingest API.
+Please read the following document first:
+
+- :ref:`ingest-api-concepts-transactions` (CONCEPTS)
+
+.. _ingest-api-advanced-transactions-multiple:
+
+Planning multiple transactions
+------------------------------
+
+To improve workflow stability, particularly during failures, the system supports distributed transactions.
+This method is essential for ensuring stable ingests. Transactions were initially discussed in the section
+:ref:`ingest-api-simple`. This section further explores the advantages of this method by detailing the planning
+and management of parallel transactions.
+
+All rows are ingested into the data tables within the scope of transactions. Once a transaction is committed,
+all relevant contributions remain in the destination tables. Conversely, if the transaction is aborted,
+the rows are removed. The transaction *abort* operation (:ref:`ingest-trans-management-end`) won't revert all
+modifications made to tables. It will only remove rows ingested within the corresponding transaction.
+For instance, any tables created during transactions will stay in Qserv. Any chunk allocations made during
+transactions will also stay. Leaving some tables empty after this operation won't confuse Qserv even if
+the tables remain empty after publishing the database.
+
+When designing a workflow for a specific catalog or a general-purpose workflow, it is crucial to consider potential
+failures during ingests. Estimating the likelihood of encountering issues can guide the decision-making process
+for planning the number and size of transactions to be started by the workflow. Here are some general guidelines:
+
+- If the probability of failures is low, it is advisable to divide the input dataset into larger portions
+ and ingest each portion in a separate transaction.
+- Conversely, if the probability of failures is high, using smaller transactions may be more appropriate.
+
+Another approach is to create a self-adjusting workflow that dynamically decides on transaction sizes based
+on feedback from previous transactions. For instance, the workflow could begin with several small transactions
+as probes and then progressively increase or decrease the number of contributions per transaction based on the results.
+This technique has the potential to enhance workflow performance.
+
+Other factors influencing the transaction planning process include:
+
+- **Availability of input data**: Contributions to the catalogs may arrive incrementally over an extended period.
+- **Temporary disk space limitations**: The space for storing intermediate products (partitioned CSV files) may be restricted.
+- **Qserv configuration**: The number of worker nodes in the Qserv setup can impact the workflow design.
+
+What is a resonable number of transactions per catalog ingest?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When planning ingest activities, consider the following global limits:
+
+- The total number of transactions per Qserv instance is capped by an unsigned 32-bit number.
+ The transaction identifier ``0`` is reserved by the Ingest System, so the maximum number of
+ transactions is ``4294967295``.
+- The total number of transactions per table is limited to ``8192`` due to the MySQL partition
+ limit. Practically, opening more than ``100`` transactions per database is not advisable because
+ of the overheads associated with MySQL partitioning.
+
+Another factor to consider is the implementation of transactions. The Ingest system directly maps transactions
+to MySQL table partitions. Each partition is represented by two files in the filesystem of the worker where
+the corresponding table resides (in the current implementation of Qserv, the data tables use the ``MyISAM`` storage engine):
+
+- ``#p.MYD``: The data file of the MySQL partition.
+- ``#p.MYI``: The index file of the MySQL partition.
+
+In the extreme case, the number of files representing chunked tables would be roughly equal to the total number of
+chunks multiplied by the number of transactions open per catalog. For example, if there are ``150,000`` chunks in
+a catalog and ``10`` transactions are open during the catalog ingest, the total number of files spread across
+all workers could be as many as ``3,000,000``. If the number of workers is ``30``, then there would be
+approximately ``100,000`` files per worker's filesystem, all in a single folder.
+
+In reality, the situation may not be as severe because the chunks-to-transactions "matrix" would be rather sparse,
+and the actual number of files per directory could be about ``10`` times smaller. Additionally, all MySQL table partitions will
+be eliminated during the catalog *publishing* phase. After that, each table will be represented
+with the usual three files:
+
+- ``.frm``: The table definition file.
+- ``.MYD``: The data file.
+- ``.MYI``: The index file.
+
+Nevertheless, it is advisable to avoid opening thousands of transactions per catalog ingest, even though the *hard*
+limit for MySQL partitions per table might seem quite high at ``8192``.
+
+.. _ingest-api-advanced-transactions-parallel:
+
+Parallel transactions
+---------------------
+
+This section covers some parallel ingest scenarios that may increase the overall performance of a workflow.
+
+Ingesting chunks in parallel within a single transaction
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is the simplest scenario that assumes the following organization of the workflow:
+
+#. **Sequential**: Start a common transaction before uploading the first chunk.
+#. **Parallel**: For each chunk:
+
+ #. **Sequential**: Allocate a chunk
+ #. **Sequential**: Ingest contributions into each chunk.
+
+#. **Sequential**: Commit the common transaction after all contributions are successfully uploaded.
+
+The following diagram illustrates the idea:
+
+.. image:: /_static/ingest-trans-multiple-one.png
+ :target: ../../../_images/ingest-trans-multiple-one.png
+ :alt: One Transaction
+
+Things to consider:
+
+- The chunk allocation operations are serialized in the current version of the system. This may introduce
+ indirect synchronization between parallel chunk-specific ingests. The total latency incurred by such synchronization
+ is the latency of allocating one chunk multiplied by the number of chunks.
+- The proposed scheme may not be very efficient if the number of chunks is large (heuristically, many thousands)
+ while chunk contributions are small. In this case, the latency of the chunk allocation requests may become a significant
+ factor limiting the performance of the workflow.
+- Any failure to ingest a contribution will result in aborting the entire transaction. This can significantly
+ impact the workflow's performance, especially if the amount of data to be ingested is large.
+ impact the workflow's performance, especially if the amount of data to be ingested is large.
+
+Best use:
+
+- When the number of chunks is small and the amount of data to be ingested into each chunk is large, or
+ if the amount of data or the number of contributions to be ingested into each chunk is large. In this case
+ negative effects of the chunk allocation latency are negligible.
+
+Ingesting chunks in parallel within dedicated transactions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is a more complex scenario that assumes the following organization of the workflow:
+
+- **Parallel**: For each chunk do the following:
+
+ #. **Sequential**: Start a separate transaction dedicated for ingesting all contributions of the chunk.
+ #. **Sequential**: Allocate the chunk and ingest all contributions into the chunk.
+ #. **Sequential**: Commit the transaction after all contributions into the chunk are successfully uploaded.
+
+The following diagram illustrates the idea:
+
+.. image:: /_static/ingest-trans-multiple-chunks.png
+ :target: ../../../_images/ingest-trans-multiple-chunks.png
+ :alt: Per-chunk Transaction
+
+Things to consider:
+
+- Although this scheme assigns each chunk to a dedicated transaction, it is not strictly necessary.
+ The Ingest system allows allocating the same chunk and ingesting contributions into that chunk from any (or multiple) transactions.
+ Just ensure that the same set of rows (the same set of contributions) is not ingested within more than one transaction.
+ This rule applies to any workflow regardless.
+- Failures in one chunk transaction will not affect chunk contributions made in the scope of other transactions.
+ This is a significant advantage of this scheme compared to the previous one.
+
+Best use:
+
+- When ingesting a large dataset, it can be divided into independently ingested groups based on chunks.
+ Transactions offer a mechanism to handle failures effectively.
+
+
+Scattered ingest of chunk contributions within multiple transactions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Workflow organization:
+
+- **Parallel**: For each transaction do the following:
+
+ #. **Sequential**: Start a transaction dedicated for ingesting a subset of contributions of any chunks that
+ may be related to cteh contributions of teh subset.
+ #. **Sequential**: For each contribution in the subset:
+
+ #. **Sequential**: Allocate a chunk as needed for the contribution.
+ #. **Sequential**: Ingest the contributions into the chunk.
+
+ #. **Sequential**: Commit the transaction after ingesting all contributions in the subset.
+
+The following diagram illustrates the idea:
+
+.. image:: /_static/ingest-trans-multiple-scattered.png
+ :target: ../../../_images/ingest-trans-multiple-scattered.png
+ :alt: Scattered Transactions
+
+Best use:
+
+- When the workflow is designed to ingest a large dataset where data are streamed into the workflow.
+ This scenario is particularly useful when the data are not available in a single file or when the data
+ are generated on-the-fly by some external process.
+
+.. tip::
+
+ One can combine the above scenarios to create a more complex workflow that meets the specific requirements
+ of the ingest process.
+
+.. _ingest-api-advanced-transactions-abort:
+
+Aborting transactions
+----------------------
+
+The concept of distributed transactions was introduced in the section :ref:`ingest-api-concepts-transactions`. Transactions
+are a fundamental mechanism for ensuring the consistency of the ingest process. The system allows aborting transactions
+to revert the effects of all contributions made to the catalogs within the scope of the transaction. This operation is particularly useful
+when the ingest process encounters an issue that cannot be resolved by the system automatically, or when the failure leaves
+the data or metadata tables in an inconsistent state. Transactions are aborted using the following service:
+
+- :ref:`ingest-trans-management-end` (REST)
+
+Reasons to abort
+^^^^^^^^^^^^^^^^
+
+There are two primary reasons for aborting a transaction, detailed in the subsections below.
+
+Communication Failures
+~~~~~~~~~~~~~~~~~~~~~~
+
+If any communication problem occurs between the workflow and the system during a contribution request, the workflow **must** unconditionally
+abort the corresponding transaction. Such problems create uncertainty, making it impossible to determine if any actual changes were made to
+the destination tables.
+
+This rule applies universally, regardless of the method used for making the contribution request (by reference, by value, synchronous, asynchronous, etc.).
+
+Ingest System Failures
+~~~~~~~~~~~~~~~~~~~~~~
+
+Unlike the previously explained scenario, this scenario assumes that the workflow can track the status of attempted contribution requests.
+The status information is reported by the ingest system. The workflow can detect a failure in the response object and decide to abort
+the transaction. However, the analysis of the failure is done slightly differently for *synchronous* and *asynchronous* requests.
+
+The algorithm for the *synchronous* requests is rather straightforward. If the attribute ``status`` of the response object
+indicates a failure as ``status=0``, the workflow must analyze the ``retry-allowed`` flag in :ref:`ingest-worker-contrib-descriptor` (REST).
+If the flag is set to ``0``, the transaction must be aborted. If the flag is set to ``1``, the workflow can retry the contribution request
+within the scope of the same transaction using the following service:
+
+- :ref:`ingest-worker-contrib-retry` (REST)
+
+The algorithm for the *asynchronous* requests is a bit more complex. The response object for the contribution submission request does not contain
+the actual completion status of the request. If the request submission was not successful as indicated by ``status=0``, it means the request was incorrect or
+made in a wrong context (no transaction open, non-existing table, etc.). In this case, the workflow must abort the transaction.
+Otherwise (the response object has ``status=1``), the actual status of the contribution request can be obtained later by polling the system
+as explained in the section:
+
+- :ref:`ingest-api-concepts-contributions-status` (CONCEPTS)
+
+The REST services explained in this section return the contribution descriptor object that contains the status of the contribution request.
+The workflow must first check if a contribution has finished (or failed) or if it's still in progress (or in the wait queue of the processor).
+
+- :ref:`ingest-worker-contrib-descriptor` (REST)
+
+Possible values of the attribute ``status`` (**Note** this is an attribute of the contribution itself not the completion status of teh REST request)
+are explained in the above-mentioned document. Any value other than ``IN_PROGRESS`` indicates that the contribution request has finished (or failed).
+Should the request fail, the workflow must then analyze the flag ``retry-allowed`` as explained above.
+
+What happens when a transaction is aborted?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Aborting a transaction is a relatively quick operation. The primary change involves the removal of MySQL table partitions associated with the transaction.
+The following table files on disk will be deleted:
+
+- ``#p.MYD``: The data file of the MySQL partition.
+- ``#p.MYI``: The index file of the MySQL partition.
+
+All queued or in-progress contribution requests will be dequeued or stopped. The final status of the requests will be either ``CANCELLED`` (for requests
+that were still in the queue) or some other failure state depending on the processing stage of a request. The system will not attempt to process
+them further.
+
+What to do if a transaction cannot be aborted?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+It's possible that the system will not be able to abort a transaction. For example, if one of the workers is down or is not responding to the abort request.
+In such cases, the status of the transaction will be ``IS_ABORTING`` or ``ABORT_FAILED`` as explained in the section:
+
+- :ref:`ingest-trans-management-states` (CONCEPTS)
+
+If the transaction cannot be aborted, the workflow developer must be prepared to handle the situation. There are a few options:
+
+- The workflow may be programmed to retry the abort operation after a certain timeout.
+- If retrying doesn't help, the user of the workflow should contact the Qserv administrators to resolve the issue.
diff --git a/doc/ingest/api/advanced/unpublishing.rst b/doc/ingest/api/advanced/unpublishing.rst
new file mode 100644
index 0000000000..7eecd383c4
--- /dev/null
+++ b/doc/ingest/api/advanced/unpublishing.rst
@@ -0,0 +1,54 @@
+
+
+.. _ingest-api-advanced-unpublishing-databases:
+
+Ingesting tables into the published catalogs
+--------------------------------------------
+
+.. warning::
+
+ Currently, the ingest system only supports adding new tables to databases. It does not permit adding rows to previously
+ ingested tables. Any attempts to modify existing tables will be blocked by the system.
+
+In some cases, especially when ingesting large-scale catalogs, the input data for all tables may not be available at the start
+of the ingest campaign. Some tables may be ready for ingestion earlier than others. Another scenario is when a previously
+ingested table needs to be re-ingested with corrected data. In these situations, the catalog must be built incrementally
+while allowing Qserv users to access the previously published tables. The previously described workflow of ingesting all
+tables at once and then publishing the catalog as a whole would not work here. To address these scenarios, the system allows
+temporarily *un-publishing* the catalog to add new or replace existing tables. The following REST service should be used for
+this:
+
+- :ref:`ingest-db-table-management-unpublish-db` (REST)
+
+Key points to note:
+
+- This operation is very quick.
+- The database state transition is largely transparent to Qserv users, except when replacing an existing table with a newer
+ version. The un-published database, including all previously ingested tables, will still be visible and queryable by Qserv
+ users.
+- The operation requires the ingest workflow to use an administrator-level authorization key. This will be demonstrated in
+ the example below.
+
+
+The modified workflow sequence expected in this case is as follows:
+
+#. Unpublish the existing catalog.
+#. Delete an existing table if it needs to be replaced.
+#. Register a new table (or a new version of the removed table) or multiple tables as needed.
+#. Start transactions.
+#. Load contributions for the new tables.
+#. Commit transactions.
+#. Publish the catalog again.
+
+This sequence can be repeated as needed to modify the catalog. Note that starting from step **3**, this sequence
+is no different from the simple scenario of ingesting a catalog from scratch. The last step of the sequence
+will only affect the newly added tables. Hence, the performance of that stage will depend only on the scale and the amount
+of data ingested into the new tables.
+
+Here is an example of how to unpublish a catalog:
+
+.. code-block:: bash
+
+ curl http://qserv-master01:25081/replication/config/database/test101 \
+ -X PUT -H 'Content-Type: application/json' \
+ -d'{"admin_auth_key":""}'
diff --git a/doc/ingest/api/advanced/warnings.rst b/doc/ingest/api/advanced/warnings.rst
new file mode 100644
index 0000000000..063986032c
--- /dev/null
+++ b/doc/ingest/api/advanced/warnings.rst
@@ -0,0 +1,106 @@
+
+.. _ingest-api-advanced-warnings:
+
+Using MySQL warnings for the data quality control
+=================================================
+
+The context
+-----------
+
+The table ingest is presently implemented using MySQL/MariaDB bulk insert statement:
+
+.. code-block:: sql
+
+ LOAD DATA [LOCAL] INFILE ...
+
+This is currently the most efficient and performant method for adding rows into tables from input CSV files (:ref:`ingest-api-concepts-contributions`).
+The technique is detailed in https://mariadb.com/kb/en/load-data-infile/.
+
+This method differs significantly from the standard SQL ``INSERT``. One caveat of using this mechanism is that MySQL (MariaDB)
+attempts to ingest all input data into the table, even if some rows (or fields within rows) cannot be correctly interpreted. Consequently:
+
+- The table may contain fewer (or sometimes more) rows than expected.
+- Some cell values may be truncated.
+- Some cells may contain incorrect data.
+
+In order to help client applications detect these problems, MySQL offers diagnostic tools (queries, counters) that report
+internal issues encountered during data loading. These are detailed in https://mariadb.com/kb/en/show-warnings/.
+
+The current implementation of the Ingest system leverages these features by capturing warnings (as well as notes and errors)
+and recording them within the Replication database in association with the corresponding contribution requests. This is done
+for each request regardless of how it was submitted, whether via the proprietary binary protocol of
+:ref:`ingest-tools-qserv-replica-file`, or by calling the REST services of the Ingest system:
+
+- :ref:`ingest-worker-contrib-by-ref` (WORKER)
+- :ref:`ingest-worker-contrib-by-val` (WORKER)
+
+Both interfaces also offer a parameter to control the depth of the warning reports by specifying the desired limit on
+the number of warnings to be retained for each request. This limit is optional. If not specified at the time of request
+submission, the service will use the limit configured at the worker ingest server's startup.
+
+The REST services that return information on contributions have another optional parameter that indicates whether the client
+is interested in seeing just the total number of warnings or the complete description of all warnings retained by the system.
+The effect of this parameter on the resulting JSON object returned by the services is explained in:
+
+- :ref:`ingest-worker-contrib-descriptor` (WORKER)
+
+In addition to the individual descriptions (if required) of the warnings, the relevant services also report three summary counters:
+
+``num_warnings``:
+ The total number of warnings detected by MySQL when loading data into the destination table. Note that this number is not
+ the same as the number of warning descriptions returned by the REST services. Unlike the latter, ``num_warnings`` represents
+ the true number of warnings. Only a subset of those is captured in full detail by MySQL.
+
+``num_rows``:
+ The total number of rows parsed by the Ingest system in the input file. The ingest service always parses the input files as
+ it needs to extend each row in order to prepend them with a unique identifier of the corresponding super-transaction (the name
+ of the added column is ``qserv_trans_id``).
+
+``num_rows_loaded``:
+ The total number of rows that were actually loaded by the system into the destination table. Note that in case MySQL
+ encountered any problems with the input data while interpreting and ingesting those into the destination table, this
+ counter may not be the same as ``num_rows``. In practice, a value reported in ``num_rows_loaded`` could be either lower or
+ higher than the value reported in ``num_rows``.
+
+Using warnings and counters
+---------------------------
+
+The interfaces described above provide a range of options for workflow developers and Qserv data administrators:
+
+- Workflow developers can enhance their workflows to analyze the reported counters for contributions. This helps determine
+ if an ingest operation was genuinely successful or if issues occurred.
+- Data administrators can utilize both counters and warning descriptions to analyze ingest results and debug input data.
+ Any data issues can be traced back to their source (e.g., LSST Pipeline).
+
+The following subsections present techniques that can be leveraged in this context.
+
+Analyzing counters
+^^^^^^^^^^^^^^^^^^
+
+The ingest operation should be considered successful if both of the following conditions are met:
+
+- ``num_warnings`` equals to ``0``
+- ``num_rows`` is the same as ``num_rows_loaded``
+
+If these conditions are not met, a data administrator should inspect the warning descriptions in detail to identify the cause
+of the discrepancy.
+
+Increasing the depth of the warnings queue
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. hint::
+
+ The default imposed by MySQL would be ``64``. And the upper bound for the limit is ``65535``.
+
+Significantly increasing the limit above the default value should be considered a temporary measure. All warnings are recorded
+within the persistent state of the Replication/Ingest system, and the database serving the system may have limited storage
+capacity. Capturing many millions of descriptions across all contributions when ingesting medium-to-large scale catalogs may
+also significantly reduce the overall performance of the ingest system.
+
+Data administrators may temporarily increase the upper limit for the number of warnings to debug input data. The limit can be
+set when submitting contribution requests via the APIs mentioned earlier in this chapter. Alternatively, the Replication/Ingest
+worker server can be started with the following command-line option:
+
+.. code-block:: bash
+
+ qserv-replica-worker --worker-loader-max-warnings=
diff --git a/doc/ingest/api/concepts/contributions.rst b/doc/ingest/api/concepts/contributions.rst
new file mode 100644
index 0000000000..6053d0eff3
--- /dev/null
+++ b/doc/ingest/api/concepts/contributions.rst
@@ -0,0 +1,87 @@
+
+.. _ingest-api-concepts-contributions:
+
+Table contributions
+===================
+
+The API defines a *contribution* as a set of rows ingested into a table via a separate request.
+These are the most important characteristics of contributions:
+
+- Each contribution request is always made within the scope of a transaction. This association
+ is crucial for data provenance and data tagging purposes.
+- Information on contributions is preserved in the persistent state of the Ingest system.
+- Contributions have unique identifiers assigned by the Ingest system.
+
+.. _ingest-api-concepts-contributions-atomicity:
+
+Request atomicity and retries
+-----------------------------
+
+The contribution ingest requests are considered as atomic operations with a few important caveats:
+
+- The contributions are not committed to the table until the transaction is committed even
+ if the contribution request was successful.
+
+- Failed contribution requests must be evaluated by the workflow to determine if the target table
+ remains in a consistent state. This is indicated by the ``retry-allowed`` attribute returned
+ in the response object. Based on the value of this flag, the workflow should proceed as follows:
+
+ - ``retry-allowed=0``: The workflow must roll back the transaction and initiate a new
+ contribution request within the scope of a new transaction. For more details, refer to:
+
+ - :ref:`ingest-api-advanced-transactions-abort` (ADVANCED)
+ - :ref:`ingest-trans-management-end` (REST)
+
+ - ``retry-allowed=1``: The workflow can retry the contribution within the scope of the same
+ transaction using:
+
+ - :ref:`ingest-worker-contrib-retry` (REST)
+
+Note that for contributions submitted by reference, there is an option to configure a request
+to automatically retry failed contributions. The maximum number of such retries is controlled
+by the ``num_retries`` attribute of the request:
+
+- :ref:`ingest-worker-contrib-by-ref` (REST)
+
+Contributions pushed to the service by value can not be automatically retried. The workflow
+would have to decide on the retrying the failed contributions explicitly.
+
+Multiple contributions
+----------------------
+
+When ingesting rows into a table (whether *partitioned* or *regular*), the workflow does not need to complete
+this in a single step from one input file. The Ingest system supports building tables from multiple contributions.
+Contributions can be made within different transactions or multiple contributions can be ingested within the same transaction.
+It is the responsibility of the workflow to keep track of what has been ingested into each table, within which transaction,
+and to handle any failed transactions appropriately.
+
+Ingest methods
+--------------
+
+Data (rows) of the contributions are typically stored in the ``CSV``-formatted files. In this
+case the files would be either directly pushed to the worker Ingest server or uploaded by
+the Ingest service from a location that is accessible to the worker:
+
+- :ref:`ingest-worker-contrib-by-val` (REST)
+- :ref:`ingest-worker-contrib-by-ref` (REST)
+
+The first option (ingesting by value) also allows pushing data of contributions
+directly from the memory of the client process (worklow) w/o the need to store the data in the files.
+
+.. _ingest-api-concepts-contributions-status:
+
+Status of the contribution requests
+-----------------------------------
+
+The system allows to pull the information on the contributions given their identifiers:
+
+- :ref:`ingest-info-contrib-requests` (REST)
+
+An alternative option is to query the information on contributions submitted in a scope of
+a transaction:
+
+- :ref:`ingest-trans-management-status-one` (REST)
+
+The schema of the contribution descriptor objects is covered by:
+
+- :ref:`ingest-worker-contrib-descriptor`
diff --git a/doc/ingest/api/concepts/families.rst b/doc/ingest/api/concepts/families.rst
new file mode 100644
index 0000000000..5ce1d13e55
--- /dev/null
+++ b/doc/ingest/api/concepts/families.rst
@@ -0,0 +1,37 @@
+
+.. _ingest-api-concepts-database-families:
+
+Database families
+=================
+
+The concept of *database families* originated from the need to correctly distribute data of the *partitioned* tables within Qserv. This allows
+Qserv to accurately process queries that ``JOIN`` between the tables of different databases. A database family is a group of databases where
+all tables within the family share the same partitioning parameters:
+
+- the number of *stripes*
+- the number of *sub-stripes*
+- the *overlap* radius
+
+This will ensure that all *chunk* tables with the same chunk number will have:
+
+- the same spatial dimensions in the coordinate system adopted by Qserv
+- the same number and sizes of *sub-chunks* within each chunk.
+
+The families are defined by the Replication/Ingest system and are not visible to Qserv users. Each family has a unique
+identifier (name). The system uses family names to correctly distribute the data of partitioned tables among Qserv worker
+nodes, ensuring that the data of tables joined in queries are co-located on the same worker node.
+
+Families must be defined before the databases and tables are registered in Qserv. The current implementation of the API
+automatically creates a new family when the first database with a unique combination of partitioning parameters is registered
+in the system using:
+
+- :ref:`ingest-db-table-management-register-db` (REST)
+
+If a family with the same partitioning parameters already exists in the system, the new database will be added to the existing family.
+Existing databases and families can be found using the following service:
+
+- :ref:`ingest-db-table-management-config` (REST)
+
+For instructions on partitioning the tables with the desired set of parameters, refer to the following document:
+
+- :ref:`ingest-data` (DATA)
diff --git a/doc/ingest/api/concepts/index.rst b/doc/ingest/api/concepts/index.rst
new file mode 100644
index 0000000000..65bccd87ce
--- /dev/null
+++ b/doc/ingest/api/concepts/index.rst
@@ -0,0 +1,35 @@
+
+.. _ingest-api-concepts:
+
+=============
+Main concepts
+=============
+
+.. hint::
+
+ This section of the document begins with the high-level overview of the Qserv ingest API.
+ Please read this section carefully to learn about the main concepts of the API and a sequence
+ of operations for ingesting catalogs into Qserv.
+
+ After completing the overview, a reader has two options for what to read next:
+
+ - Study the core concepts of the API in depth by visiting subsections:
+
+ - :ref:`ingest-api-concepts-table-types`
+ - :ref:`ingest-api-concepts-transactions`
+ - :ref:`ingest-api-concepts-publishing-data`
+ - etc.
+
+ - Go straight to the practical example of a simple workflow presented at:
+
+ - :ref:`ingest-api-simple`
+
+.. toctree::
+ :maxdepth: 4
+
+ overview
+ table-types
+ transactions
+ contributions
+ publishing
+ families
diff --git a/doc/ingest/api/concepts/overview.rst b/doc/ingest/api/concepts/overview.rst
new file mode 100644
index 0000000000..35b94d31d7
--- /dev/null
+++ b/doc/ingest/api/concepts/overview.rst
@@ -0,0 +1,254 @@
+.. _ingest-api-concepts-overview:
+
+Overview of the ingest workflow
+===============================
+
+The ingest workflow must accomplish a series of tasks to ingest data into Qserv.
+These tasks are presented in the correct order below:
+
+Plan the ingest
+---------------
+
+.. hint::
+
+ You may also contact Qserv experts or Qserv administrators to get help on the planning stage.
+
+There is a number of important questions to be answered and decisions to be made ahead of time in the following
+areas before starting ingesting data into Qserv. Knowing these facts allows to organize the ingest activities in
+the most efficient way.
+
+Creating a new database or adding tables to the existing one?
+
+- :ref:`ingest-api-concepts-publishing-data` (CONCEPTS)
+
+What are the types of tables to be ingested?
+
+- :ref:`ingest-api-concepts-table-types` (CONCEPTS)
+
+What should be the values of the partitioning parameters of the partitioned tables?
+
+- :ref:`ingest-api-concepts-database-families` (CONCEPTS)
+
+What is a scale of the planned ingest effort?
+
+- The amount of data (rows, bytes) to be ingested in each table
+- The number of the ``CSV`` files to be ingested
+- Sizes of the files
+- The number of the workers that are available in Qserv
+
+Where the ready to ingest data will be located?
+
+- Are there any data staging areas available?
+- :ref:`ingest-api-advanced-contributions` (ADVANCED)
+
+Prepare the input data
+----------------------
+
+Data files (table *contributions*) to be ingested into Qserv need to be in the ``CSV`` format. It's up to the workflow
+to ensure that the data is in the right format and that it's sanitized to ensure the values of the columns
+are compatible with the MySQL expectations.
+
+- :ref:`ingest-data` (DATA)
+
+Note that the data preparation stage depends on the types of tables to be ingested. Read about the table types in:
+
+- :ref:`ingest-api-concepts-table-types` (CONCEPTS)
+
+Register or un-publish a database
+---------------------------------
+
+The main goal of this step is to ensure that the database is ready for registering new tables. Firstly,
+the database should be registered in the Replication/Ingest system. Secondly, the database should be
+found (or put into) the *unpublished* state. Read about the database states in the following document
+section:
+
+- :ref:`ingest-api-concepts-publishing-data` (CONCEPTS)
+
+Further steps depend on the state of the database. If the database doesn't exists in the Replication/Ingest system
+it should be registered using:
+
+- :ref:`ingest-db-table-management-register-db` (REST)
+
+The newely registered database will be always in the *unpublished* state. If the database already exists in
+the Replication/Ingest and it's in the *published* state it should be *unpublished* state using:
+
+- :ref:`ingest-db-table-management-unpublish-db` (REST)
+
+Register tables
+---------------
+
+Before ingesting data into Qserv the corresponding tables should be registered in the Replication/Ingest system.
+Tables are registered using:
+
+- :ref:`ingest-db-table-management-register-table` (REST)
+
+Table registration requests should includes various information on each table, such as:
+
+- the name of the database where the table belongs
+- the name of the table
+- the type of the table
+- the schema
+
+Detailed instructions on this subjects can be found in the description of the service mentioned above.
+
+Configure the Ingest service
+----------------------------
+
+This step is optional. And it's mostly needed to adjust the default configuration parameters of the Ingest service
+to allow pulling contributions from the data staging areas, such as web servers, cloud storage, etc. Examples of
+the configuration parameters are: timeouts, the number of parallel requests, SSL/TLS certificates, HTTP/HTTPS proxy
+settings, etc. More information on this subject can be found in:
+
+- :ref:`ingest-config` (REST)
+
+These parameters can be adjusted in real time as needed. The changes get into effect immediately. Note that
+the parameters are set on the database level. For example, the configuration parameters set for the database ``db1``
+will not affect the ingest activities for the database ``db2``.
+
+.. note::
+
+ Please be aware that the ingest activities can also be affected by the global configuration parameters of
+ the Replication/Ingest system:
+
+ - :ref:`ingest-api-advanced-global-config` (ADVANCED)
+
+Start transactions
+------------------
+
+Making the right choices on how many transactions to start and how many contributions to send in a scope of each transaction
+is a key to the ingest performance. The transactions are used to group the contributions. In some cases, when
+contributions fail the transactions should be aborted. Should this happen all ingest efforts made in the scope of
+the failed transactions would have to be rolled back, and the workflow would have to start the corresponding ingest
+activities from the beginning. Hence the workflow should be prepared to handle the transaction aborts and make
+reasonable decisions on the amount of data to be sent in a scope of each transaction (a "size" of the transaction)
+based on the risk assesment made by the workflow developers or the data administrators who would be using the workflow
+for ingesting a catalog.
+
+.. hint::
+
+ It's recommended to make the transaction management logic of the workflow configurable.
+
+More information on this subject can be found in:
+
+- :ref:`ingest-api-concepts-transactions` (CONCEPTS)
+- :ref:`ingest-api-advanced-transactions` (ADVANCED)
+- :ref:`ingest-trans-management-start` (REST)
+
+Figure out locations of tables and chunks
+-----------------------------------------
+
+The design of the API requires the workflow to know the locations of the tables and chunks at workers.
+The locations are needed to forward the table contribution requests directly to the corresponding worker
+services. The locations can be obtained using services covered in the following document:
+
+- :ref:`table-location` (REST)
+
+Send the data to the workers
+----------------------------
+
+At this stage the actual ingest activities are started. The reader should read the following document document first
+to understand the concepts of the *contributions*:
+
+- :ref:`ingest-api-concepts-contributions` (CONCEPTS)
+
+The REST API for initiating the contribuiton requests is covered in the following documents:
+
+- :ref:`ingest-worker-contrib-by-ref` (REST)
+- :ref:`ingest-worker-contrib-by-val` (REST)
+
+Monitor the progress of the ingest activities
+----------------------------------------------
+
+The workflow should always be avare about the progress of the ingest activities, and about the status of the
+contribution requests. This is need for (at least) three reasons:
+
+#. To know when the ingest activities are finished
+#. To know when the ingest activities (and which requests) are failed
+#. To make more contribution requests if needed
+
+In the simplest *linear* design of the workflow, such as the one presented in the :ref:`ingest-api-simple`,
+the workflow may implement the monitoring as a separate step after making all contribution requests. In more
+realistic scenarious the monitoring stage should be an integral part of the same logic that is responsible for
+making the contribution requests.
+
+Besides the monitoring of the contribution requests the workflow should also monitor the status of the databases,
+transactions and Qserv workers to be sure that the ingest activities are going as planned and that the underlying
+services are healthy. These are the relevant services for the monitoring:
+
+- :ref:`ingest-config-global-workers` (REST)
+- :ref:`ingest-trans-management-status` (REST)
+
+Commit/abort the transactions
+-----------------------------
+
+Once all contributions are successfully ingested the transactions should be commited. If any problems occured within
+the transactions the workflow should be prepared to handle the transaction aborts. Both operations are performed by:
+
+- :ref:`ingest-trans-management-end` (REST)
+
+Read more about the transactions and transaction aborts in:
+
+- :ref:`ingest-api-concepts-transactions` (CONCEPTS)
+- :ref:`ingest-api-advanced-transactions-abort` (ADVANCED)
+
+Another option in the case of a catastrophic failure during the ingest would be to scrap the whole database
+or the tables and start the ingest activities from the beginning. This is a more radical approach, but it's
+sometimes the only way to recover from the failure. The services for deleting the database and the tables are
+covered in:
+
+- :ref:`ingest-db-table-management-delete` (REST)
+
+.. warning::
+
+ The deletion of the database or the tables is an irreversible operation. Use it with caution.
+
+Publish the database
+--------------------
+
+.. warning::
+
+ Depending on the types of tables created by the workflow, the amount of data ingested into the tables,
+ and the number of transactions created during the effort, the database publishing operation may take a while.
+ There is always a chance that it may fail should anything unpredicted happen during the operation. This could be
+ a problem with the underlying infrastructure, the network, the database, the workers, etc. Or it could be a problem
+ with the ingested data. The workflow should be prepared to handle the failure of the database publishing operation
+ and check the completion status of the request.
+
+.. hint::
+
+ The current implementation of the operation is *synchronous*, which means the workflow would have to wait
+ before the service sends back a response to be analyzed. However, the implementation of the operation is *idempotent*,
+ which means the workflow can retry the operation as many times as needed without any side effects should any network
+ problems occur during the operation.
+
+Formally, this would be the last stage of the actual ingest. The database and the tables are published to make them
+visible to the users. The database and the tables are published using the following services:
+
+- :ref:`ingest-db-table-management-publish-db` (REST)
+
+All new tables that were registered in the database by the workflow would be published automatically.
+And the database would be placed into *published* state.
+
+Read more on this concept in:
+
+- :ref:`ingest-api-concepts-publishing-data` (CONCEPTS)
+
+Verify the ingested data products
+---------------------------------
+
+This step is optional. A possibility of implementing the automatic verification if the ingested
+data products are correct and consistent depends on the workflow requirements and the data.
+These are some very basic verification steps that the workflow may want to implement:
+
+- the data can be queried
+- the data can be compared to the original data
+- the number of rows in the tables is correct
+
+Perform the optional post-ingest data management operation on the ingested tables
+---------------------------------------------------------------------------------
+
+This step is optional. The workflow may want to perform some post-ingest data management operations on the ingested tables.
+An alternative approach is to perform these operations after verifying the ingested data products.
+These operations are covered in:
+
+- :ref:`ingest-api-post-ingest` (API)
diff --git a/doc/ingest/api/concepts/publishing.rst b/doc/ingest/api/concepts/publishing.rst
new file mode 100644
index 0000000000..bc27bdfc92
--- /dev/null
+++ b/doc/ingest/api/concepts/publishing.rst
@@ -0,0 +1,68 @@
+
+.. _ingest-api-concepts-publishing-data:
+
+Publishing databases and tables
+===============================
+
+Databases
+---------
+
+Databases in Qserv can be in one of two states: *published* or *unpublished*. Databases in the *published* state
+are visible to Qserv users and can be queried. Generally, databases in this state are considered static and cannot be modified.
+However, certain operations are still allowed on the tables of published databases. These operations are documented in
+the following section:
+
+- :ref:`ingest-api-post-ingest`
+
+Databases in the *published* state are also subject to routine replica management operations performed by
+the Qserv Replication system.
+
+The *unpublished* state is reserved for making significant changes to the table data, the table schema, ingesting new tables, etc.
+The replica management operations are not performed on the databases in this state in order to avoid conflicts with the ongoing changes.
+
+Note that newly created databases are always in the *unpublished* state, and they are not visible to the Qserv users. When all desired
+tables are ingested into the database and the database is ready for querying, it should be *published* using the following service:
+
+- :ref:`ingest-db-table-management-publish-db` (REST)
+
+.. note::
+
+ Before a database can be published, all transactions open within the context of the database must be either committed or rolled back
+ using the following service:
+
+ - :ref:`ingest-trans-management-end` (REST)
+
+ If this condition is not met, the database publishing service will reject the request. It is the responsibility of the workflow
+ to manage these transactions.
+
+Databases can also be unpublished to allow adding new tables, or for performaging significant changes to the table data or schema
+using the following service:
+
+- :ref:`ingest-db-table-management-unpublish-db` (REST)
+
+.. note::
+
+ The database unpublishing service does not affect the visibility of the database to Qserv users. All tables that existed
+ in the database before unpublishing can still be queried by Qserv users. The unpublishing operation is transparent to the users.
+
+Tables
+------
+
+Tables in Qserv can be in one of two states: *published* or *unpublished*. This distinction is relevant only for tables within
+databases that are in the *unpublished* state. Unlike databases, the state of a table indicates whether the table is fully ingested
+and should not be modified thereafter, regardless of the database's state.
+
+Newly created tables are always in the *unpublished* state. Once all desired data is ingested into the table and it is ready for querying,
+it should be published. This occurs indirectly when the database is published. After publication, the table is marked as *published*
+and becomes visible to Qserv users.
+
+During the table publishing stage the Replication/Ingest system:
+
+- removes MySQL partitions from the data tables at workers
+- (optionally) removes MySQL partitions from the *director* index table
+
+The last step is optional. It only applies to the *director* tables, and only if the database was registered with
+the optional attribute set as ``auto_build_secondary_index=1`` when calling the service:
+
+- :ref:`ingest-db-table-management-register-db` (REST)`
+
diff --git a/doc/ingest/api/concepts/table-types.rst b/doc/ingest/api/concepts/table-types.rst
new file mode 100644
index 0000000000..c15720ae64
--- /dev/null
+++ b/doc/ingest/api/concepts/table-types.rst
@@ -0,0 +1,79 @@
+.. _ingest-api-concepts-table-types:
+
+Types of tables in Qserv
+========================
+
+There are two types of tables in Qserv:
+
+- *regular* (fully replicated)
+- *partitioned* (distributed)
+
+A copy of the regular table exists at each worker node. Tables of this type are relatively small so that they can fit on disk
+at each worker. The tables are usually meant to store the metadata or the reference data for the large partitioned tables.
+The following diagram illustrates the concept:
+
+.. image:: /_static/ingest-table-types-regular.png
+ :target: ../../../_images/ingest-table-types-regular.png
+ :alt: Regular (fully-replicated) Tables
+
+The partitioned tables are distributed across the workers. These tables are much larger than the regular tables and they can't
+fit on disk at each worker. Each such table is *horisontally* (by rows) partitioned into the so called *chunks* (or *chunk tables*). Each chunk
+table is a separate table that is stored on the MySQL server of a worker. Depending on values of the partitioning parameters (specifically
+on th number of *stripes*) a catalog may have from 10,000 to over 100,000 chunks. The names of the chunk tables are based on the name of
+the original table, the chunk number and the optional "FullTableOverlap" suffix after the base name of the table. See the following
+section for more information on the naming convention of the tables in Qserv:
+
+- :ref:`ingest-general-base-table-names`
+
+Each chunk table has a subset of the rows of the original table. A superposition of rows from all the chunk tables of the same Qserv table
+is equal to a set of rows in the original (*base*) table. The following diagram illustrates the concept:
+
+.. image:: /_static/ingest-table-types-partitioned.png
+ :target: ../../../_images/ingest-table-types-partitioned.png
+ :alt: Partitioned (chunk) Tables
+
+
+Note that each chunk of the partitioned table maps to rectangular sector of the Sky based on the spatial coordinates system adopted by Qserv.
+Spatial coordinates of all rows within a chunk table are all found within the spatial area of the chunk. The spatial areas of different
+chunks never overlap.
+
+.. note::
+
+ The chunk *overelap* table includes a "halo" of rows from the neighboring chunks. The size of the overlap is defined by the *overlap* parameter
+ of the table. The overlap is used to ensure that the rows that are close to the chunk boundary are not missed by the so called "near-neighbour"
+ queries. These table are explained late rin this section.
+
+The chunk tables are made by the partitioning process that is documented in:
+
+- :ref:`ingest-data`
+
+The partitioned tables are further classified into the following subtypes:
+
+- *director* tables
+- *dependent* tables, which are further classified into:
+
+ - *simple* (1 director)
+ - *ref-match* (2 directors)
+
+The *director* tables are the tables in which each row has a unique identifier which is similar to the *primary key* in the relational algebra.
+The *dependent* tables have rows which depend on the rows of the corresponding *director* tables via the *foreign*-like key referencing
+the corresponing *primary key*. The *simple* tables have only one *director* table, while the *ref-match* tables have two *director* tables.
+The *ref-match* tables are used to store the matches between the objects of the two different tables. The following diagram illustrates these
+concepts:
+
+.. image:: /_static/ingest-table-types-dependent.png
+ :target: ../../../_images/ingest-table-types-dependent.png
+ :alt: Dependent Tables
+
+The *director* tables may not have any *dependent* tables. Each such *director* is useable and queriable by itself. The *dependent* tables
+must have the corresponding *director* tables. Same rules apply to the *ref-match* tables.
+
+Each chunk table of the director table has the corresponfing chunk *overlap* table. The *overlap* table includes a subset of rows from the chunk table
+and a "halo" of rows from the neighboring chunks. The size of the overlap is defined by the *overlap* parameter of the table. The idea of the overlap
+is illustrated in the following diagram:
+
+.. image:: /_static/subchunks.png
+ :target: ../../../_images/subchunks.png
+ :alt: Dependent Tables
+
+The diagram shown sub-chunk boundaries within the chunk table.
diff --git a/doc/ingest/api/concepts/transactions.rst b/doc/ingest/api/concepts/transactions.rst
new file mode 100644
index 0000000000..b736824e3b
--- /dev/null
+++ b/doc/ingest/api/concepts/transactions.rst
@@ -0,0 +1,257 @@
+.. _ingest-api-concepts-transactions:
+
+Transactions
+============
+
+The distributed transaction mechanism is one of the key technologies that was
+implemented in the Qserv Ingest system to allow for the incremental updates of the overall state of the data and metadata
+while ensuring the consistency of the ingested catalogs. Transactions also play an important role in allowing
+the high-performance ingest activities to be performed in a distributed environment. Transactions if used correct may
+significantly increase the level of parallelism of the ingest workflows. Transactions are not visible to end users.
+
+Transactions are open in a scope of a database. It's a responsibility of the workflows to manage transactions as needed
+for the ingest activities uisng the following REST services:
+
+- :ref:`ingest-trans-management` (REST)
+
+Isolation and parallelism
+-------------------------
+
+The first role of the transaction is to provide the isolation of the ingest activities. The transactions allow for the
+parallel ingest of the data into the tables located at the many workers of the Qserv cluster, and into the same table
+located at the same worker. The transactions are started and commited independently of each other. The transactions
+are not visible to the users and are not used for the user queries.
+
+To understand why the transactions help to increase the level of parallelism of the ingest activities, read
+the last section on this page:
+
+- :ref:`ingest-api-concepts-transactions-impl` (CONCEPTS)
+
+Row tagging
+-----------
+
+The second role of the transactions is to implement the tagging mechanism for the ingested data. All rows
+ingested into to the data tables and the *director* index tables are tagged with the transaction identifiers
+that is unique for each transaction. Hence, all contribution requests made into the tables via this API are
+associated with a specific identifier. The identifiers are usually sent in the service request and response objects
+in an attribute ``transaction_id``. As an example of the attribute, see a description of the following REST service:
+
+- :ref:`ingest-worker-contrib-by-ref` (REST)
+
+An effect of such tagging can be seen as a special column called ``qserv_trans_id`` that is automatically added by
+the Ingest system into the table schema of each table. In the current implementation of the system, this is the very
+first column of the table. The column is of the ``UNSIGNED INT`` type and is not nullable. The column is visible
+to Qserv users and is queriable. Here is an illustration of a query and the corresponding result set illustrating the concept:
+
+.. code-block:: sql
+
+ SELECT `qserv_trans_id`, `objectId`,`chunkId`
+ FROM `dp02_dc2_catalogs`.`Object`
+ WHERE `qserv_trans_id` IN (860, 861);
+
+ +----------------+---------------------+---------+
+ | qserv_trans_id | objectId | chunkId |
+ +----------------+---------------------+---------+
+ | 860 | 1249546586455828954 | 57871 |
+ | 860 | 1249546586455828968 | 57871 |
+ . . . .
+ | 861 | 1252546054176403713 | 57891 |
+ | 861 | 1252546054176403722 | 57891 |
+ +----------------+---------------------+---------+
+
+.. note::
+
+ The database administrator can decide to drop the column from the table schema if there is a need to save the space
+ in the table. The column is not used by Qserv for any other purposes than the ingest activities. And once the ingest
+ is completed, the column is not needed anymore except for Q&A-ing the data, bookeeping and data provenance.
+
+Checkpointing
+-------------
+
+Transactions also provide the checkpointing mechanism that allows rolling back to a prior consistent state of the affected tables
+should any problem occur during the ingest activities. Transactions may spans across many workers and tables located
+at the workers. It's up to the workflow to decide what contrubutions to ingest and in what order to ingest those in
+a scope of each transaction.
+
+The following diagram illustrates the concept of the transactions in Qserv. There are 3 transactions that are started and
+commited sequentially (in the real life scenarios the transactions can be and should be started and commited in parallel,
+and indepedently of each other). Data are ingested into two separate table located at 2 workers. The diagram also shows
+a failure to ingest the data into table ``Table-A`` at ``Worker-X`` in a scope of ``Transaction-2``:
+
+.. image:: /_static/ingest-transactions-failed.png
+ :target: ../../../_images/ingest-transactions-failed.png
+ :alt: Failed Transaction Contribution
+
+At this point the table ``Table-A`` at ``Worker-X`` is found in an inconsistent state. The workflow can decide to roll back
+the failed transaction and to re-try the ingest activities in a scope of the new transaction. The rollback is performed by
+the worker ingest service:
+
+- :ref:`ingest-trans-management-end` (REST)
+
+Also read the following document to learn more about the transaction abort:
+
+- :ref:`ingest-api-advanced-transactions-abort` (ADVANCED)
+
+Removing the failed transaction would result in the following state of the tables, which is *clean* and consistent:
+
+.. image:: /_static/ingest-transactions-aborted.png
+ :target: ../../../_images/ingest-transactions-aborted.png
+ :alt: Aborted Transaction
+
+After that, the workflow can re-try **ALL** ingest activities that were meant to be done in a scope of the previously
+failed transaction by starting another transaction. If the ingest activities are successful, the tables will be in the
+consistent state:
+
+.. image:: /_static/ingest-transactions-resolved.png
+ :target: ../../../_images/ingest-transactions-resolved.png
+ :alt: Another Transaction
+
+
+Transaction status and state
+----------------------------
+
+Each transaction is in a well-defined *state* at each moment of time. The state is a part of the broader collection
+if the transaction attributes called the transaction *status*. All of this can be obtained by calling services
+documented in the following section:
+
+- :ref:`ingest-trans-management-status` (REST)
+- :ref:`ingest-trans-management-states` (REST)
+
+The services provide a flexible filtering mechanism for finding the transactions of interest in various scopes and states
+and reporting the information at different levels of details as needed by the workflows or other applications.
+
+These are just a few typical applications for this information in a context of the workflows:
+
+- *Dynamic* transaction management (versus the *static* management where all transactions would be started at once):
+
+ - Starting the limited number of transactions at the beginning of the ingest
+ - Monitoring the progress and performance of the transactions
+ - Committing transactions where all table contributes were successfully ingested
+ - Starting new transactions to load more contributions to meet the performance goals
+
+- Locating the failed transactions and re-trying the ingest activities in a scope of the new transactions.
+- Locating failed table contribution requests that were made in a scope of a transaction to see if it's possible
+ to retry the contributions w/o aborting the transaction.
+- Building a Web dashboard.
+
+Contexts
+--------
+
+When starting (or finishing a transaction) using the corresponding services (see below) a workflow may optionally
+attach an piece of arbitrary workflow-defined information (the JSON object) to the transaction. The object is called
+the *context*. It will be stored within the Replication/Ingest system's database and be associated with the transaction.
+The object could be as large as ``16 MB``. In effect, the context is a part of the transaction's persistent state.
+
+The initial version of the context object is passed along the transaction start request in the attribute ``context``:
+
+- :ref:`ingest-trans-management-start` (REST)
+
+The context object may also be updated when aborting or committing a transaction by:
+
+- :ref:`ingest-trans-management-end` (REST)
+
+Contexts are also retrieved by the status retrieval services:
+
+- :ref:`ingest-trans-management-status` (REST)
+
+The workflow may use the contexts for the following reasons:
+
+- Store the information on the input contributions made in a scope of a transaction to be used later for the recovery
+ from the failures. The information may include locations of the input files, as well as any other information
+ allowing to retry the contributions. Making the workflows to depend on the contexts may simplify the implementation
+ of the workflows by allowing to avoid the need to store the information in the external databases or files.
+ Altogether, the contexts may improve robustness of the workflows.
+- Store the information for the purpose of internal bookkeeping that would be independent of the user workflow's
+ infrastructure or environment.
+- Store the additional information to be used as a source of metadata for data provenance systems.
+
+Obviously, the workflow implementation may have its own mechanism for that, and it probably should. However, attaching
+the metadata to transactions in the persistent state of the system along with the transactions has a few important benefits.
+In particular, it guarantees consistency between transactions and contexts. Secondly, it provides the precise timing for
+the ingest operations (the start and finish times are measured by the Ingest system at the right moments).
+Thirdly, the information may be seen from the general-purpose Web Dashboard application of Qserv and could also be used
+by the database support teams for building various metrics on the performance of the Qserv Ingest system.
+
+
+.. _ingest-api-concepts-transactions-impl:
+
+Implementation Details
+----------------------
+
+The Qserv transactions are quite different from the ones in the typical RDBMS implementations. Firstly, they are not designed
+as an an isolation mechanis for executing user queries, and the are not visible to Qserv users. In Qserv, tables that are being
+ingested are not seen or queriable by the users anyway. The main purpose of the transactions in Qserv is to allow for
+the incremental updates of the distributed state of data in Qserv across many (potentially - hundreds of) workers.
+Each worker runs its own instance of the MySQL/MariaDB server which is not aware of the of the others. Some might say that
+transactions are associated with *vertical slices* of rows in the tables that are located at the workers.
+
+The second technical problem to be addressed by the transactions is a lack of the transaction support in the MyISAM table
+engine that is used in Qserv for the data tables. The MyISAM engine is used in Qserv due to it ssimplicity and high performance.
+Unfortunately, failuires while ingesting data into the MyISAM tables can leave the table in a corrupted state. The transactions
+provide a mechanism allowing to roll back the tables to a consistent state in case of the failures. The current implementation
+of the transactions in Qserv is based on the MySQL/MariaDB partitions:
+
+- https://mariadb.com/kb/en/partitioning-overview/
+
+
+.. warning::
+
+ When the catalog is being published, the partitioned MyISAM tables are converted to the regular format.
+ This operation is performed by the Qserv Ingest system.
+ The conversion is a time-consuming operation and may take a long time to complete for
+ a single table. An observed performance of the operation per table is on a scale of ``20 MB/s`` to ``50 MB/s``.
+ However, a typical catalog will have thousands of such chunk tables which would be processed in parallel
+ at all workers of the Qserv cluster. The resulting performance of the conversion would be on a scale of
+ many ``GB/s``, and the operation would be completed in a reasonable time.
+
+ - A definition of the *reasonable time* is given rather loosely here. An overall idea is that
+ such conversion should be on the same scale (smaller) as the table ingest *per se*. A similar
+ philosophy is applied to other data management operations in Qserv besides the ingest.
+
+From a prospective of the workflows, these are the most important limitations of the transactions:
+
+- Transaction identifiers are the 32-bit unsigned integer numbers. The maximum number of the transactions that can be
+ started in the system is 2^32 - 1 = 4,294,967,295. The transactions are not re-used, so the number of the transactions
+ that can be started in the system is limited by the number of the unique transaction identifiers that can be generated
+ by the system.
+
+- The transaction with the identifier ``0`` is reserved for the system for the so called *default* transaction.
+ The workflows can't ingest any contributions in a context of that transaction, or manage this special transaction.
+
+- MySQL tables only allow up to ``8,000`` partitions per table. This is a limitation of the MySQL/MariaDB partitioning mechanism.
+ And there is a certain overhead in MySQL for each partition. Hence, it's not recommended to start more than ``1,000`` transactions
+ during the ingest.
+
+Transaction numbers directly map to the partition identifiers of the MySQL/MariaDB partitioned tables. Here is an example
+of a few chunk tables of a catalog that is still being ingested:
+
+.. code-block:: bash
+
+ -rw-rw----+ 1 rubinqsv gu 4868 Sep 10 20:48 gaia_source_1012.frm
+ -rw-rw----+ 1 rubinqsv gu 48 Sep 10 20:48 gaia_source_1012.par
+ -rw-rw----+ 1 rubinqsv gu 0 Sep 10 20:46 gaia_source_1012#P#p0.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 10 20:46 gaia_source_1012#P#p0.MYI
+ -rw-rw----+ 1 rubinqsv gu 0 Sep 10 20:46 gaia_source_1012#P#p1623.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 10 20:46 gaia_source_1012#P#p1623.MYI
+ -rw-rw----+ 1 rubinqsv gu 31000308 Sep 10 20:48 gaia_source_1012#P#p1628.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 11 19:49 gaia_source_1012#P#p1628.MYI
+ -rw-rw----+ 1 rubinqsv gu 4868 Sep 10 20:48 gaia_source_1020.frm
+ -rw-rw----+ 1 rubinqsv gu 48 Sep 10 20:48 gaia_source_1020.par
+ -rw-rw----+ 1 rubinqsv gu 0 Sep 10 20:46 gaia_source_1020#P#p0.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 10 20:46 gaia_source_1020#P#p0.MYI
+ -rw-rw----+ 1 rubinqsv gu 51622084 Sep 10 20:48 gaia_source_1020#P#p1624.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 11 19:49 gaia_source_1020#P#p1624.MYI
+ -rw-rw----+ 1 rubinqsv gu 0 Sep 10 20:46 gaia_source_1020#P#p1630.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 10 20:46 gaia_source_1020#P#p1630.MYI
+ -rw-rw----+ 1 rubinqsv gu 4868 Sep 10 20:47 gaia_source_1028.frm
+ -rw-rw----+ 1 rubinqsv gu 48 Sep 10 20:47 gaia_source_1028.par
+ -rw-rw----+ 1 rubinqsv gu 0 Sep 10 20:46 gaia_source_1028#P#p0.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 10 20:46 gaia_source_1028#P#p0.MYI
+ -rw-rw----+ 1 rubinqsv gu 739825104 Sep 10 20:48 gaia_source_1028#P#p1625.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 11 19:49 gaia_source_1028#P#p1625.MYI
+ -rw-rw----+ 1 rubinqsv gu 0 Sep 10 20:46 gaia_source_1028#P#p1629.MYD
+ -rw-rw----+ 1 rubinqsv gu 2048 Sep 10 20:46 gaia_source_1028#P#p1629.MYI
+
+This snapshot was taken by looking at the MariaDB data directory at one of the Qserv workers. Note that the tables
+are partitioned by the transaction numbers, where the transaction identifiers are the numbers after the ``#P#`` in
+the file names.
diff --git a/doc/ingest/api/index.rst b/doc/ingest/api/index.rst
new file mode 100644
index 0000000000..99353ca24b
--- /dev/null
+++ b/doc/ingest/api/index.rst
@@ -0,0 +1,21 @@
+
+.. note::
+
+ Information in this guide corresponds to the version **38** of the Qserv REST API. Keep in mind
+ that each implementation of the API has a specific version. The version number will change
+ if any changes to the implementation or the API that might affect users will be made.
+ The current document will be kept updated to reflect the latest version of the API.
+
+#####################################
+The Ingest Workflow Developer's Guide
+#####################################
+
+.. toctree::
+ :maxdepth: 4
+
+ introduction
+ concepts/index
+ simple/index
+ advanced/index
+ post-ingest/index
+ reference/index
diff --git a/doc/ingest/api/introduction.rst b/doc/ingest/api/introduction.rst
new file mode 100644
index 0000000000..9919e399f0
--- /dev/null
+++ b/doc/ingest/api/introduction.rst
@@ -0,0 +1,90 @@
+Introduction
+============
+
+This document presents an API that is available in Qserv for constructing the data ingest applications (also mentioned
+in the document as *ingest workflows*). The API is designed to provide a high-performance and reliable mechanism for
+ingesting large quantities of data where the high performance or reliability of the ingests is at stake.
+The document is intended to be a practical guide for the developers who are building those applications.
+It provides a high-level overview of the API, its main components, and the typical workflows that can be built using the API.
+
+At the very high level, the Qserv Ingest system is comprised of:
+
+- The REST server that is integrated into the Master Replication Controller. The server provides a collection
+ of services for managing metadata and states of the new catalogs to be ingested. The server also coordinates
+ its own operations with Qserv itself and the Qserv Replication System to prevent interferences with those
+ and minimize failures during catalog ingest activities.
+- The Worker Ingest REST service run at each Qserv worker node alongside the Qserv worker itself and the worker MariaDB server.
+ The role of these services is to actually ingest the client's data into the corresponding MySQL tables.
+ The services would also do an additional (albeit, minimal) preprocessing and data transformation (where or when needed)
+ before ingesting the input data into MySQL. Each worker server also includes its own REST server for processing
+ the "by reference" ingest requests as well as various metadata requests in the scope of the workers.
+
+Implementation-wise, the Ingest System heavily relies on services and functions of the Replication System including
+the Replication System's Controller Framework, various (including the Configuration) services, and the worker-side
+server infrastructure of the Replication System.
+
+Client workflows interact with the system's services via open interfaces (based on the HTTP protocol, REST services,
+JSON data format, etc.) and use ready-to-use tools to fulfill their goals of ingesting catalogs.
+
+Here is a brief summary of the Qserv Ingest System's features:
+
+- It introduces the well-defined states and semantics into the ingest process. With that, a process of ingesting a new catalog
+ now has to go through a sequence of specific steps maintaining a progressive state of the catalog within Qserv
+ while it's being ingested. The state transitions and the corresponding enforcements made by the system would
+ always ensure that the catalog would be in a consistent state during each step of the process.
+ Altogether, this model increases the robustness of the process, and it also makes it more efficient.
+
+- To facilitate and implement the above-mentioned state transitions the new system introduces a distributed
+ *tagging* and *checkpointing* mechanism called *super-transactions*. The transactions allow for incremental
+ updates of the overall state of the data and metadata while allowing to safely roll back to a prior consistent
+ state should any problem occur during data loading within such transactions.
+
+ - The data tagging capability of the transactions can be also used by the ingest workflows and by
+ the Qserv administrators for bookkeeping of the ingest activities and for the quality control of
+ the ingested catalogs.
+
+- In its very foundation, the system has been designed for constructing high-performance and parallel ingest
+ workflows w/o compromising the consistency of the ingested catalogs.
+
+- For the actual data loading, the system offers plenty of options, inluding pushing data into Qserv directly
+ via a proprietary binary protocol using :ref:`ingest-tools-qserv-replica-file`, :ref:`ingest-worker-contrib-by-val`
+ in the HTTP request body, or :ref:`ingest-worker-contrib-by-ref`. In the latter case, the input data (so called table
+ *contributions*) will be pulled by the worker services from remote locations as instructed by the ingest workflows.
+ The presently supported sources include the object stores (via the HTTP/HTTPS protocols) and the locally mounted
+ distributed filesystems (via the POSIX protocol).
+
+ - The ongoing work on the system includes the development of the support for the ingesting contributions
+ from the S3 object stores.
+
+- The data loading services also collect various information on the ongoing status of the ingest activities,
+ abnormal conditions that may occur during reading, interpreting, or loading the data into Qserv, as well
+ as the metadata for the data that is loaded. The information is retained within the persistent
+ state of the Replication/Ingest System for the monitoring and debugging purposes. A feedback is provided
+ to the workflows on various aspects of the ingest activities. The feedback is useful for the workflows to adjust their
+ behavior and to ensure the quality of the data being ingested.
+
+ - To get further info on this subject, see sections :ref:`ingest-general-error-reporting` and
+ :ref:`ingest-worker-contrib-descriptor-warnings`.
+ In addition, the API provides REST services for obtaining metadata on the state of catalogs, tables, distributed
+ transactions, contribution requests, the progress of the requested operations, etc.
+
+**What the Ingest System does NOT do**:
+
+- As per its current implementation (which may change in the future) it does not automatically partition
+ input files. This task is expected to be a responsibility of the ingest workflows. The only data format
+ is is presently supported for the table payload are ``CSV`` and ``JSON`` (primarily for ingesting
+ user-generated data products as explained in :ref:`http-frontend-ingest`).
+
+- It does not (with an exception of adding an extra leading column ``qserv_trans_id`` required by
+ the implementation of the previously mentioned *super-transactions*) pre-process the input ``CSV``
+ payload sent to the Ingest Data Servers by the workflows for loading into tables.
+ It's up to the workflows to sanitize the input data and to make them ready to be ingested into Qserv.
+
+More information on the requirements and the low-level technical details of its implementation (unless it's
+needed for the purposes of this document's goals) can be found elsewhere.
+
+It's recommended to read the document sequentially. Most ideas presented in the document are introduced in
+a section :ref:`ingest-api-concepts` and illustrated with a simple practical example in :ref:`ingest-api-simple`.
+The section is followed by a few more sections covering :ref:`ingest-api-advanced` and :ref:`ingest-api-post-ingest`.
+The :ref:`ingest-api-reference` section of the document provides complete descriptions of the REST services and tools
+mentioned in the document.
diff --git a/doc/ingest/api/post-ingest/index.rst b/doc/ingest/api/post-ingest/index.rst
new file mode 100644
index 0000000000..c26fdd6857
--- /dev/null
+++ b/doc/ingest/api/post-ingest/index.rst
@@ -0,0 +1,12 @@
+
+.. _ingest-api-post-ingest:
+
+=================================
+Post-Ingest Data Management Tasks
+=================================
+
+The following optional steps are performed after the data has been ingested:
+
+- :ref:`admin-row-counters` (ADMIN)
+- :ref:`admin-data-table-index` (ADMIN)
+- :ref:`admin-director-index` (ADMIN)
diff --git a/doc/ingest/api/reference/index.rst b/doc/ingest/api/reference/index.rst
new file mode 100644
index 0000000000..24a58e83d8
--- /dev/null
+++ b/doc/ingest/api/reference/index.rst
@@ -0,0 +1,12 @@
+
+.. _ingest-api-reference:
+
+######################
+Ingest API Reference
+######################
+
+.. toctree::
+ :maxdepth: 4
+
+ rest/index
+ tools
diff --git a/doc/ingest/api/reference/rest/controller/config.rst b/doc/ingest/api/reference/rest/controller/config.rst
new file mode 100644
index 0000000000..65f47786a6
--- /dev/null
+++ b/doc/ingest/api/reference/rest/controller/config.rst
@@ -0,0 +1,426 @@
+.. _ingest-config:
+
+Configuring parameters of the ingests
+=====================================
+
+.. _ingest-config-set:
+
+Setting configuration parameters
+--------------------------------
+
+Parameters are set for a database (regardless of the *published* status) using the following service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``PUT``
+ - ``/ingest/config``
+
+The request object has the following schema:
+
+.. code-block::
+
+ { "database" : ,
+ "SSL_VERIFYHOST" : ,
+ "SSL_VERIFYPEER" : ,
+ "CAPATH" : ,
+ "CAINFO" : ,
+ "CAINFO_VAL" : ,
+ "PROXY_SSL_VERIFYHOST" : ,
+ "PROXY_SSL_VERIFYPEER" : ,
+ "PROXY_CAPATH" : ,
+ "PROXY_CAINFO" : ,
+ "PROXY_CAINFO_VAL" : ,
+ "CURLOPT_PROXY" : ,
+ "CURLOPT_NOPROXY" : ,
+ "CURLOPT_HTTPPROXYTUNNEL" : ,
+ "CONNECTTIMEOUT" : ,
+ "TIMEOUT" : ,
+ "LOW_SPEED_LIMIT" : ,
+ "LOW_SPEED_TIME" : ,
+ "ASYNC_PROC_LIMIT" :
+ }
+
+Where:
+
+``database`` : *string* : **required**
+ The required name of a database affected by the operation.
+
+``SSL_VERIFYHOST`` : *number* = ``2``
+ The optional flag that tells the system to verify the host of the peer. If the value is set
+ to ``0`` the system will not check the host name against the certificate. Any other value would tell the system
+ to perform the check.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html.
+
+``SSL_VERIFYPEER`` : *number* = ``1``
+ The optional flag that tells the system to verify the peer's certificate. If the value is set
+ to ``0`` the system will not check the certificate. Any other value would tell the system to perform the check.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html.
+
+``CAPATH`` : *string* = ``/etc/ssl/certs``
+ The optional path to a directory holding multiple CA certificates. The system will use the certificates
+ in the directory to verify the peer's certificate. If the value is set to an empty string the system will not use
+ the certificates.
+
+ Putting the empty string as a value of the parameter will effectively turn this option off as if it has never been
+ configured for the database.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_CAPATH.html.
+
+``CAINFO`` : *string* = ``/etc/ssl/certs/ca-certificates.crt``
+ The optional path to a file holding a bundle of CA certificates. The system will use the certificates
+ in the file to verify the peer's certificate. If the value is set to an empty string the system will not use
+ the certificates.
+
+ Putting the empty string as a value of the parameter will effectively turn this option off as if it has never been
+ configured for the database.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_CAINFO.html.
+
+``CAINFO_VAL`` : *string* = ``""``
+ The optional value of a certificate bundle for a peer. This parameter is used in those cases when it's
+ impossible to inject the bundle directly into the Ingest workers' environments. If a non-empty value of the parameter
+ is provided then ingest servers will use it instead of the one mentioned (if any) in the above-described
+ attribute ``CAINFO``.
+
+ **Attention**: Values of the attribute are the actual certificates, not file paths like in the case of ``CAINFO``.
+
+``PROXY_SSL_VERIFYHOST`` : *number* = ``2``
+ The optional flag that tells the system to verify the host of the proxy. If the value is set
+ to ``0`` the system will not check the host name against the certificate. Any other value would tell the system
+ to perform the check.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_PROXY_SSL_VERIFYHOST.html.
+
+``PROXY_SSL_VERIFYPEER`` : *number* = ``1``
+ The optional flag that tells the system to verify the peer's certificate. If the value is set
+ to ``0`` the system will not check the certificate. Any other value would tell the system to perform the check.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_PROXY_SSL_VERIFYPEER.html.
+
+``PROXY_CAPATH`` : *string* = ``""``
+ The optional path to a directory holding multiple CA certificates. The system will use the certificates
+ in the directory to verify the peer's certificate. If the value is set to an empty string the system will not use
+ the certificates.
+
+ Putting the empty string as a value of the parameter will effectively turn this option off as if it has never been
+ configured for the database.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_PROXY_CAPATH.html.
+
+``PROXY_CAINFO`` : *string* = ``""``
+ The optional path to a file holding a bundle of CA certificates. The system will use the certificates
+ in the file to verify the peer's certificate. If the value is set to an empty string the system will not use
+ the certificates.
+
+ Putting the empty string as a value of the parameter will effectively turn this option off as if it has never been
+ configured for the database.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_PROXY_CAINFO.html.
+
+``PROXY_CAINFO_VAL`` : *string* = ``""``
+ The optional value of a certificate bundle for a proxy. This parameter is used in those cases when it's
+ impossible to inject the bundle directly into the Ingest workers' environments. If a non-empty value of the parameter
+ is provided then ingest servers will use it instead of the one mentioned (if any) in the above-described
+ attribute ``PROXY_CAINFO``.
+
+ **Attention**: Values of the attribute are the actual certificates, not file paths like in the case of ``PROXY_CAINFO``.
+
+``CURLOPT_PROXY`` : *string* = ``""``
+ Set the optional proxy to use for the upcoming request. The parameter should be a null-terminated string
+ holding the host name or dotted numerical IP address. A numerical IPv6 address must be written within ``[brackets]``.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_PROXY.html.
+
+``CURLOPT_NOPROXY`` : *string* = ``""``
+ The optional string consists of a comma-separated list of host names that do not require a proxy
+ to get reached, even if one is specified.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_NOPROXY.html.
+
+``CURLOPT_HTTPPROXYTUNNEL`` : *number* = ``0``
+ Set the optional tunnel parameter to ``1`` to tunnel all operations through the HTTP proxy
+ (set with ``CURLOPT_PROXY``).
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_HTTPPROXYTUNNEL.html.
+
+``CONNECTTIMEOUT`` : *number* = ``0``
+ The optional maximum time in seconds that the system will wait for a connection to be established.
+ The default value means that the system will wait indefinitely.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html
+
+``TIMEOUT`` : *number* = ``0``
+ The optional maximum time in seconds that the system will wait for a response from the server.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_TIMEOUT.html
+
+``LOW_SPEED_LIMIT`` : *number* = ``0``
+ The optional transfer speed in bytes per second that the system considers too slow and will abort the transfer.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_LIMIT.html
+
+``LOW_SPEED_TIME`` : *number* = ``0``
+ The optional time in seconds that the system will wait for the transfer speed to be above the limit
+ set by ``LOW_SPEED_LIMIT``.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_TIME.html
+
+``ASYNC_PROC_LIMIT`` : *number* = ``0``
+ The optional maximum concurrency limit for the number of contributions to be processed in a scope of
+ the database. The actual number of parallel requests may be further lowered by the hard limit specified by
+ the Replication System worker's configuration parameter (``worker``, ``num-async-loader-processing-threads``).
+ The parameter can be adjusted in real time as needed. It gets into effect immediately. Putting ``0`` as a value of
+ the parameter will effectively turn this option off as if it has never been configured for the database.
+
+ This attribute directly maps to https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_TIME.html
+
+ **Note**: The parameter is available as of API version ``14``.
+
+If a request is successfully finished it returns the standard JSON object w/o any additional data but
+the standard completion status.
+
+.. _ingest-config-get:
+
+Retrieving configuration parameters
+-----------------------------------
+
+.. warning::
+ As of version ``14`` of the API, the name of the database is required to be passed in the request's query instead of
+ passing it in the JSON body. The older implementation was wrong.
+
+
+.. list-table::
+ :widths: 10 25 65
+ :header-rows: 1
+
+ * - method
+ - service
+ - query parameters
+ * - ``GET``
+ - ``/ingest/config``
+ - ``database=``
+
+Where the mandatory query parameter ``database`` specifies the name of a database affected by the operation.
+
+If the operation is successfully finished it returns an extended JSON object that has the following schema (in addition
+to the standard status and error reporting attributes):
+
+.. code-block::
+
+ { "database" : ,
+ "SSL_VERIFYHOST" : ,
+ "SSL_VERIFYPEER" : ,
+ "CAPATH" : ,
+ "CAINFO" : ,
+ "CAINFO_VAL" : ,
+ "PROXY_SSL_VERIFYHOST" : ,
+ "PROXY_SSL_VERIFYPEER" : ,
+ "PROXY_CAPATH" : ,
+ "PROXY_CAINFO" : ,
+ "PROXY_CAINFO_VAL" : ,
+ "CURLOPT_PROXY" : ,
+ "CURLOPT_NOPROXY" : ,
+ "CURLOPT_HTTPPROXYTUNNEL" : ,
+ "CONNECTTIMEOUT" : ,
+ "TIMEOUT" : ,
+ "LOW_SPEED_LIMIT" : ,
+ "LOW_SPEED_TIME" : ,
+ "ASYNC_PROC_LIMIT" :
+ }
+
+The attributes of the response object are the same as the ones described in the section :ref:`ingest-config-set`.
+
+.. _ingest-config-global-workers:
+
+Global configuration parameters of workers
+------------------------------------------
+
+.. note::
+ This is the same service that was described in:
+
+ - :ref:`ingest-db-table-management-config` (REST)
+
+ The response object of the service also returns the information on the workers.
+
+There are two sectons related to workers in the response object. The first section ``config.general.worker``
+includes the general parameters of the ingest services. Values of the parameters are the same for all
+workers. The second section ``config.workers`` has the information on the individual workers.
+
+The general information on all workers
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The schema of the relevant section of the respionse object is illustrated by the following example:
+
+.. code-block:: json
+
+ { "config": {
+ "general" : {
+ "worker" : {
+ "num-loader-processing-threads" : 64,
+ "num-http-loader-processing-threads" : 8,
+ "num-async-loader-processing-threads" : 8,
+
+ "ingest-charset-name" : "latin1",
+
+ "ingest-max-retries" : 10,
+ "ingest-num-retries" : 1,
+
+ "loader-max-warnings" : 64,
+
+ "async-loader-auto-resume" : 1,
+ "async-loader-cleanup-on-resume" : 1,
+ },
+ }
+ }
+ }
+
+Where:
+
+``config.general.worker`` : *object*
+ A collection of the general parameters of the worker ingest service.
+
+``num-loader-processing-threads`` : *number*
+ The number of ingest request processing threads in the service that supports the proprietary
+ binary protocol.
+
+``num-http-loader-processing-threads`` : *number*
+ The number of ingest request processing threads in the HTTP-based ingest service. Note that
+ the service is used for processing *synchronous* contribution requess and for submitting
+ the *asynchronous* requests to the service.
+
+``num-async-loader-processing-threads`` : *number*
+ The number of ingest request processing threads in a thread pool that processes
+ the *asynchronous* contribution requests.
+
+``ingest-charset-name`` : *string*
+ The name of a character set for parsing the payload of the contributions.
+
+``ingest-max-retries`` : *number*
+ The maximum number of the automated retries of failed contribution attempts
+ in cases when such retries are still possible. The parameter represents the *hard*
+ limit for the number of retries regardless of what's specified in the related
+ parameter ``ingest-num-retries`` or in the contributions requests made by the workflows.
+ The primary purpose of the parameter is to prevent accidental overloading
+ of the ingest system should a very large number of retries accidentally specified
+ by the ingest workflows for individual contributions. Setting a value of the parameter
+ to ``0`` will unconditionally disable any retries.
+
+``ingest-num-retries`` : *number*
+ The default number of the automated retries of failed contribution attempts
+ in cases when such retries are still possible. The limit can be changed for
+ individual contributions. Note that the effective number of retries specified
+ by this parameter or the one set in the contribution requests can not
+ exceed the *hard* limit set in the related parameter ``ingest-max-retries``.
+ Setting a value of the parameter to 0 will disable automatic retries (unless they are
+ explicitly enabled or requested by the ingest workflows for individual contributions).
+
+``loader-max-warnings`` : *number*
+ The maximum number of warnings to retain after executing ``LOAD DATA [LOCAL] INFILE``
+ when ingesting contributions into worker MySQL database. The warnings (if any) will be recorded in
+ the persisent state of the Replication/Ingest system and returned to the ingest workflow upon request.
+
+``async-loader-auto-resume`` : *number*
+ The flag controlling the behavior of the worker's *asynchronous* ingest service after
+ (the deliberate or accidental) restarts. If the value of the parameter is not ``0`` then the service
+ will resume processing incomplete (queued or on-going) requests. Setting a value of the parameter
+ to ``0`` will result in the unconditional failing of all incomplete contribution requests existed prior
+ the restart.
+
+ .. warning::
+
+ Requests failed in the last (loading) stage can't be resumed, and they will require aborting
+ the corresponding transaction. If the automaticu resume is enabled rhese request will be automatically
+ closed and marked as failed.
+
+``async-loader-cleanup-on-resume`` : *number*
+ The flag controlling the behavior of worker's *asynchronous* ingest service after
+ restarting the service. If the value of the parameter is not ``0`` the service will try to clean
+ up the temporary files that might be left on disk for incomplete (queued or ongoing) requests.
+ The option may be disabled to allow debugging the service.
+
+Worker-specific information
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The schema of the relevant section of the respionse object is illustrated by the following example:
+
+.. code-block:: json
+
+ { "config": {
+ "workers" : [
+ { "name" : "db02",
+ "is-enabled" : 1,
+ "is-read-only" : 0,
+
+ "loader-host" : {
+ "addr" : "172.24.49.52",
+ "name" : "sdfqserv002.sdf.slac.stanford.edu"
+ },
+ "loader-port" : 25002,
+ "loader-tmp-dir" : "/qserv/data/ingest",
+
+ "http-loader-host" : {
+ "name" : "sdfqserv002.sdf.slac.stanford.edu",
+ "addr" : "172.24.49.52"
+ },
+ "http-loader-port" : 25004,
+ "http-loader-tmp-dir" : "/qserv/data/ingest",
+ },
+ ]
+ }
+ }
+
+Where:
+
+``config.workers`` : *array*
+ A collection of worker nodes, where each object represents a worker node.
+
+``name`` : *string*
+ The unique identifier of a worker node.
+
+``is-enabled`` : *number*
+ The flag that tells if the worker node is enabled. If the value is set to ``0`` the worker node is disabled.
+ Workers which are not enables do not participate in the ingest activities.
+
+``is-read-only`` : *number*
+ The flag that tells if the worker node is read-only. If the value is set to ``0`` the worker node is read-write.
+ Workers which are in the read-only statte do not participate in the ingest activities.
+
+**Parameters of the ingest service that supports the proprietary binary protocol**:
+
+``loader-host`` : *object*
+ The object with the information about the loader host.
+
+ - ``addr`` : *string*
+ The IP address of the lder host.
+
+ - ``name`` : *string*
+ The FQDN (fully-qualified domain name) of the host.
+
+``loader-port`` : *number*
+ The port number of the ingest service.
+
+``loader-tmp-dir`` : *string*
+ The path to the temporary directory on the loader host that is used by the ingest service
+ as a staging area for the contributions.
+
+**Parameters of the HTTP-based ingest service**:
+
+``http-loader-host`` : *object*
+ The object with the information about the loader host.
+
+ - ``addr`` : *string*
+ The IP address of the lder host.
+
+ - ``name`` : *string*
+ The FQDN (fully-qualified domain name) of the host.
+
+``http-loader-port`` : *number*
+ The port number of the ingest service.
+
+``http-loader-tmp-dir`` : *string*
+ The path to the temporary directory on the loader host that is used by the ingest service
+ as a staging area for the contributions.
diff --git a/doc/ingest/api/reference/rest/controller/db-table-management.rst b/doc/ingest/api/reference/rest/controller/db-table-management.rst
new file mode 100644
index 0000000000..f7624f8eb8
--- /dev/null
+++ b/doc/ingest/api/reference/rest/controller/db-table-management.rst
@@ -0,0 +1,559 @@
+Database and table management
+=============================
+
+.. _ingest-db-table-management-config:
+
+Finding existing databases and database families
+------------------------------------------------
+
+The following service pulls all configuration information of of the Replication/Ingest System, including info
+on the known database families, databases and tables:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``GET``
+ - ``/replication/config``
+
+Upon successful (see :ref:`ingest-general-error-reporting`) completion of the request, the service will return an object
+that has the following schema (of which only the database and database family-related fields are shown):
+
+.. code-block:: json
+
+ {
+ "config": {
+ "database_families" : [
+ {
+ "overlap" : 0.01667,
+ "min_replication_level" : 3,
+ "num_sub_stripes" : 3,
+ "name" : "production",
+ "num_stripes" : 340
+ }
+ ],
+ "databases" : [
+ {
+ "database" : "dp01_dc2_catalogs_02",
+ "create_time" : 0,
+ "is_published" : 1,
+ "publish_time" : 1662688661000,
+ "family_name" : "production",
+ "tables" : [
+ {
+ "ang_sep" : 0,
+ "is_director" : 1,
+ "latitude_key" : "coord_dec",
+ "create_time" : 1662774817703,
+ "unique_primary_key" : 1,
+ "flag" : "",
+ "name" : "Source",
+ "director_database_name" : "",
+ "is_ref_match" : 0,
+ "is_partitioned" : 1,
+ "longitude_key" : "coord_ra",
+ "database" : "dp02_dc2_catalogs",
+ "director_table" : "",
+ "director_key2" : "",
+ "director_database_name2" : "",
+ "director_key" : "sourceId",
+ "director_table2" : "",
+ "director_table_name2" : "",
+ "is_published" : 1,
+ "director_table_name" : "",
+ "publish_time" : 1663033002753,
+ "columns" : [
+ {
+ "name" : "qserv_trans_id",
+ "type" : "INT NOT NULL"
+ },
+ {
+ "type" : "BIGINT NOT NULL",
+ "name" : "sourceId"
+ },
+ {
+ "type" : "DOUBLE NOT NULL",
+ "name" : "coord_ra"
+ },
+ {
+ "type" : "DOUBLE NOT NULL",
+ "name" : "coord_dec"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+
+**Notes**:
+
+- The sample object was truncated for brevity. The actual number of families, databases, tables and columns were
+ much higher in the real response.
+- The number of attributes varies depending on a particular table type. The example above shows
+ attributes for the table ``Source``. This table is *partitioned* and is a *director* (all *director*-type tables
+ are partitioned in Qserv).
+
+
+.. _ingest-db-table-management-register-db:
+
+Registering databases
+----------------------
+
+Each database has to be registered in Qserv before one can create tables and ingest data. The following
+service of the Replication Controller allows registering a database:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``POST``
+ - ``/ingest/database``
+
+The service requires a JSON object of the following schema:
+
+.. code-block::
+
+ {
+ "database" : ,
+ "num_stripes" : ,
+ "num_sub_stripes" : ,
+ "overlap" : ,
+ "auto_build_secondary_index" : ,
+ "local_load_secondary_index" :
+ }
+
+Where:
+
+``database`` : *string*
+ The required name of the database to be created.
+
+``num_stripes`` : *number*
+ The required number of stripes that was used when partitioning data of all tables to be ingested in a scope of the database.
+
+``num_sub_stripes`` : *number*
+ The required number of sub-stripes that was used when partitioning data of all tables to be ingested in a scope of the database.
+
+``overlap`` : *number*
+ The required overlap between the stripes.
+
+``auto_build_secondary_index`` : *number* = ``1``
+ The flag that specifies the desired mode for building the *director* (used to be known as the *secondary*)
+ indexes of the director tables of the catalog. The flag controls the automatic building of the indexes, where:
+
+ - ``1``: Build the index automatically during transaction commit time.
+ - ``0``: Do not build the index automatically during transaction commit time. In this case, it will be up to a workflow
+ to trigger the index building as a separated "post-ingest" action using the corresponding service:
+
+ - :ref:`ingest-director-index-build`
+
+ **Note**: Catalogs in Qserv may have more than one director table. This option applies to all such tables.
+
+.. warning::
+
+ - The service will return an error if the database with the same name already exists in the system.
+ - Values of attributes ``num_stripes``, ``num_sub_stripes`` and ``overlap`` are expected to match
+ the corresponding partitioning parameters used when partitioning all partitioned tables of the new database.
+ Note that the current implementation of the Qserv Ingest system will not validate contributions to the partitioned
+ tables to enforce this requirement. Only the structural correctness will be checked. It's up to a workflow
+ to ensure the data ingested into tables are correct.
+ - Building the *director* index during transaction commit time (for the relevant tables) may have a significant
+ impact on the performance of the transaction commit operation. The impact is proportional to the size of the
+ contributions made into the table during the transaction. This may orotolng the transaction commit time.
+ An alternative option is to build the indexes as a separated "post-ingest" action using the corresponding service:
+
+ - :ref:`ingest-director-index-build`
+
+If the operation is successfully finished (see :ref:`ingest-general-error-reporting`) a JSON object returned by the service
+will have the following attribute:
+
+.. code-block::
+
+ {
+ "database": {
+ ...
+ }
+ }
+
+The object containing the database configuration information has the same schema as it was explained earlier in section:
+
+- :ref:`ingest-db-table-management-config`
+
+
+.. _ingest-db-table-management-register-table:
+
+Registering tables
+------------------
+
+All tables, regardless if they are *partitioned* or *regular* (fully replicated on all worker nodes), have to be registered
+using the following Replication Controller's service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``POST``
+ - ``/ingest/table``
+
+The service requires a JSON object of the following schema:
+
+Where a JSON object sent to the service with the request shall describe that table. This is a schema of the object for
+the **partitioned** tables is presented below:
+
+.. code-block::
+
+ {
+ "database" : ,
+ "table" : ,
+ "is_partitioned" : ,
+ "schema" : [
+ { "name" : ,
+ "type" :
+ },
+ ...
+ ],
+ "director_table" : ,
+ "director_key" : ,
+ "director_table2" : ,
+ "director_key2" : ,
+ "latitude_key" : ,
+ "longitude_key" : ,
+ "flag" : ,
+ "ang_sep" : ,
+ "unique_primary_key" :
+ }
+
+A description of the *regular* tables has a fewer number of attributes (attributes that which are specific to the *partitioned*
+tables are missing):
+
+.. code-block::
+
+ {
+ "database" : ,
+ "table" : ,
+ "is_partitioned" : ,
+ "schema": [
+ {
+ "name" : ,
+ "type" :
+ },
+ ...
+ ]
+ }
+
+Where the attributes are:
+
+``database`` : *string*
+ The required name of the existing database.
+
+``table`` : *string*
+ The required name of a table to be created.
+
+``is_partitioned`` : *number*
+ The required type of table. Allowed values:
+
+ - ``1`` for partitioned tables (including any subtypes)
+ - ``0`` for the regular tables.
+
+``schema`` : *array*
+ The required definition of the table schema, where each entry of the array is an object with the following attributes:
+
+ - ``name``: The name of the column.
+ - ``type``: The type of the column. The type must adhere to the MySQL requirements for column types.
+
+``director_table`` : *string*
+ The name of the corresponding first (or left) *director* table. The name is required to be not empty for
+ the *dependent* tables and it has to be empty for the *director* tables. This is the only way to differentiate between
+ two types of *partitioned* tables.
+
+ **Note**: The *ref-match* tables are considered as the *dependent* tables since they have columns that are pointing
+ to the corresponding *director* tables. See attributes: ``director_key``, ``director_table2``, and ``director_key2``.
+
+``director_key`` : *string*
+ The required name of a column in a *partitioned* table. A role of the column depends on a subtype of
+ the table:
+
+ - *director*: the primary key of the table
+ - *dependent*: the foreign key pointing to the corresponding column of the *director* table
+
+``director_table2`` : *string*
+ The name of the corresponding second (or right) *director* table. The non-empty value
+ name is required for the *ref-match* tables and it has to be empty for the *director* and *dependent* tables.
+
+ **Note**: The very presence of this attribute in the input configuration would imply an intent to register
+ a "ref-match* table. In this case, non-empty values of the attributes ``director_key2`` , ``flag`` and ``ang_sep``
+ will be required in order to succeed with the registration.
+
+``director_key2`` : *string*
+ The name of a column that is associated (AKA *foreign key*) with corresponding column of the second *director* table.
+ A value of this attribute is required for and it must not be empty when registering the *ref-match* tables.
+ It will be ignored for other table types. See a description of the attribute ``director_table2``.
+
+``latitude_key`` : *string*
+ The required name of a column in a *director* table represents latitude. It's optional for the *dependent* tables.
+
+``longitude_key`` : *string*
+ The required name of a column in a *director* table represents longitude. It's optional for the *dependent* tables.
+
+``flag`` : *string*
+ The name of the special column that is required to be present on the *ref-match* tables.
+ Values of the column are populated by the tool ``sph-partition-matches`` when partitioning the input files
+ of the *ref-match* tables. The data type of this column is usually:
+
+ .. code-block:: sql
+
+ INT UNSIGNED
+
+``ang_sep`` : *double*
+ The value of the angular separation for the matched objects that is used by Qserv to process queries which
+ involve the *ref-match* tables. The value is in radians. The value is required to be non-zero for the *ref-match* tables.
+
+``unique_primary_key`` : *number* = ``0``
+ The optional flag allows to drop the uniqueness requirement for the *director* keys of the table. The parameter
+ is meant to be used for testing new table products, or for the *director* tables that won't have any dependants (child tables).
+ Allowed values:
+
+ - ``0``: The primary key is not unique.
+ - ``1``: The primary key is unique.
+
+.. warning::
+
+ - The table schema does not include definitions of indexes. Those are managed separately after the table is published.
+ The index management interface is documented in a dedicated document
+
+ - **TODO**: Managing indexes of MySQL tables at Qserv workers.
+
+ - The service will return an error if the table with the same name already exists in the system, or
+ if the database didn' exist at a time when teh request was delivered to the service.
+
+ - The service will return an error if the table schema is not correct. The schema will be checked for the correctness.
+
+.. note:: Requirements for the table schema:
+
+ - The variable-length columns are not allowed in Qserv for the *director* and *ref-match* tables. All columns of these
+ tables must have fixed lengths. These are the variable length types: ``VARCHAR``, ``VARBINARY``, ``BLOB``, ``TEXT``,
+ ``GEOMETRY`` and ``JSON``.
+
+ - The *partitioned* tables are required to have parameters ``director_key``, ``latitude_key`` and ``longitude_key``.
+ - The *director* tables are required to have non-empty column names in the parameters ``director_key``, ``latitude_key`` and ``longitude_key``.
+ - The *dependent* tables are required to have a non-empty column name specified in the parameter ``director_key``.
+ - The *dependent* tables are allowed to have empty values in the parameters ``latitude_key`` and ``longitude_key``.
+
+ - For tables where the attributes ``latitude_key`` and ``longitude_key`` are provided (either because they are required
+ of if they are optional), values must be either both non-empty or empty. An attempt to specify only one of the attribute
+ or have a non-empty value in an attribute while the other one has it empty will result in an error.
+
+ - All columns mentioned in attributes ``director_key``, ``director_key2``, ``flag``, ``latitude_key`` and ``longitude_key``
+ must be present in the table schema.
+
+ - Do not use quotes around the names or type specifications.
+
+ - Do not start the columm names with teh reserved prefix ``qserv``. This prefix is reserved for the Qserv-specific columns.
+
+An example of the schema definition for the table ``Source``:
+
+.. code-block:: json
+
+ [
+ {
+ "name" : "sourceId"
+ "type" : "BIGINT NOT NULL",
+ },
+ {
+ "name" : "coord_ra"
+ "type" : "DOUBLE NOT NULL",
+ },
+ {
+ "name" : "coord_dec"
+ "type" : "DOUBLE NOT NULL",
+ }
+ ]
+
+If the operation is successfully finished (see :ref:`ingest-general-error-reporting`) a JSON object returned by the service
+will have the following attribute:
+
+.. code-block::
+
+ {
+ "database": {
+ ...
+ }
+ }
+
+The object will contain the updated database configuration information that will also include the new table.
+The object will have the same schema as it was explained earlier in section:
+
+- :ref:`ingest-db-table-management-config`
+
+**Notes on the table names**:
+
+- Generally, the names of the tables must adhere to the MySQL requirements for identifiers
+ as explained in:
+
+ - https://dev.mysql.com/doc/refman/8.0/en/identifier-qualifiers.html
+
+- The names of identifiers (including tables) in Qserv are case-insensitive. This is not the general requirement
+ in MySQL, where the case sensitivity of identifiers is configurable one way or another. This requirement
+ is enforced by the configuration of MySQL in Qserv.
+
+- The length of the name should not exceed 64 characters as per:
+
+ - https://dev.mysql.com/doc/refman/8.0/en/identifier-length.html
+
+- The names should **not** start with the prefix ``qserv``. This prefix is reserved for the Qserv-specific tables.
+
+
+.. _ingest-db-table-management-publish-db:
+
+Publishing databases
+--------------------
+
+Databases are published (made visible to Qserv users) by calling this service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``PUT``
+ - ``/ingest/database/:database``
+
+The name of the database is provided as a parameter ``database`` of the resource path. There are a few optional
+parameters to be sent in the JSON body of the request:
+
+.. code-block::
+
+ {
+ "consolidate_secondary_index" : ,
+ "row_counters_deploy_at_qserv" :
+ }
+
+Where:
+
+``consolidate_secondary_index`` : *number* = ``0``
+ The optional parameter that controls the final format of all the *director* index tables of the database.
+ Normally, the *director* indexes are MySQL-partitioned tables. If the value of this optional parameter is
+ not ``0`` then the Ingest System will consolidate the MySQL partitions and turn the tables into the monolitical form.
+
+ .. warning::
+
+ Depending on the scale of the catalog (sizes of the affected tables), this operation may be quite lengthy (up to many hours).
+ Besides, based on the up to the date experience with using the MySQL-partitioned director indexes, the impact of the partitions
+ on the index's performance is rather negligible. So, it's safe to ignore this option in most but very special cases that are not
+ discussed by the document.
+
+ One can find more info on the MySQL partitioning at:
+
+ - https://dev.mysql.com/doc/refman/8.0/en/partitioning.html
+
+``row_counters_deploy_at_qserv`` : *number* = ``0``
+ This optional flag that triggers scanning and deploying the row counters as explained at:
+
+ - :ref:`admin-row-counters` (ADMIN)
+ - :ref:`ingest-row-counters-deploy` (REST)
+
+ To trigger this operation the ingest workflow should provide a value that is not ``0``. In this case the row counters
+ collection service will be invoked with the following combination of parameters:
+
+ .. list-table::
+ :widths: 50 50
+ :header-rows: 1
+
+ * - attr
+ - value
+ * - ``overlap_selector``
+ - ``CHUNK_AND_OVERLAP``
+ * - ``force_rescan``
+ - ``1``
+ * - ``row_counters_state_update_policy``
+ - ``ENABLED``
+ * - ``row_counters_deploy_at_qserv``
+ - ``1``
+
+.. warning::
+
+ The row counters deployment is a very resource-consuming operation. It may take a long time to complete
+ depending on the size of the catalog. This will also delay the catalog publiushing stage of an ingest compaign.
+ A better approach is to deploy the row counters as the "post-ingest" operation as explained in:
+
+ - (**TODO** link) Deploying row counters as a post-ingest operation
+
+.. note::
+
+ The catalogs may be also unpublished to add more tables. The relevant REST service is documented in:
+
+ - (**TODO** link) Un-publishing databases to allow adding more tables
+
+
+.. _ingest-db-table-management-unpublish-db:
+
+Un-publishing databases to allow adding more tables
+---------------------------------------------------
+
+Unpublished databases as well as previously ingested tables will be still visible to users of Qserv.
+The main purpose of this operation is to allow adding new tables to the existing catalogs.
+The new tables won't be seen by users until the catalog is published back using the following REST service:
+
+- :ref:`ingest-db-table-management-publish-db`
+
+Databases are un-published by calling this service:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``PUT``
+ - ``/replication/config/database/:database``
+
+The name of the database is provided in a parameter ``database`` of the resource. The only mandatory parameter
+to be sent in the JSON body of the request is:
+
+``admin_auth_key`` : *string*
+ The administrator-level authentication key that is required to publish the database.
+ The key is used to prevent unauthorized access to the service.
+
+ **Note**: The key is different from the one used to publish the database. The elevated privileges
+ are needed to reduce risks of disrupting user access to the previously loaded and published databases.
+
+
+.. _ingest-db-table-management-delete:
+
+Deleting databases and tables
+-----------------------------
+
+These services can be used for deleting non-*published* (the ones that are still ingested) as well as *published* databases,
+or tables, including deleting all relevant persistent structures from Qserv:
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``DELETE``
+ - | ``/ingest/database/:database``
+ | ``/ingest/table/:database/:table``
+
+To delete a non-*published* database (or a table from such database) a client has to provide the normal level authentication
+key ``auth_key`` in a request to the service:
+
+.. code-block::
+
+ { "auth_key" :
+ }
+
+The name of the databases affected by the operation is specified at the resource's path.
+
+Deleting databases (or tables from those databases) that have already been published requires a user to have
+elevated administrator-level privileges. These privileges are associated with the authentication key ``admin_auth_key``
+to be sent with a request instead of ``auth_key``:
+
+.. code-block::
+
+ { "admin_auth_key" :
+ }
+
+Upon successful completion of the request (for both above-mentioned states of the database), the service will return the standard
+response as explained in the section mentoned below. After that, the database (or the table, depending on a scope of a request)
+name can be reused for further ingests if needed.
+
+- :ref:`ingest-general-error-reporting`
+
diff --git a/doc/ingest/api/reference/rest/controller/director-index.rst b/doc/ingest/api/reference/rest/controller/director-index.rst
new file mode 100644
index 0000000000..33595bdefd
--- /dev/null
+++ b/doc/ingest/api/reference/rest/controller/director-index.rst
@@ -0,0 +1,139 @@
+Director Index Management
+=========================
+
+.. _ingest-director-index-build:
+
+(Re-)building the Index
+-----------------------
+
+.. note:: API version notes:
+
+ - As of version ``21``, the service can no longer be used to (re-)build indexes of all *director*
+ tables of a catalog. It's now required to provide the name of the affected table in the parameter ``director_table``.
+
+ - As of version ``22``, the service no longer support the option ``allow_for_published``. Any attempts to specify
+ the option will result in a warning reported by the service back to a client. The service will ignore the option.
+
+.. warning::
+ Be advised that the amount of time needed to build an index of a large-scale catalog may be quite large.
+ The current implementation of the secondary index is based on MySQL's InnoDB table engine. The insert
+ time into this B-Tree table has logarithmic performance. It may take many hours to build catalogs of
+ billions of objects. In some earlier tests, the build time was 20 hours for a catalog of 20 billion objects.
+
+
+The service of the **Master Replication Controller** builds or rebuilds (if needed) the *director* (used to be known as
+the *secondary*) index table of a database. The target table must be *published* at the time of this operation.
+
+.. list-table::
+ :widths: 10 90
+ :header-rows: 0
+
+ * - ``POST``
+ - ``/ingest/index/secondary``
+
+The request object has the following schema:
+
+.. code-block::
+
+ { "database" : ,
+ "director_table" : ,
+ "rebuild" : ,
+ "local" :
+ }
+
+Where:
+
+``database`` : *string*
+ The required name of a database affected by the operation.
+
+``director_table`` : *string*
+ The required name of the *director* table for which the index is required to be (re-)built.
+
+``rebuild`` : *number* = ``0``
+ The optional flag that allows recreating an existing index. If the value is set to ``0`` the service
+ will refuse to proceed with the request if the index already exists. Any other value would tell the service
+ to drop (if exists) the index table before re-creating and re-populating it with entries.
+
+``local`` : *number* = ``0``
+ The optional flag that tells the service how to ingest data into the index table, where:
+
+ - ``0``: Index contributions are required to be directly placed by the Replication/Ingest System at a location
+ that is directly accessible by the MySQL server hosting the index table. This could be either some local folder
+ of a host where the service is being run or a folder located at a network filesystem mounted on the host.
+ Once a file is in place, it would be ingested into the destination table using this protocol:
+
+ .. code-block:: sql
+
+ LOAD DATA INFILE ...
+
+ **Note**: Be aware that this option may not be always possible (or cause complications) in Kubernetes-based
+ deployments of Qserv.
+
+ - ``1`` (or any other numeric value): Index contributions would be ingested into the table using this protocol:
+
+ .. code-block:: sql
+
+ LOAD DATA LOCAL INFILE ...
+
+ **Note**: Files would be first copied by MySQL at some temporary folder owned by the MySQL service before being
+ ingested into the table. This option has the following caveats:
+
+ - The protocol must be enabled in the MySQL server configuration by setting a system variable: ``local_infile=1``.
+ - The temporary folder of the MySQL server is required to have sufficient space to temporarily accommodate index
+ contribution files before they'd be loaded into the table. In the worst-case scenario, there should be enough
+ space to accommodate all contributions of a given catalog. One could make a reasonable estimate for the latter
+ by knowing the total number of rows in the director table of the catalog, the size of the primary
+ key (typically the ``objectId`` column) of the table, as well as types of the ``chunk`` and ``subChunk``
+ columns (which are usually the 32-bit integer numbers in Qserv).
+ - This ingest option would also affect (lower) the overall performance of the operation due to additional
+ data transfers required for copying file contributions from a location managed by the **Master Replication Controller**
+ to the temporary folder of the MySQL server.
+
+If the operation succeeded, the service will respond with the default JSON object which will not carry any additional
+attributes on top of what's mandated in :ref:`ingest-general-error-reporting`.
+
+In case of errors encountered during an actual attempt to build the index was made, the object may have a non-trivial
+value of the ``error_ext``. The object wil carry specific reasons for the failures. The schema of the object
+is presented below:
+
+.. code-block::
+
+ "error_ext" : {
+
: {
+ : {
+ : ,
+ ...
+ },
+ },
+ ...
+ }
+
+Where:
+
+``table`` : *string*
+ The placeholder for the name of the director table.
+
+``worker`` : *string*
+ The placeholder for the name of the worker service that failed to build the index.
+
+``chunk`` : *number*
+ The placeholder for the chunk number.
+
+``error`` : *string*
+ The placeholder for the error message.
+
+Here is an example of how this object might look like:
+
+.. code-block::
+
+ "error_ext" : {
+ "object" : {
+ "qserv-db01" : {
+ 122 : "Failed to connect to the worker service",
+ 3456 : "error: Table 'tes96__Object' already exists, errno: 1050",
+ },
+ "qserv-db23" : {
+ 123 : "Failed to connect to the worker service"
+ }
+ }
+ }
diff --git a/doc/ingest/api/reference/rest/controller/index.rst b/doc/ingest/api/reference/rest/controller/index.rst
new file mode 100644
index 0000000000..427d79ace2
--- /dev/null
+++ b/doc/ingest/api/reference/rest/controller/index.rst
@@ -0,0 +1,14 @@
+#############################
+Master Replication Controller
+#############################
+
+.. toctree::
+ :maxdepth: 4
+
+ config
+ db-table-management
+ trans-management
+ table-location
+ info
+ director-index
+ row-counters
diff --git a/doc/ingest/api/reference/rest/controller/info.rst b/doc/ingest/api/reference/rest/controller/info.rst
new file mode 100644
index 0000000000..7194a567fa
--- /dev/null
+++ b/doc/ingest/api/reference/rest/controller/info.rst
@@ -0,0 +1,137 @@
+Information services
+====================
+
+.. _ingest-info-chunks:
+
+Chunk disposition
+-----------------
+
+.. warning::
+ Do not use this service for the chunk placement decisions during catalog ingestion. The service is for
+ informational purposes only.
+
+The service of the **Master Replication Controller** return information about the chunk *replicas* in a scope of a given database:
+
+.. list-table::
+ :widths: 10 15 75
+ :header-rows: 1
+
+ * - method
+ - service
+ - query parameters
+ * - ``GET``
+ - ``/ingest/chunks``
+ - ``database=``
+
+Where:
+
+``name`` : *string*
+ The required name of a database affected by the operation.
+
+The resulting object has the following schema:
+
+.. code-block::
+
+ {
+ "replica": [
+ {
+ "chunk" : ,
+ "worker": ,
+ "table" : {
+ : {
+ "overlap_rows" : ,
+ "overlap_data_size" : ,
+ "overlap_index_size" : ,
+ "rows" : ,
+ "data_size" : ,
+ "index_size" :
+ },
+ ...
+ }
+ },
+ ...
+ ]
+ }
+
+Where:
+
+``replica`` : *array*
+ A collection of chunk **replicas**, where each object representes a chunk replica. Replicas of a chunk
+ are essentially the same chunk, but placed on different workers.
+
+``chunk`` : *number*
+ The chunk number.
+
+``worker`` : *string*
+ The unique identifier of a worker where the chunk replica is located.
+
+``table`` : *object*
+ The object with the information about the chunk replica in the scope of
+ a particular *partitioned* table.
+
+ **Attention**: The current implementation is incomplete. It will return ``0`` for all attributes
+ of the table object.
+
+``overlap_rows`` : *number*
+ The number of rows in the chunk's overlap table.
+
+``overlap_data_size`` : *number*
+ The number of bytes in the chunk's overlap table (measured by the size of the corresponding file).
+
+``overlap_index_size`` : *number*
+ The number of bytes in the index of the chunk's overlap table (measured by the size
+ of the corresponding file).
+
+``rows`` : *number*
+ The number of rows in the chunk table.
+
+``data_size`` : *number*
+ The number of bytes in the chunk table (measured by the size of the corresponding file).
+
+``index_size`` : *number*
+ The number of bytes in the index of the chunk table (measured by the size of
+ the corresponding file).
+
+.. _ingest-info-contrib-requests:
+
+Status of the contribution request
+----------------------------------
+
+The service of the **Master Replication Controller** returns information on a contribution request:
+
+.. list-table::
+ :widths: 10 15 75
+ :header-rows: 1
+
+ * - method
+ - service
+ - query parameters
+ * - ``GET``
+ - ``/ingest/trans/contrib/:id``
+ - | ``include_warnings=<0|1>``
+ | ``include_retries=<0|1>``
+
+Where:
+
+``id`` : *number*
+ The required unique identifier of the contribution request that was submitted
+ to a Worker Ingest service earlier.
+
+``include_warnings`` : *number* = ``0``
+ The optional flag telling the service to include warnings into the response. Any value
+ that is not ``0`` is considered as ``1``, meaning that the warnings should be included.
+
+``include_retries`` : *number* = ``0``
+ The optional flag telling the service to include retries into the response. Any value
+ that is not ``0`` is considered as ``1``, meaning that the retries should be included.
+
+The resulting object has the following schema:
+
+.. code-block::
+
+ { "contribution" :