This document holds what is known about Apache Solr, for its driver (W27, D162 and D166). Steps 3 to 8a of DRIVER.md filled it, and steps 10 to 17a finished it on 2026-10-07. The headings are the template of DRIVER.md.
Steps 5a to 7 measured Solr 9.9.0, 9.10.1 and 10.0.0 in SolrCloud mode, as
admin and as dbmeta_user, on 2026-10-01 and 2026-10-02. A fact marked
"recorded" is in testdata/solr/, and the name in quotes after it is the
name of its request in requests.json there. A fact marked "measured" names
how it was measured. The three releases gave the same answers to every SQL
statement. The few differences are under Flavors. Each section starts with
the measured facts. The facts after them come from the sources, and each one
that is not measured says so. The sources are these:
- "The SQL module" is
solr/modules/sqlofgithub.com/apache/solrat its main branch, read on 2026-10-01:SQLHandler.java,SolrSchema.javaandSolrTable.java. It is the code that answers/sql. - "The JDBC driver" is
solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/sqlof the same repository, read on 2026-10-01. It is the driver of SolrJ, in Java. - "solr-go" is
github.com/stevenferrer/solr-go, a Go client of the Collections API, the Schema API, the query API and the update handler, read on 2026-10-01. It sends no SQL. - "Gemini" is
gemini-3.8-flash, and "DeepSeek" isdeepseek-flash, asked on 2026-10-01 for step 5a and on 2026-10-02 for step 7. - "The dbmeta entry" is
container/solr.goindbmeta.
- Apache Solr is a search server built on Apache Lucene. A collection holds documents, and each document holds fields. Parallel SQL reads a collection as a table, with Apache Calcite, and sends the work to the search handlers of Solr (the SQL module).
- R holds.
dbrunstartssolr-9.9.0,solr-9.10.1andsolr-10.0.0, each in the Staged tier. 9.9.0 and 10.0.0 have the cadencetested, and 9.10.1 the cadencenightly(the dbmeta entry). Each one runs SolrCloud with the ZooKeeper that Solr embeds, the modulesql, and basic authentication (measured withdbrun starton 2026-10-01). - H holds. A statement is
POST /solr/<collection>/sql, and the answer is JSON (recorded: "a statement"). - S holds. The dialect is the SQL of Calcite, read only, with the lexical rules of MySQL (recorded, and the SQL module).
- The priority is in TARGETS.md, under D162.
dburlhas no scheme for Solr, andusqlhas no driver for it (read in../dburl/scheme.goand../usql/drivers/on 2026-10-01). So no driver ofusqlis replaced.dbmetahas no model for Solr. Its entry makes the collectiondbmetawith the configuration set_default, the administratoradmin, and the ordinary userdbmeta_userwith the rolesearch, which can read a collection and run SQL on it (the dbmeta entry).- SQL in Solr cannot write.
INSERT,UPDATEandDELETEfail, and a write goes through the update handler, which is not SQL (recorded: "crud: sql insert", "crud: sql update", "crud: sql delete" and "crud: insert"). DRIVER.md says to stop and ask Ken when a server refuses one of insert, update and delete. See the open questions.
These facts were recorded on each release:
- A statement is the form field
stmtof aPOSTto/solr/<collection>/sql, sent asapplication/x-www-form-urlencoded(recorded: "a statement").GETwithstmtin the query gives the same answer (recorded: "a statement by GET"). - The collection of the path only names the handler. A statement sent to
/solr/dbmeta/sqlreads the collectiondbimpthat itsFROMnames (recorded: "a statement sent to another collection"). The path of a collection that does not exist answers 404 with an HTML page on 9.x, 405 on 10.0.0 foradmin, and 403 fordbmeta_user(recorded: "a statement sent to a collection that does not exist"). - A body of JSON, such as
{"params": {"stmt": ...}}, is not read. The answer is HTTP 200 with the errorstmt parameter cannot be null(recorded: "a statement in a body of JSON"). A request with nostmtanswers the same (recorded: "no statement"). - Authentication is HTTP basic. A request with a wrong password answers 401
with an HTML page and
WWW-Authenticate: Basic realm="solr"(recorded: "a wrong password"). A request with no credentials answers 401 (the dbmeta entry, whose check of readiness waits for that answer). includeMetadata=trueadds a first tuple that names the columns (Responses).aggregationModeisfacetormap_reduce, and changes howGROUP BYruns (recorded: "feature: a group by facet" and "feature: a group by map reduce").numWorkers=2fails aGROUP BYinmap_reducewithIndexOutOfBoundsExceptionon this server of one node (recorded: "lead: two workers").timeAllowed=1changes nothing (recorded: "lead: a time limit").- The setup of
requests.jsonmakes the collection with the Configsets API (/solr/admin/configs?action=CREATE&baseConfigSet=_default), the Collections API (/solr/admin/collections?action=CREATE) and the Schema API (POST /solr/dbimp/schemawithadd-field-typeandadd-field), and adds the documents withPOST /solr/dbimp/update?commit=true(recorded: "make the configuration set", "make the collection", "add the field types", "add the fields" and "add the documents of every type"). Each takes a few hundred milliseconds. - On a server that deleted a collection, a new collection of the same name
can fail with
Core with name 'dbimp_shard1_replica_n1' already exists, or later withLock held by this virtual machine. Solr keeps a listener for the configuration set of the deleted core. When a configuration set of that name is made again, the listener fails to reload the old core, and the core stays ininitFailures. Only a restart of Solr cleared the lock (measured on 10.0.0 on 2026-10-01, fromsolr.log). So the setup unloads that core, and each release was recorded on a fresh start.
The integration tests of step 14 measured these facts on each release on 2026-10-07:
- The luke handler,
GET /solr/<collection>/admin/luke?show=schema&numTerms=0, answers JSON with a member name that repeats, such asStopFilterFactoryinsideschema.types.<type>.indexAnalyzer.filters. A strict decoder fails on it, so the driver reads it with the option that allows a repeated name. The handler of an alias reads the collection of the alias. - Each write that the tests sent with
update?commit=truewas visible to the next statement, as the principal read it. No test waited for it.
These facts come from the sources, and are not measured:
- The handler takes the connection parameters of Calcite that it allows,
then sets
lextoMYSQLandzkto its own ZooKeeper, so a caller cannot change either.numWorkersdefaults to 1,aggregationModetofacet, andincludeMetadatato false (the SQL module). - The JDBC driver sends
stmtand every property of its connection as parameters to/sqlof a replica that it picks at random (the JDBC driver). workerCollectionandworkerZkhostname wheremap_reduceruns (Gemini and DeepSeek, not measured).
- Step 9 decides the URL (D27 and D35).
dburlhas no scheme to follow. - The dbmeta entry gives each principal as
http://user:password@host:port, withadminanddbmeta_user, and the port 8983 in the container. The path/solris part of every request. - A statement needs a collection in its path (Requests), and any collection
that the user can read serves. Its
FROMnames the table. - D166 decides the DSN:
solr://user:password@host:8983/<collection>. The path names the collection whose handler takes the statement. The keys aretlsandmode, and the driver refuses every other key. The port stays 8983 withtls=true.dbrunprints theurlsolr://user:password@host:portwith no path (measured withdbrun dsn --jsonon 2026-10-07), so a test adds the path of its collection, and a statement of a DSN with no path needsWithDatabase(Open questions). modeisfacet, and the driver sends it asaggregationMode. A DSN withmode=map_reducefails withdbimp.ErrNotSupported, because that mode cut aGROUP BYat 100 groups with no sign (Responses).WithParameter("aggregationMode", "map_reduce")turns it on for one statement, and the integration tests use that to show the cut.- The driver registers the name
solrwith no alias (D28).dburlhas no scheme for Solr yet, so the schemesolris the name of the driver and not a scheme ofdburl.
These facts were recorded on each release:
- The answer is
{"result-set": {"docs": [...]}}. Each row is an object. The last element is the EOF tuple,{"EOF": true, "RESPONSE_TIME": n}(recorded: "a statement"). - The keys of a row are in the order of the
SELECT, and a field with no value isnull(recorded: "the order of the columns"). A key has the case that the statement wrote:SELECT ID, N_I FROM DBIMPgives the keysIDandN_I, and names are matched without regard to case (recorded: "names in another case"). - With no rows and no
includeMetadata, the answer holds only the EOF tuple, so it names no column (recorded: "no rows, without the metadata"). - With
includeMetadata=true, the first tuple is{"isMetadata": true, "fields": [...], "aliases": {...}}.fieldsnames the field of each column in order, andaliasesmaps each field to the name of its column. It comes with no rows too (recorded: "the metadata of the columns" and "no rows, with the metadata"). It names no type. - An aggregate with no alias is named
EXPR$0,EXPR$1and so on (recorded: "an aggregate with no name"). - Two columns with one name are wrong.
SELECT id AS a, id AS b, n_i AS bgivesfields["id", "id", "n_i"],aliases{"n_i": "b", "id": "b"}, and rows of three keysb, each with the value ofn_i(recorded: "two columns with one name"). SELECT *names every field of the schema, and the pseudo fields_query_andscore(recorded: "every field").- A statement with no
LIMITreads every document through the export handler, in no order that the statement names: 303 rows of 303 (recorded: "a result with no limit"). Each field must have docValues. ATextFieldfails withmust have DocValues to use this feature, aSortableTextFieldneedsuseDocValuesAsStored, and aUUIDFieldor a location fails withExport fields must be one of the following types: int,float,long,double,string,date,boolean,SortableText(recorded: "a text field with no limit", "a sortable text field with no limit" and "a uuid and a location with no limit").scorewith no limit fails (recorded: "score with no limit"). - A statement with
LIMITreads through the select handler.LIMIT 1000of 303 documents gave 303 rows.LIMIT 5 OFFSET 5andOFFSET 5 ROWS FETCH NEXT 5 ROWS ONLYgave the sixth to the tenth (recorded: "a limit above the rows", "a limit and an offset" and "fetch next rows").OFFSETwith noLIMITfails withOFFSET without LIMIT not supported by Solr!(recorded: "an offset with no limit"). - A
GROUP BYinfacetmode gave all 302 groups, ordered by the value, also withORDER BY count(*) DESC(recorded: "a group of each value" and "lead: a group ordered by its count, with no limit"). Inmap_reducemode, aGROUP BYwithORDER BY count(*) DESCand noLIMITgave 100 groups of 301, with no sign that it cut the result (recorded: "lead: a group ordered by its count, map reduce").DISTINCTgave all 302 values (recorded: "distinct values"). - There is no cursor and no next page. One body holds the whole result. A
large answer has no
Content-Length, so it streams, and a small one has one (recorded: "a result with no limit" and "a statement"). - With
Accept-Encoding: gzip, an answer of 300 rows came as gzip, andVary: Accept-Encodingis on every answer (recorded: "a gzip answer"). GET /solrredirects to/solr/, with 302 on 9.x and 301 on 10.0.0 (recorded: "a path with no slash"). No answer to/sqlredirected.- No answer was HTTP 429.
These facts come from the sources, and are not measured:
- With no limit, the table reads
/export. With a limit, it reads/selectwithrowsset to the limit and the offset (the SQL module). - In
facetmode with an order by a metric, the facet has a default limit of 100 (Gemini and DeepSeek). The server showed the cut inmap_reducemode and not infacetmode.
The column Kind names the kind of each type in TYPES.md, which
maps every kind onto its Go type (D135 and D137). Step 8a wrote this table
on 2026-10-02, and Ken decided it in D166. A row names the class of the
field type in the schema of Solr, because the result names none. The driver
learns the SQL type of each column from metadata.COLUMNS, which is the
database type of the table, and the class of the field type from the luke
handler, which is what makes a BoolField a bool, a BinaryField a
[]byte and a UUIDField a uuid.UUID (D166). metadata.COLUMNS alone
cannot do that, because it names VARCHAR for each of them (Open questions).
The cells for those three types name the luke handler for that reason, and
the rest of the table is the table of step 8a.
| Wire type | Kind | Go type | Scan type | Database type | Can be NULL |
|---|---|---|---|---|---|
| null | null | nil |
interface {} |
`` | yes |
| StrField | string | string |
string |
VARCHAR |
yes |
| TextField | string | string, the stored text |
string |
VARCHAR |
yes |
| SortableTextField | string | string, the stored text |
string |
VARCHAR |
yes |
| IntPointField | integer | int64 |
int64 |
BIGINT |
yes |
| LongPointField | integer | int64 |
int64 |
BIGINT |
yes |
| FloatPointField | float | float64, from the text of the float32 |
float64 |
DOUBLE |
yes |
| DoublePointField | float | float64 |
float64 |
DOUBLE |
yes |
| BoolField | boolean | bool, from the text "true" or "false", by the field type of the luke handler (D166) |
bool |
VARCHAR |
yes |
| DatePointField | timestamp | time.Time, in UTC, with milliseconds |
time.Time |
TIMESTAMP |
yes |
| BinaryField | binary | []byte, from the base64 text, by the field type of the luke handler (D166) |
[]uint8 |
VARCHAR |
yes |
| UUIDField | uuid | uuid.UUID, from its text, by the field type of the luke handler (D166) |
uuid.UUID |
VARCHAR |
yes |
| LatLonPointSpatialField | string | string, such as "45.5,-122.6" |
string |
VARCHAR |
yes |
| SpatialRecursivePrefixTreeFieldType | string | string, the WKT text |
string |
VARCHAR |
yes |
| BBoxField | string | string, such as "ENVELOPE(-10, 20, 15, 10)" |
string |
VARCHAR |
yes |
| PointType | string | string, such as "1.5,2.5" |
string |
VARCHAR |
yes |
| multi-valued field | array | []any, of the JSON values of its elements |
[]interface {} |
ANY |
yes |
| aggregate | number | int64, float64, or *apd.Decimal for an integer too large for int64 |
interface {} |
`` | yes |
| score | float | float64 |
float64 |
DOUBLE |
yes |
These facts were recorded on each release, from documents with a field of each type (recorded: "every type", unless a line names another request):
- A row is JSON, and names no type.
metadata.COLUMNSnames a SQL type for each field of a collection, toadminand todbmeta_user:VARCHAR(12) for a string, a text, a boolean, a binary, a UUID and each spatial field,BIGINT(-5) for an int and a long,DOUBLE(8) for a float, a double andscore,TIMESTAMP(93) for a date, andANY(2000) for a multi-valued field and_text_(recorded: "the types of the columns" and "lead: the types of the spatial fields"). A vector namesDOUBLE, and a date range namesTIMESTAMP(recorded: "the types of the columns"). - An
IntPointFieldand aLongPointFieldkeep every digit, to -9223372036854775808 and 9223372036854775807. - A
FloatPointFieldwrites the text of its float32: 3.4028235E38 and 0.1. ADoublePointFieldwrites 1.7976931348623157E308, and 2.0 with its fraction. - A document whose double holds
Infinity, or whose float holdsNaN, fails the query before any row, withclass java.lang.String cannot be cast to class java.lang.Double(recorded: "lead: infinities"). - A single-valued
BoolFieldis the string"true"or"false", and a multi-valued one is an array of JSON booleans. A filtern_b = trueworks (recorded: "a filter on a boolean"). - A
DatePointFieldis ISO 8601 text in UTC with milliseconds, such as"2026-10-01T12:34:56.123Z". Solr cut the nanoseconds of the input to milliseconds.0001-01-01T00:00:00Zand9999-12-31T23:59:59.999Zread back as they were written. A filter compares a date with its text (recorded: "a filter on a date"). - A
BinaryFieldis base64 text. AUUIDFieldis its text. ALatLonPointSpatialFieldis"lat,lon". ASpatialRecursivePrefixTreeFieldTypeis WKT, such as"POINT(-122.6 45.5)", aBBoxFieldis"ENVELOPE(-10, 20, 15, 10)", and aPointTypeis"1.5,2.5"(recorded: "lead: spatial fields", on 10.0.0 only). - A multi-valued field is a JSON array of its values. Through the select
handler, the values keep the order of the document. Through the export
handler, they come sorted, such as
[-1,9223372036854775807]for a document that held[9223372036854775807,-1](recorded: "every type with docValues, with no limit"). - The update handler drops an empty string, an empty array, and an array
that holds only
"", so each of them reads as null. - Text outside ASCII arrives as UTF-8, and a character outside the Basic
Multilingual Plane as a pair of
\uescapes, such as🙂. - An aggregate names no type.
count(*)andsumof integers are JSON integers, andavgof the integers 2147483647 and -2147483648 is0, an integer, where the mean is -0.5 (recorded: "the average of integers"). scoreis a JSON number, such as 0.13076457 (recorded: "feature: score").- A
DenseVectorFieldfails the query when a row holds a vector, withclass java.util.ArrayList cannot be cast to class java.lang.Double, after the rows before it (recorded: "a vector"). ADateRangeFieldfails the same way withText '[2026-01-01 TO 2026-12-31]' could not be parsed at index 0(recorded: "a date range"). The select handler returns both (recorded: "every type through the select handler"). - An
EnumFieldTypeand aCurrencyFieldTypeneed a configuration file that_defaultdoes not hold. The Schema API refused the first with HTTP 400, and failed the second with HTTP 500 andUnable to reload core, which also broke the collection (recorded: "add an enum type with no configuration file" and "add a currency type with no configuration file"). - An expression in the projection, such as
n_i * 2or aCAST, fails withCannot invoke "String.contains(java.lang.CharSequence)" because "fname" is null, so no column holds a computed value (recorded: "an expression in the projection").SELECT 1 AS onefails becauseoneis a word of the grammar (recorded: "a literal in the projection").
The round trip of step 14a measured these facts on each release on 2026-10-07, with a field that the test added through the Schema API:
- The type
text_generalis multi-valued unless the field says"multiValued": false, so a field of that type that does not say it reads as an array (SQL typeANY). - An atomic update that sets an empty string stores it, and a read gives
"". An add of a document drops an empty string, so the read gives NULL. The round trip replaces the whole document for each update, so each value goes through the add. - A
FloatPointFieldwith the value 3.4028235E38, and aDoublePointFieldwith the largest double, read back as they were written. An aggregate,sumof aLongPointField, is a JSON integer. - A multi-valued field of integers, strings or booleans keeps the order of the
document when a statement has a
LIMIT.
These facts come from the sources, and are not measured:
- The SQL module maps the field types
stringtoString,int,long,pintandplongtoLong,float,double,pfloatandpdoubletoDouble,pdatetoDate, and a multi-valued field toANY. Any other type takes its Java class from the class of the field type, and falls back toString(the SQL module). - The JDBC driver takes the type of a column from the Java class of its value
in the first row, and calls a null column a
String(the JDBC driver).
These facts were recorded on each release:
?fails with HTTP 500 and an HTML page,java.lang.AssertionError: unsupported predicate expression: =(CAST($4):VARCHAR, CAST(?0):VARCHAR)(recorded: "a placeholder").:idis a syntax error (recorded: "a named parameter").- A quote in a literal is written twice, as
'it''s'(recorded: "a quote in a literal").
These facts come from the sources, and are not measured:
- The server binds no parameters. The JDBC driver has a
PreparedStatementwhosesetString,setIntand every other setter do nothing, so it sends the text with its?(the JDBC driver).
These facts were recorded on each release:
BEGINandCOMMITare syntax errors (recorded: "begin" and "commit").POST /solr/dbimp/update?rollback=truefails with HTTP 500,Rollback is currently not supported in SolrCloud mode. (SOLR-4895)(recorded: "lead: roll back the update handler").- A write of the update handler with
commit=trueis visible to the next statement (recorded: "crud: insert" and "crud: select").
These facts come from the sources, and are not measured:
- The JDBC driver has
commitandrollbackthat do nothing (the JDBC driver). - A document can carry
_version_for a check of optimistic concurrency on one document (Gemini).
These facts were recorded on each release:
- An error of a statement answers HTTP 200, with the EOF tuple that holds
EXCEPTION, such asFailed to execute sqlQuery 'SELEC id FROM dbimp' against JDBC connection 'jdbc:calcitesolr:'. Caused by: ...(recorded: "a syntax error", "a table that does not exist" and "a column that does not exist"). - An error can come after some rows. A date range in the third row gave two
rows and then the EOF tuple with
EXCEPTION(recorded: "an error after some rows"). AGROUP BYof an integer inmap_reducemode gave the group of -2147483648 and then failed on the group of NULL,class java.lang.String cannot be cast to class java.lang.Long(recorded: "an error after some rows of a group"). - A
?answers HTTP 500 with an HTML page (recorded: "a placeholder"). - A wrong password answers HTTP 401, and a request that the role does not
allow answers HTTP 403, each with an HTML page (recorded: "a wrong password"
and "crud: insert" as
dbmeta_user). - The Collections API and the Schema API answer an error as JSON with
responseHeader.statusanderror.msg. On 9.xerror.metadatais a list of names and values, and on 10.0.0 an object (recorded: "remove the collection of an earlier run"). - No answer was HTTP 429 or HTTP 503.
These facts were recorded on each release:
- A client that left a query after 1 ms did not stop the next statement, which answered at once (recorded: "a query after the client left").
canCancel=truewithqueryUUIDon/sqlregisters nothing that/tasks/cancelknows: the cancel answered"cancellationResult": "not found"after the query ended (recorded: "lead: a query that can be cancelled" and "lead: cancel a query by its id")./tasks/listanswered an empty list to both users (recorded: "the tasks that run").timeAllowed=1did not cut a result of 300 rows (recorded: "lead: a time limit").
These facts come from the sources, and are not measured:
- When the client leaves, the next write of the stream fails, and the
handler stops the stream that it runs. Whether the workers of
map_reducestop is not known (Gemini). DeepSeek said that Solr does not stop a query when the client leaves. The data of the measurements was too small to show either. - The JDBC driver throws
UnsupportedOperationExceptionfromcancel(the JDBC driver).
These facts were recorded on each release:
- Two statements in one request fail with a syntax error at the
;, and so does one statement that ends with;(recorded: "two statements in one request" and "a statement that ends with a semicolon"). - Comments
--and/* */work (recorded: "comments"). - A name can be quoted with backticks. A text in double quotes is a name, so
id = "1"fails (recorded: "quoted names" and "a string in double quotes"). EXPLAIN PLAN FORgives one row with the columnPLAN(recorded: "explain").UNION,DISTINCT,HAVINGand an order by an aggregate work (recorded: "feature: union", "distinct values" and "feature: having and an order of an aggregate"). An order bymax(id)of a string fails with aClassCastException(recorded: "lead: distinct ordered by another column, with no limit").- A
JOINworks. A join ofdbimpwith itself onidgave the row of1(recorded: "lead: a join of a collection with itself", on 10.0.0 only). A join ofdbimpwith the empty collectiondbmetagives no rows and no error (recorded: "feature: join"). field = 'text'on aTextFieldis a search of the text,n_s = 'h*'is a wildcard, andn_ss = 'a'matches a multi-valued field that holdsa(recorded: "feature: full text search", "feature: a wildcard in equality" and "feature: a multi-valued field in a filter")._query_ = 'n_t:fox'runs a query of Lucene, and_query_ = 'n_t:fox AND n_i:[0 TO *]'fails withCannot parse 'n_i:[0'(recorded: "feature: a lucene query" and "feature: a lucene query with spaces").ARRAY_CONTAINSis not a function (recorded: "feature: array contains").- A streaming expression is
POST /solr/<collection>/streamwith the form fieldexpr, and answers in the same form as/sql, to both users (recorded: "a streaming expression").
The integration tests of step 14 measured these facts on each release on 2026-10-07:
-
A backslash in a literal is no escape:
'x\'is the textx\. Only a quote written twice stands for itself. The driver writes an argument so. -
A field named like a word of the grammar, such as
year, is a syntax error until it is quoted with backticks. -
An equality on a
StrFieldtakes?and*in its text as wildcards. A text with?and spaces matched no document, and a longer text with quotes, a backslash and spaces matched other documents (measured withcurlon 10.0.0 on 2026-10-07). An equality on text without those characters finds the document. -
A statement with names in other case, such as
SELECT ID, N_I FROM DBIMP, runs, and each key has the case that the statement wrote (recorded: "names in another case"). -
The driver answers
SELECT version()itself, because the SQL has no statement for the version (D181). It sendsGET /solr/admin/info/system?wt=jsonand returns one row with the columnversion, a string such as9.10.1, which islucene.solr-spec-version. It needs no collection, so a DSN with no path works for this statement. Only a query returns the row, andExecContextsends the statement to the server. The driver ignores case, the white space around the statement, and one final;. It takes no argument and no other text. An option such asWithDatabasehas no effect on it (TestSelectVersion,TestSelectVersionOnlyThatandTestIntegrationVersion).
errors.Is(err, dbimp.ErrAuthentication) is true when the server refused the
credential, and only then. It is never true for a refusal for lack of a
permission. The method Is of *Error decides it, and *dbimp.StatusError
under it also matches HTTP 401.
The match is true for:
- HTTP 401 with the title "Bad credentials" (recorded: "a wrong password").
The match is false for:
- HTTP 403 with the title "Unauthorized request, Response code: 403". The role of the user does not allow the request (recorded: the requests of the ordinary user that the manifest numbers 122 and 127).
- An exception in the body of an answer with HTTP 200, and every other status.
These facts were recorded on each release, as dbmeta_user, who has the
role search:
dbmeta_userruns every SQL statement on a collection that exists, with the same answer asadmin,metadata.COLUMNSandmetadata.TABLESincluded (recorded).dbmeta_usercannot write. The update handler answers HTTP 403 (recorded: "crud: insert", "crud: update" and "crud: delete").dbmeta_usercannot read the Schema API, the list of collections, or the version, which/solr/admin/info/systemand/api/node/systemgive toadminaslucene.solr-spec-version, such as10.0.0. Each answers HTTP 403 (recorded: "schema: the fields", "the list of collections", "the version" and "the version through the v2 API").dbmeta_usercan read the luke handler,/solr/<collection>/admin/luke, which names each field and its type, and can run a streaming expression (recorded: "the luke handler" and "a streaming expression").- SQL has no statement that gives the version (Gemini, not measured). The
driver answers
SELECT version()from the system handler (D181), so the ordinary user gets the error of that request, an*solr.ErrorwithHTTPStatus403, and the driver adds no other check. - Step 16:
usqlhas no driver for Solr, so there is no statement ofusqlto run.GET /solr/admin/info/system?wt=jsongavelucene.solr-spec-versiontoadminon each release, and HTTP 403 todbmeta_user(measured byTestIntegrationVersionon 2026-10-07). On 2026-10-08 the same test showed thatSELECT version()gives9.10.1toadminon 9.10.1, with or without a collection in the DSN, and the HTTP 403 todbmeta_user.
- No other product answers
/sqlin this form, as far as the sources know. Gemini named Lucidworks Fusion and Alfresco Search Services, which bundle Solr. Neither is measured, anddbrunstarts neither. - The three releases differ in these answers only (recorded):
- An error of the Collections API or the Schema API holds
error.metadataas a list on 9.x and as an object on 10.0.0. - The path of a collection that does not exist answers 404 on 9.x and 405
on 10.0.0, for
admin. GET /solrredirects with 302 on 9.x and 301 on 10.0.0.
- An error of the Collections API or the Schema API holds
- Solr also runs in standalone mode, where
/sqlfails with an error, because SQL needs SolrCloud (the SQL module, not measured).
Step 10 writes this table from the code.
| Interface | Implemented | Reason |
|---|---|---|
driver.DriverContext |
yes | OpenConnector parses the DSN once, for every connection. |
driver.Connector |
yes | The connector owns the transport, which every connection shares. |
io.Closer on the connector |
yes | Close closes the idle connections of the transport. |
driver.Pinger |
yes | Ping counts the documents of the collection of the DSN, which checks the credentials and the handler of SQL. The ordinary user cannot read the version. |
driver.SessionResetter |
no | A connection holds nothing on the server, because the SQL of Solr has no sessions. |
driver.Validator |
no | A connection holds nothing on the server, so it is always valid. |
driver.NamedValueChecker |
yes | It keeps an Option, a decimal, a dbimp.Date, a dbimp.LocalDateTime and a uuid.UUID, which the driver writes as literals (D166). |
driver.QueryerContext |
yes | The server binds no argument, so the driver writes each one into the statement as a literal (D166). |
driver.ExecerContext |
yes | Exec reads the result to its end. The SQL of Solr takes no write, so RowsAffected fails (D163). |
driver.ConnPrepareContext |
yes | A prepared statement runs as its text, with its arguments, each time. |
driver.ConnBeginTx |
yes | BeginTx fails with dbimp.ErrNotSupported, because Solr has no transactions (D166). |
driver.RowsColumnScanner |
yes | A value is decoded when its row is read, and assigned when it is scanned. |
driver.RowsNextResultSet |
no | A request holds one statement, so an answer has one result. |
driver.RowsColumnTypeScanType |
yes | The driver reads the type of each column from metadata.COLUMNS and the luke handler (D166). |
driver.RowsColumnTypeDatabaseTypeName |
yes | The SQL type of metadata.COLUMNS, such as BIGINT, and empty for a column that it does not name, such as an aggregate. |
driver.RowsColumnTypeLength |
no | The server names no length. |
driver.RowsColumnTypeNullable |
yes | The server does not say whether a column can be NULL, and every type can be. |
driver.RowsColumnTypePrecisionScale |
no | The server names no precision and no scale. |
usql has no driver for Solr. These are faults of the JDBC driver, which a
driver here must not repeat (the JDBC driver):
commitandrollbackdo nothing, so a caller believes that it has a transaction (D20).- Each setter of
PreparedStatementdoes nothing, so a statement with?reaches the server with its?, and fails with HTTP 500 (D34). - It takes the type of a column from the Java class of its value in the
first row, calls a column of nulls a
String, and gives aLongthe JDBC typeDOUBLE. setMaxRowsappendslimit <n>to the text of a statement that has no limit.cancelthrowsUnsupportedOperationException.
These are faults of the server that a caller of a driver meets:
- Two columns with one name give wrong values (Responses).
- A
GROUP BYinmap_reducemode with an order by an aggregate and noLIMITgives 100 groups, with no sign that it cut the result (Responses). - A value that the SQL layer cannot convert, such as a vector, a date range or an infinity, fails the whole query, and a caller cannot read the other rows (Types).
Step 5a asked Gemini and DeepSeek on 2026-10-01 for the operations, the features and the types. The server settled each answer:
- Gemini said that SQL only reads, and that the update handler writes. The
server agrees. DeepSeek said that
INSERT,UPDATEandDELETEwork in SQL. The server refuses each one (recorded: "crud: sql insert", "crud: sql update" and "crud: sql delete"). - Gemini said that a
BoolFieldarrives as a JSON boolean, and DeepSeek agreed. A single-valued one arrives as a string, and a multi-valued one as booleans (recorded: "every type"). - Both said that a
DenseVectorFieldarrives as an array of numbers, and that aDateRangeFieldand aCurrencyFieldTypearrive as text. The first two fail the query (recorded: "a vector" and "a date range"). The Schema API refused the third without its configuration file. - Both said that a
LongPointFieldkeeps every digit. The server agrees. - Gemini said that an alias of a collection can stand for a view. A statement on an alias works (recorded: "a statement on an alias").
Step 7 asked both models on 2026-10-02 what step 6 did not find. Each lead was sent to the server:
- Both said that the server binds no parameters. The server refuses
?and:id(recorded). - Both said that there are no transactions. Gemini named
update?rollback=true, which rolls back every change since the last commit. SolrCloud refuses it (recorded: "lead: roll back the update handler"). - Both said that there is no cursor, and that a
GROUP BYinfacetmode has a default limit of 100 when it is ordered by an aggregate. Infacetmode the server gave all 302 groups. Inmap_reducemode it gave 100 of 301 (recorded: "lead: a group ordered by its count, with no limit" and "lead: a group ordered by its count, map reduce"). - Gemini named the Schema API, the luke handler and
metadata.COLUMNSfor the types. DeepSeek namedmetadata.COLUMNS. The ordinary user can read the luke handler andmetadata.COLUMNS, and not the Schema API (recorded). - Both said that
canCancelandqueryUUIDdo not reach/sql, and thattimeAllowedis a limit and not a cancel./tasks/canceldid not find the query, andtimeAllowed=1cut nothing (recorded: "lead: cancel a query by its id" and "lead: a time limit"). - Gemini said that
numWorkersandworkerCollectionchange howmap_reduceruns.numWorkers=2fails on one node (recorded: "lead: two workers"). - DeepSeek named the spatial types RPT and BBox. Each arrives as its text, and
the SQL layer names each
VARCHAR(recorded: "lead: spatial fields" and "lead: the types of the spatial fields"). - Neither model named a flavor that
dbruncan start.
Step 8a asked both models on 2026-10-02 to review the type table against TYPES.md and D135, with the values that the server sent:
- Both agreed with each row. Both said that a
BoolField, aBinaryField, aUUIDFieldand each spatial type give astring, because the SQL layer names each oneVARCHAR, and D135 gives a column named as a string a string. - Both said that
metadata.COLUMNSis the only source of the type that the database names, and that an aggregate, which it does not name, takes its type from its JSON token. - DeepSeek said that an empty array arrives as null, so a multi-valued field
gives nil and never an empty
[]any. The server agrees (recorded: "every type"). - DeepSeek said that the types that fail on the server, the vector, the date range, the enum and the currency, get no row until the server returns a value.
Ken settled the first seven questions of step 8a on 2026-10-02, and D163 and D166 hold the answers:
- The driver reads only. It sends what the SQL takes, and returns the
refusal of the server for
INSERT,UPDATEandDELETE(D163). - The driver reads the type of each column from
metadata.COLUMNS, once for each table and connection (D166). - A
BoolFieldis abool, aBinaryFielda[]byteand aUUIDFieldauuid.UUID(D166). - The driver refuses
aggregationMode=map_reducein the DSN (D166). - The driver always sends
includeMetadata=true(D166). - The driver refuses a result whose columns share a name (D166).
- The ordinary user cannot learn the version, which waits for
dbmeta(D166). The driver answersSELECT version()for the administrator only, and the ordinary user gets the HTTP 403 (D181).
The steps after step 9 met these questions. Ken closed question 8 on 2026-10-07 (D178, item 4). Questions 9 to 15 still wait for Ken. The driver follows the first answer of each one, and says so:
- Closed by D178, item 4. D166 said that the driver reads the class of the
field type from
metadata.COLUMNS. It cannot:metadata.COLUMNSnamesVARCHARfor a boolean, a binary and a UUID, andtypeNamenames the Java class of the SQL type (recorded: "the types of the columns"). The class of the field type is in the luke handler, which both principals can read (recorded: "the luke handler"). Ken decided on 2026-10-07 that the driver reads both, as two statements for each table, and keeps the schema for the connection. The type table names the luke handler in the three cells that D166 names. This amends item 3 of D166. - Closed by D178. The path of the DSN names the collection, and
dbrunprints aurlwith no path (measured on 2026-10-07). The driver takes a DSN with no path, and fails each statement that has noWithDatabasewithdbimp.ErrInvalidValue. The other choice is a DSN that must have a path.Pinghas the same rule, because it counts the documents of the collection. WithDatabasenames the collection of the path of one statement. The other choice isdbimp.ErrNotSupported, as for Druid and Pinot, which have no database to choose.- Closed by D178.
WithTimeoutfails withdbimp.ErrNotSupportedfor a timeout other than zero, becausetimeAllowedcut nothing. The other choice is a deadline that the driver sets on the request. A deadline of the context stops the request already (D36). - Closed by D178. The driver finds the tables of a statement by a scan of its text for the
words
FROMandJOIN. A table that the scan misses, or whose case differs from the name of its collection, has no schema, and its columns read by their JSON tokens: aBoolFieldis astringthere. Solr matches the names of tables without regard to case (recorded: "names in another case"), and the driver asksmetadata.COLUMNSand the luke handler for the name as the statement wrote it. Whether each one answers a name in other case is not measured. The other choice is a request for the collections that the statement names, which the ordinary user cannot make (recorded: "the list of collections"). - Closed by D178. An equality on a
StrFieldreads*and?in its text as wildcards, and a text with spaces matched other documents (measured on 10.0.0 on 2026-10-07). The driver writes the literal that the caller gave, and the server decides what it means. The other choice is a refusal of an argument with*or?, which also refuses a wildcard that a caller writes on purpose (feature "wildcard in equality"). - Closed by D178. The driver exports
ErrCut, as Druid does, for an answer that ends before its tupleEOF. The error wrapsdbimp.ErrIncompleteafter a row. - The driver stores no
context.Context. The function that reads the types of the columns takes the context of the statement, and the rows drop it beforeQueryContextreturns, so hard rule 4 needs no new exception.
The tests that need a server are in solr/integration_test.go,
solr/features_integration_test.go and solr/roundtrip_integration_test.go
(steps 14 and 14a). They read SOLR_DSN for admin and SOLR_ORDINARY_DSN
for dbmeta_user, which are the url of each principal in dbrun (D9). That
url has the scheme solr and no path, and each test adds the path of the
collection that it reads. A test skips when the variable that it needs is
empty. Each test runs as each principal.
TestMainmakes five collections, each with a configuration set of its own that copies_default, so that a change of one schema does not reload another collection. The main collection holds a document of every type and 300 more, as step 6 did, and an alias. Three collections hold a small catalog of authors, books and reviews. One holds a document withInfinity. Each name starts withdbimp_it_and the time of the run, andTestMainremoves the collections, the alias and the configuration sets at the end.- The administrator writes through the update handler and the Schema API, because the SQL of Solr takes no write (D163), and the principal reads through the driver. Each write commits, and the next statement sees it, so no test sleeps.
TestIntegrationCRUDshows the refusal ofINSERT,UPDATEandDELETE, each of the five operations of the update handler, and the refusal of the update handler to the ordinary user. It also makes the three collections of the catalog: it adds the rows, reads them, reads the books of one author by the indexed fieldauthor_id, updates, deletes by id and by query, and reads until each collection is empty. Solr has no foreign key, so a field holds the id of another row.TestIntegrationSchemashows thatCREATE TABLE,CREATE INDEXandCREATE VIEWfail, and that a collection, an alias, a default value, a dynamic field, a copy field,metadata.TABLESandmetadata.COLUMNSwork. The Schema API answers HTTP 403 to the ordinary user, and the luke handler does not.TestIntegrationFeaturesholds each other entry offeatures.json. An entry that the survey marks no sends the operation and shows the refusal or the effect: the cut of aGROUP BYinmap_reducemode, which the driver refuses in the DSN, a statement with two columns of one name, a placeholder that the server refuses while the driver binds it, a transaction, and a cancel by an id that the server does not know.TestIntegrationRoundTriprunsdbimptest.RoundTripfor each of the types thatfeatures.jsonmarks yes, and for a multi-valued field of three kinds, and shows each type that it marks no. The server refusesINSERT,UPDATEandDELETE, so the test adds a field through the Schema API, writes each value as a document, and reads it with a select by the id. An update replaces the document. Each value is written as a bound argument, and the round trip logs that Solr has no literal of an insert and goes on.TestIntegrationArgumentsandTestIntegrationFeatureshold the literals of a query. An empty string, an empty array and empty bytes read as NULL.TestIntegrationEveryType,TestIntegrationScan,TestIntegrationErrors,TestIntegrationContext,TestIntegrationConcurrent,TestIntegrationLargeResult,TestIntegrationColumnTypes,TestIntegrationOptionsandTestIntegrationPingread every type throughRows.Scan, and hold the errors, the context, two queries at the same time, a result with no limit and the options.- The ordinary user has the role
search. It cannot write and cannot read the Schema API, the list of collections or the version, and it can read the luke handler and run a streaming expression (TestIntegrationVersionand the tests above).
All three releases passed on 2026-10-07 with the final code, as both principals:
| Release | Tests that passed | Skipped | Failed |
|---|---|---|---|
solr-9.9.0 |
291 | 0 | 0 |
solr-9.10.1 |
291 | 0 | 0 |
solr-10.0.0 |
291 | 0 | 0 |
The count is the lines --- PASS of go test -v -run Integration, subtests
included. The releases gave the same answer to every test. A join worked on
9.9.0, 9.10.1 and 10.0.0.
Step 17a compares this driver with couchbase, the first driver (D97). It was
written on 2026-10-07 from the staged code. A fact of Couchbase comes from
COUCHBASE.md, and a fact of Solr from the sections above.
| Couchbase | Apache Solr | |
|---|---|---|
| Request | POST /query/service, with statement, args and $name |
POST /solr/<collection>/sql, with the form field stmt, and includeMetadata and aggregationMode from the driver (Requests) |
| Database | The key query_context of the body |
The collection in the path of the request names the handler, and FROM names the table (The DSN) |
| Language | SQL++, which is close to SQL | SQL, parsed by Calcite with the lexical rules of MySQL. The server only reads (Statements) |
| DDL | In SQL++ | None. CREATE TABLE, CREATE INDEX and CREATE VIEW fail. A collection and a field go through the Collections API and the Schema API (Statements) |
| Parameters | ?, $1 and $name |
None. ? fails with HTTP 500 and :id is a syntax error, so the driver writes each argument as a literal (Parameters) |
| Framing | One body for the whole result, which does not page | One body for the whole result, {"result-set":{"docs":[...]}}, which ends with the tuple EOF. There is no cursor and no next page (Responses) |
| Columns | signature, before the first row |
The first tuple with includeMetadata=true, which names the fields and the alias of each. With none, a result with no rows names no column (Responses) |
| Order | The projection on 7.6 and 8.0, the names on 7.2 | The statement |
| Errors | Can come with HTTP 200, after some rows | HTTP 200 with the tuple EOF that holds EXCEPTION, before any row or after some. A status that is not 2xx comes with a page of HTML (Errors) |
| Types | JSON. No date, decimal, UUID or binary | JSON with no type. The SQL type of a column is in metadata.COLUMNS, and the class of its field in the luke handler. A date is ISO 8601 text, a binary is base64, and a boolean is the text "true" (Types) |
| Cancel | The server stops a query when the client leaves | No request cancels a statement of /sql. Whether the server stops one when the client leaves is not measured (Cancellation and timeouts) |
| Transactions | BEGIN WORK in SQL++, carried by txid |
None. BEGIN is a syntax error, and the update handler cannot roll back in SolrCloud (Transactions) |
| Authentication | Basic, or creds in the body |
Basic |
| Default port | 8093, or 18093 with TLS | 8983 |
The differences that a caller sees:
- Every write fails with the refusal of the server, because the SQL of Solr takes none, and the driver does not send a write to the update handler (D163).
- A statement needs a collection, from the path of the DSN or from
WithDatabase, where Couchbase takes a statement with noquery_context(D166). - The server binds no argument. The driver writes each one into the text of the statement as a literal, where Couchbase sends them to the server (D34 and D166).
- A result with a column that appears twice is refused, and
aggregationModemap_reduceis refused in the DSN (D166). - The driver sends no cancel when the context ends, because Solr has no request for it. Whether the server stops a statement when the client leaves is not measured (D166).
couchbase |
solr |
|
|---|---|---|
| Size, without tests, on 2026-10-07 | About 1300 lines in 8 files | About 1800 lines in 10 files |
Config |
QueryContext, ScanConsistency, Timeout, Durability, TxTimeout |
Host, Port, TLS, User, Password, Collection, Mode |
| Options for one statement | Six With options for one statement, through WithOptions or an argument, and two for BeginTx, through WithOptions only (D40, D46 and D109). WithDatabase sets query_context, as WithQueryContext does. WithParameter sets any key of the body |
WithTimeout, WithReadonly, WithParameter and WithDatabase, through WithOptions or an argument (D109). WithTimeout fails for a timeout other than zero, because timeAllowed cut nothing. WithReadonly changes nothing. WithDatabase names the collection of the path. WithParameter sets a field of the form, and replaces includeMetadata and aggregationMode |
| Arguments | Sent to the server as args and $name |
Written into the statement by dbimp.Syntax.Bind, as NULL, a number, TRUE, FALSE, a quoted text, a date in UTC with milliseconds, base64, a UUID and a decimal. A ? or an @name is a placeholder, and one in a literal or a comment is not (solr/literal.go) |
| Rows | dbimp.ObjectRows from the root package, after the driver reads the signature. SELECT RAW has a reader of its own |
A reader of its own over jsontext, which reads one tuple at a time and keeps the order of the metadata (solr/rows.go) |
| Types of the columns | ColumnTypeDatabaseTypeName and ColumnTypeScanType from the signature, and ColumnTypeNullable |
The same three methods, from metadata.COLUMNS and the luke handler, which the driver reads once for each table and connection (solr/schema.go) |
| Values | int64, float64, or *apd.Decimal for an integer too large for int64. Bytes are decoded from base64 (D44) |
By the type of the column: int64, float64, bool, string, time.Time, []byte, uuid.UUID and []any for a multi-valued field. An aggregate reads by its JSON token (D166) |
Result of Exec |
RowsAffected from metrics.mutationCount |
RowsAffected and LastInsertId return dbimp.ErrNotSupported, because the SQL of Solr changes no rows (D163) |
| Transactions | BeginTx sends BEGIN WORK. ReadOnly sends readonly |
BeginTx returns dbimp.ErrNotSupported (D166) |
| Reset of a session | ResetSession, which it keeps as a guard (D41 and D102), and IsValid |
None. A connection holds only the schemas that it read |
| Cancel | The request carries the context, and net/http stops it when the context ends (D36 and D42) |
The request carries the context, and net/http stops it. The driver sends no cancel (D166) |
| Errors | *ResponseError, with the HTTP status, the status of the body, and a list of Error{Code, Msg} |
*Error{HTTPStatus, Message}, which unwraps to *dbimp.StatusError for a status that is not 2xx, and ErrCut for an answer that ends before its tuple EOF |
| Authentication | Basic | Basic, and the driver follows no redirect, so the credentials go to the host of the DSN only (D166) |
| Other exports | The With options and Option |
The With options and Option, Error, ErrCut, Config, ModeFacet and Name |
The differences that a caller sees:
- A value keeps its type, a date, a binary value and a UUID too, where Couchbase gives JSON shapes (D135 and D166).
- A column costs no request when the connection knows its table, and the first
statement on a table sends two more requests, one to
metadata.COLUMNSand one to the luke handler (D166). WithReadonlysucceeds and does nothing, where Couchbase sendsreadonly, because the SQL of Solr takes no write (D163).RowsAffectedalways returns an error, where Couchbase counts every statement bymutationCount(D163).WithTimeoutfails for a timeout other than zero, where Couchbase sendstimeout(D166).