From 969b7094f22ac21a0c047b68cfb5a9070542e4a7 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Wed, 24 Apr 2024 15:19:08 -0500 Subject: [PATCH 01/20] (PDB-5739) certname-names-query: migrate to certnames_status cf. fda5d3c7671caf725dbc54e4533b31d88e0852d6 --- src/puppetlabs/puppetdb/query.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/puppetlabs/puppetdb/query.clj b/src/puppetlabs/puppetdb/query.clj index 9c9c21e6f3..0c03a90325 100644 --- a/src/puppetlabs/puppetdb/query.clj +++ b/src/puppetlabs/puppetdb/query.clj @@ -394,8 +394,8 @@ (defn certname-names-query [active] (if active - "SELECT name FROM certnames WHERE deactivated IS NULL AND expired IS NULL" - "SELECT name FROM certnames WHERE deactivated IS NOT NULL OR expired IS NOT NULL") ) + "SELECT certname FROM certnames_status WHERE deactivated IS NULL AND expired IS NULL" + "SELECT certname FROM certnames_status WHERE deactivated IS NOT NULL OR expired IS NOT NULL")) (defn compile-resource-equality "Compile an = operator for a resource query. `path` represents the field From e0808f8c66b0f35e946ff6de3a0be027fc84557a Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Wed, 24 Apr 2024 16:22:51 -0500 Subject: [PATCH 02/20] (PDB-5739) clj-jdbc-termination-ex?: require SQLException Previously this function could try to call .getSQLState on unrelated exceptions. --- src/puppetlabs/puppetdb/jdbc.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/puppetlabs/puppetdb/jdbc.clj b/src/puppetlabs/puppetdb/jdbc.clj index 560b33ee0d..5c78853e44 100644 --- a/src/puppetlabs/puppetdb/jdbc.clj +++ b/src/puppetlabs/puppetdb/jdbc.clj @@ -37,8 +37,9 @@ (and (instance? ExceptionInfo ex) (let [{:keys [rollback handling]} (ex-data ex)] (and rollback + (instance? SQLException handling) (= (sql-state :admin-shutdown) - (some-> handling .getSQLState)))))) + (.getSQLState handling)))))) (defmacro with-db-connection [spec & body] `(sql/with-db-connection [db# ~spec] From 8b43bbe6516307a8731747a011deb282ab166ee7 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Tue, 30 Apr 2024 20:38:51 -0500 Subject: [PATCH 03/20] (PDB-5739) Fix command service request-shutdown invocation Noticed failing schema check in command-test output. --- src/puppetlabs/puppetdb/command.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/puppetlabs/puppetdb/command.clj b/src/puppetlabs/puppetdb/command.clj index 9580370f1c..be749ed6a3 100644 --- a/src/puppetlabs/puppetdb/command.clj +++ b/src/puppetlabs/puppetdb/command.clj @@ -901,8 +901,9 @@ (let [msg (trs "Unable to shut down delayed command scheduler, requesting server shutdown")] (log/error msg) (shut-down-after-command-scheduler-unresponsive + ;; This fails request-shutdown schema validation during command-test #(request-shutdown {:puppetlabs.trapperkeeper.core/exit - {:status 2 :messages [msg *err*]}}))))) + {:status 2 :messages [[msg *err*]]}}))))) (def delay-pool-size "Thread count for delaying messages for retry, and broadcast pool From 410177e2ad9fe913e59886fa11611c44972b74db Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Wed, 24 Apr 2024 16:47:56 -0500 Subject: [PATCH 04/20] (PDB-5739) Prefer inventory in fact-related query docs Since the other endpoints have to manufacture a synthetic representation of the data (i.e. flattened key/values, etc.) and so are likely to be more expensive, at least when a large number of factsets is involved. --- documentation/api/query/v4/fact-contents.markdown | 9 +++++---- documentation/api/query/v4/facts.markdown | 12 +++++++----- documentation/api/query/v4/factsets.markdown | 8 +++++--- documentation/api/query/v4/inventory.markdown | 6 +++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/documentation/api/query/v4/fact-contents.markdown b/documentation/api/query/v4/fact-contents.markdown index c9f5477b1b..5ca41332f6 100644 --- a/documentation/api/query/v4/fact-contents.markdown +++ b/documentation/api/query/v4/fact-contents.markdown @@ -16,10 +16,11 @@ canonical: "/puppetdb/latest/api/query/v4/fact-contents.html" [factsets]: ./factsets.markdown [nodes]: ./nodes.markdown -You can query fact information with greater power by using the `/fact-contents` -endpoint. This endpoint provides the capability to descend into structured -facts and query tree nodes deep within this data by using the concept of paths -and values. +The `/fact-contents` endpoint provides selective access to factset +subtrees via `fact path`s. Note that the `inventory` endpoint will +often provide more flexible and efficient access to the same +information. + ## Paths and Values Structured facts can be thought of as trees. For example, diff --git a/documentation/api/query/v4/facts.markdown b/documentation/api/query/v4/facts.markdown index b167c73673..5e65b788aa 100644 --- a/documentation/api/query/v4/facts.markdown +++ b/documentation/api/query/v4/facts.markdown @@ -16,9 +16,11 @@ canonical: "/puppetdb/latest/api/query/v4/facts.html" [fact-contents]: ./fact-contents.markdown [nodes]: ./nodes.markdown -You can query facts by making an HTTP request to the `/facts` endpoint. - -In Puppet's world, you only interact with facts from one node at a time, so any given fact consists of only a **fact name** and a **value.** But because PuppetDB interacts with a whole population of nodes, each PuppetDB fact also includes a **certname** and an **environment.** +The `/facts` endpoint provides access to a represntation of node +factsets where a result is returned for each top-level key in the +node's structured factset. Note that the `inventory` endpoint will +often provide more flexible and efficient access to the same +information. ## `/pdb/query/v4/facts` @@ -37,8 +39,8 @@ See [the AST query language page][ast]. ### Query fields -* `name` (string): the name of the fact. -* `value` (string, numeric, Boolean): the value of the fact. +* `name` (string): the top-level name of the fact. +* `value` (json): the value of the fact. * `certname` (string): the node associated with the fact. * `environment` (string): the environment associated with the fact. diff --git a/documentation/api/query/v4/factsets.markdown b/documentation/api/query/v4/factsets.markdown index 6d5b08e1ef..fe9f32f299 100644 --- a/documentation/api/query/v4/factsets.markdown +++ b/documentation/api/query/v4/factsets.markdown @@ -18,9 +18,11 @@ canonical: "/puppetdb/latest/api/query/v4/factsets.html" [producers]: ./producers.markdown [nodes]: ./nodes.markdown -You can query factsets by making an HTTP request to the `/factsets` endpoint. - -A factset is the set of all facts for a single certname. +The `/factsets` endpoint provides access to a represntation of node +factsets where each result includes the structured facts for a node +broken down into a vector of top-level key/value pairs. Note that the +`inventory` endpoint will often provide more flexible and efficient +access to the same information. ## `/pdb/query/v4/factsets` diff --git a/documentation/api/query/v4/inventory.markdown b/documentation/api/query/v4/inventory.markdown index 95f8012fb1..11f3863c9d 100644 --- a/documentation/api/query/v4/inventory.markdown +++ b/documentation/api/query/v4/inventory.markdown @@ -20,9 +20,9 @@ canonical: "/puppetdb/latest/api/query/v4/inventory.html" [query]: query.markdown [ast]: ./ast.markdown -The `/inventory` endpoint enables an alternative query syntax for digging into -structured facts, and can be used instead of the `facts`, `fact-contents`, and -`factsets` endpoints for most fact-related queries. +The `/inventory` endpoint provides an alternate and potentially more +efficient way to access structured facts as compared to the `facts`, +`fact-contents`, and `factsets` endpoints. ## `/pdb/query/v4/inventory` From 17ee84df3b7029998ee1642cbfe3434fba8157d5 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Tue, 30 Apr 2024 12:55:25 -0500 Subject: [PATCH 05/20] (PDB-5739) fact-query->sql: change stable||volatile to union Since we've observed that the || operation can be expensive, and we don't actually want a full factset map here (we want the individual top-level subtrees), rewrite it as a union. This shouldn't change anything material since pdb ensures the two key sets are disjoint. This also assumes that it's not important to be compatible with the existing stable||volatile index here. The existing query could only have been using the index if postgres is clever enough to see all the way through the name -> key (via jsonb_each/.*) operations to identify cases where the index might be relevant. --- src/puppetlabs/puppetdb/query.clj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/puppetlabs/puppetdb/query.clj b/src/puppetlabs/puppetdb/query.clj index 0c03a90325..5b90328180 100644 --- a/src/puppetlabs/puppetdb/query.clj +++ b/src/puppetlabs/puppetdb/query.clj @@ -385,8 +385,10 @@ FROM (SELECT certname, producer_timestamp, environment_id, - (jsonb_each((stable||volatile))).* - FROM factsets) fs + facts.* + FROM factsets, + lateral (select * from jsonb_each(stable) + union all select * from jsonb_each(volatile)) as facts) fs LEFT JOIN environments env ON fs.environment_id = env.id ) AS facts WHERE %s" (column-map->sql fact-columns) where)] From b0e92a4eb384a7dffc285b58f26ed14403f23617 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Tue, 30 Apr 2024 12:55:25 -0500 Subject: [PATCH 06/20] (PDB-5739) fact-contents-core: change stable||volatile to union Since we've observed that the || operation can be expensive, and we don't actually want a full factset map here (we want the individual top-level subtrees), rewrite it as a union. This shouldn't change anything material since pdb ensures the two key sets are disjoint. This also assumes that it's not important to be compatible with the existing stable||volatile index here. The existing query could only have been using the index if postgres is clever enough to see all the way through the recursive CTE, name -> key (via jsonb_each/.*) operations to identify cases where the index might be relevant. --- src/puppetlabs/puppetdb/query_eng/engine.clj | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index c17e24dae5..bea22a60a2 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -546,12 +546,11 @@ " left join lateral (" " with recursive flattened_one (parent_path, parent_types, key, value, type) as (" " select" - " array[]::text[]," - " ''," - " (jsonb_each(fs.stable||fs.volatile)).*," - " 's'" + " array[]::text[], '', facts.*, 's'" + " from (select * from jsonb_each(fs.stable)" + " union all select * from jsonb_each(fs.volatile)) as facts" " union all" - ;; -- jsonb_each().* expands into key and value columns + ;; -- jsonb_each().* expands into key and value columns via facts.* " select" " parent_path || flattened_one.key," " parent_types || flattened_one.type," From 1ef77367b1c144486993d07b0fb96f76c6b4c068 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Wed, 24 Apr 2024 18:09:36 -0500 Subject: [PATCH 07/20] (PDB-5739) factsets-query-base: change stable||volatile to union Since we've observed that the || operation can be expensive, and we don't actually want a full factset map here (we want the individual top-level subtrees), rewrite it as a union. This shouldn't change anything material since pdb ensures the two key sets are disjoint. This also assumes that it's not important to be compatible with the existing stable||volatile index here. The existing query could only have been using the index if postgres is clever enough to see all the way through the json_agg -> json_build_object -> name -> key (via jsonb_each/.*) operations to identify cases where the index might be relevant. --- src/puppetlabs/puppetdb/query_eng/engine.clj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index bea22a60a2..c40d563718 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -1373,7 +1373,10 @@ [(hsql-hash-as-href "fs.certname" :factsets :facts) :href]] :from [[{:select [[:key :name] :value :fs.certname] - :from [[[:raw "jsonb_each(fs.volatile || fs.stable)"]]]} + :from [[{:union-all + [{:select :* :from [[[:jsonb_each :fs.stable]]]} + {:select :* :from [[[:jsonb_each :fs.volatile]]]}]} + :t-union]]} :t]]} :facts_data]]} :join-deps #{:fs}} From 18d7b8432e30b62fa267a8e7aef577e3ad49c7bc Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 11:29:04 -0500 Subject: [PATCH 08/20] (PDB-5739) drop-local-unused-joins-from-query: include cross-joins Add a missing hsql case. --- src/puppetlabs/puppetdb/query_eng/engine.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index c40d563718..623a37e37f 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -3281,7 +3281,8 @@ function-reqs (extract-function-deps plan) required? (set/union proj-reqs where-reqs function-reqs) join-key? (if (empty? (:call plan)) - #{:full-join + #{:cross-join + :full-join :join :left-join :merge-full-join From 47fb1012e7bfb664e93529114b783c707bb5933d Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 11:36:55 -0500 Subject: [PATCH 09/20] (PDB-5739) facts-query: change stable||volatile to union Since we've observed that the || operation can be expensive, and we don't actually want a full factset map here (we want the individual top-level subtrees), rewrite it as a union. This shouldn't change anything material since pdb ensures the two key sets are disjoint. This also assumes that it's not important to be compatible with the existing stable||volatile index here. The existing query could only have been using the index if postgres is clever enough to see through the key (via jsonb_each/.*) operation to identify cases where the index might be relevant. --- src/puppetlabs/puppetdb/query_eng/engine.clj | 17 +++++++++++------ .../puppetdb/http/query_logging_test.clj | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 623a37e37f..92a9efb0fa 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -520,15 +520,20 @@ :queryable? true :field :fs.value :join-deps #{:fs}}} - :selection {:from [[[:raw - (str "(select certname," - " environment_id," - " (jsonb_each((stable||volatile))).*" - " from factsets)")] + ;; Consider rewrite-fact-query when making any changes here + :selection {:select [:certname :environment_id :key :value] + :from [[{:select [:certname :environment_id + :facts.key :facts.value] + :from :factsets + ;; fixme: don't need cross-join? + :cross-join [[[:lateral + {:union-all + [{:select :* :from [[[:jsonb_each :stable]]]} + {:select :* :from [[[:jsonb_each :volatile]]]}]}] + :facts]]} :fs]] :left-join [[:environments :env] [:= :fs.environment_id :env.id]]} - :relationships (merge certname-relations {"environments" {:local-columns ["environment"] :foreign-columns ["name"]} diff --git a/test/puppetlabs/puppetdb/http/query_logging_test.clj b/test/puppetlabs/puppetdb/http/query_logging_test.clj index 7709721c23..4b1a6d304e 100644 --- a/test/puppetlabs/puppetdb/http/query_logging_test.clj +++ b/test/puppetlabs/puppetdb/http/query_logging_test.clj @@ -82,7 +82,7 @@ "foo"] [["/v4" ["from" "facts"]] "\"from\",\"facts\"" - "(jsonb_each((stable||volatile)))" + " JSONB_EACH(stable) UNION ALL " nil] ;; stream-query-result [["/v4/catalogs/myhost.localdomain" [] {:origin "bar"}] @@ -168,7 +168,7 @@ ;; match the AST/SQL logs for facts query (is (logs-include? logs "\"from\",\"facts\"")) - (is (logs-include? logs "(jsonb_each((stable||volatile)))"))))))))))) + (is (logs-include? logs " JSONB_EACH(stable) UNION ALL "))))))))))) (deftest no-PuppetDBServer-tk-service-queries-are-logged-when-log-queries-is-false (tk-log/with-logged-event-maps logs From d299c7aeef04aa38b1a67e69b1d51e4904e5ab36 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 13:25:52 -0500 Subject: [PATCH 10/20] (PDB-5739) -plan->sql AndExpression/OrExpression: produce vector Produce an hsql vector, not a lazy seq. --- src/puppetlabs/puppetdb/query_eng/engine.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 92a9efb0fa..e77fc528c7 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -1740,11 +1740,11 @@ AndExpression (-plan->sql [expr options] - (concat [:and] (map #(-plan->sql % options) (:clauses expr)))) + (apply vector :and (map #(-plan->sql % options) (:clauses expr)))) OrExpression (-plan->sql [expr options] - (concat [:or] (map #(-plan->sql % options) (:clauses expr)))) + (apply vector :or (map #(-plan->sql % options) (:clauses expr)))) NotExpression (-plan->sql [expr options] From c9bd7ca1d0c877c831396a6c3cc9c1c458913fa2 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 13:39:15 -0500 Subject: [PATCH 11/20] (PDB-5739) query-eng.engine: convert various raw operations to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 28 +++++++++++--------- test/puppetlabs/puppetdb/query_eng_test.clj | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index e77fc528c7..70007d64bf 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -423,7 +423,9 @@ (assoc-in [:projections "expires_facts"] {:type :boolean :queryable? true - :field [:raw "coalesce(certname_fact_expiration.expire, true)"] + :field [:coalesce + :certname_fact_expiration.expire + [:inline true]] :join-deps #{:certname_fact_expiration}}) (assoc-in [:projections "expires_facts_updated"] {:type :timestamp @@ -1000,7 +1002,7 @@ :join-deps #{:ci}}} :selection {:from [:certnames] :join [[{:select [:certname_id - [[:raw "array_agg(array[catalog_inputs.type, name])"] :inputs]] + [[:array_agg [:array [:catalog_inputs.type :name]]] :inputs]] :from [:catalog_inputs] :group-by [:certname_id]} :ci] [:= :certnames.id :ci.certname_id]] @@ -1373,7 +1375,9 @@ "facts" {:type :queryable-json :queryable? true :field {:select [(h/row-to-json :facts_data)] - :from [[{:select [[[:raw "json_agg(json_build_object('name', t.name, 'value', t.value))"] + :from [[{:select [[[:json_agg [:json_build_object + [:inline "name"] :t.name + [:inline "value"] :t.value]] :data] [(hsql-hash-as-href "fs.certname" :factsets :facts) :href]] @@ -1502,15 +1506,15 @@ [selection node-purge-ttl] (let [timestamp (-> node-purge-ttl t/ago t/to-string)] (assoc selection - :with {:inactive_nodes {:select [[:certname]] - :from [:certnames_status] - ;; Since we use our own bespoke parameter extraction, we cannot use any parameters - ;; in this wrapping honeysql cte, so we have to format the string ourselves. - ;; If we can switch to using honeysql for parameter extraction, we can define this - ;; filter in honeysql and let it convert the timestamps to SQL. - :where [:raw - (str "(deactivated IS NOT NULL AND deactivated > '" timestamp "')" - " OR (expired IS NOT NULL and expired > '" timestamp "')")]} + :with {:inactive_nodes + {:select [[:certname]] + :from [:certnames_status] + ;; Because have bespoke parameter extraction, + ;; honeysql must not generate any parameters for + ;; this cte (i.e. everything must be inline). + :where [:or + [:and [:<> :deactivated nil] [:> :deactivated [:inline timestamp]]] + [:and [:<> :expired nil] [:> :expired [:inline timestamp]]]]} ;; the postgres query planner currently blindly applies De Morgan's law if ;; it can identify that we have negated these is-not null checks, such as ;; in the default query for active nodes. So in addition to relying on the diff --git a/test/puppetlabs/puppetdb/query_eng_test.clj b/test/puppetlabs/puppetdb/query_eng_test.clj index 4158ff6b76..720ef0aa93 100644 --- a/test/puppetlabs/puppetdb/query_eng_test.clj +++ b/test/puppetlabs/puppetdb/query_eng_test.clj @@ -68,7 +68,7 @@ (deftest test-plan-cte (is (re-matches - #"WITH inactive_nodes AS \(SELECT certname FROM certnames_status WHERE \(deactivated IS NOT NULL AND deactivated > '\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ'\) OR \(expired IS NOT NULL and expired > '\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ'\)\), not_active_nodes AS \(SELECT certname FROM certnames_status WHERE \(deactivated IS NOT NULL\) OR \(expired IS NOT NULL\)\) SELECT table.foo AS \"foo\" FROM table WHERE \(\? = \?\)" + #"WITH inactive_nodes AS \(SELECT certname FROM certnames_status WHERE \(\(deactivated IS NOT NULL\) AND \(deactivated > '\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ'\)\) OR \(\(expired IS NOT NULL\) AND \(expired > '\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ'\)\)\), not_active_nodes AS \(SELECT certname FROM certnames_status WHERE \(deactivated IS NOT NULL\) OR \(expired IS NOT NULL\)\) SELECT table\.foo AS \"foo\" FROM table WHERE \(\? = \?\)" (-> {:projections {"foo" {:type :string :queryable? true :field :table.foo}} From db5a70bc7ff6e2eccae08227b45bfad445b447a7 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 13:46:11 -0500 Subject: [PATCH 12/20] (PDB-5739) -plan->sql Query: nest via :nest not (str ...) --- src/puppetlabs/puppetdb/query_eng/engine.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 70007d64bf..650624b907 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -1634,7 +1634,7 @@ true (sql/format :allow-dashed-names? true) true first)] (if (:subquery? query) - [:raw (str " ( " sql " ) ")] + [:nest [:raw sql]] sql))) InExpression From 31a9755898fc0477eec49fa47bdb9a2461a8cd90 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 16:20:19 -0500 Subject: [PATCH 13/20] (PDB-5739) jsonb-path-binary-expression: convert to hsql --- src/puppetlabs/puppetdb/scf/storage_utils.clj | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/puppetlabs/puppetdb/scf/storage_utils.clj b/src/puppetlabs/puppetdb/scf/storage_utils.clj index 7c42bc3a7b..003267522c 100644 --- a/src/puppetlabs/puppetdb/scf/storage_utils.clj +++ b/src/puppetlabs/puppetdb/scf/storage_utils.clj @@ -364,19 +364,23 @@ because -> is not indexable (with GIN) but ? is. Assumes a GIN index on the column supplied." [op column qmarks] - (if (= "~" (name op)) - (let [path-elts (cons column qmarks) - path (apply str - (str/join "->" (butlast path-elts)) - (when-let [x (last path-elts)] ["->>" x]))] - [:raw (str/join \space - [(str "((" path ")") (name op) "(?#>>'{}')" - "and" column "??" "?)"])]) - (let [delimited-qmarks (str/join "->" qmarks)] - [:raw (str/join \space - [(str "(" column "->" delimited-qmarks ")") - (name op) "?" - "and" column "??" "?"])]))) + ;; Internal expectations + (when-not (or (vector? column) (keyword? column) (string? column)) + (throw (ex-info "invalid column type" {:value column}))) + (when-not (keyword? op) + (throw (ex-info "invalid operator type" {:value op}))) + (let [column (if (string? column) (keyword column) column)] + (if (= "~" (name op)) + (let [path (cond + (empty? qmarks) column + (empty? (rest qmarks)) [:->> column (first qmarks)] + :else [:->> (apply vector :-> column (butlast qmarks)) (last qmarks)])] + [:and + [:nest [op path [:nest [:#>> [:raw "?"] [:inline "{}"]]]]] + [:? column [:raw "?"]]]) + [:and + [op (apply vector :-> column qmarks) [:raw "?"]] + [:? column [:raw "?"]]]))) (defn jsonb-scalar-cast [typ] From 7e00bac85c89359c8b6e204e2a9cee22f5508eff Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 16:53:12 -0500 Subject: [PATCH 14/20] (PDB-5739) hsql-hash-as-str hsql-hash-as-href: convert to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 30 +++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 650624b907..47803bc5c2 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -67,6 +67,7 @@ (def field-schema (s/conditional keyword? s/Keyword map? {:select s/Any s/Any s/Any} #(= :raw (first %)) [(s/one (s/eq :raw) "raw") (s/one s/Str "sql")] + vector? [s/Any] :else [s/Keyword])) ; The use of 'column' here is a misnomer. This refers to the result of @@ -242,19 +243,16 @@ (map-invert pdb-fns->pg-fns)) (defn hsql-hash-as-str - [column-keyword] - (->> column-keyword - name - su/sql-hash-as-str - (conj [:raw]))) + [column-kw] + [:encode [:cast column-kw :bytea] [:inline "hex"]]) (defn hsql-hash-as-href [entity parent child] - [:raw (str "format(" - (str "'/pdb/query/v4/" (name parent) "/%s/" (name child) "'") - ", " - entity - ")")]) + [:format + [:inline (str "/pdb/query/v4/" (name parent) "/%s/" (name child))] + (if (string? entity) + [:raw entity] + entity)]) (defn hsql-uuid-as-str [column-keyword] @@ -734,7 +732,7 @@ :field {:select [(h/row-to-json :t)] :from [[{:select [[(h/coalesce :metrics (h/scast :metrics_json :jsonb)) :data] - [(hsql-hash-as-href (su/sql-hash-as-str "hash") :reports :metrics) + [(hsql-hash-as-href (hsql-hash-as-str :hash) :reports :metrics) :href]]} :t]]} :join-deps #{:reports}} "logs" {:type :json @@ -742,7 +740,7 @@ :field {:select [(h/row-to-json :t)] :from [[{:select [[(h/coalesce :logs (h/scast :logs_json :jsonb)) :data] - [(hsql-hash-as-href (su/sql-hash-as-str "hash") :reports :logs) + [(hsql-hash-as-href (hsql-hash-as-str :hash) :reports :logs) :href]]} :t]]} :join-deps #{:reports}} "receive_time" {:type :timestamp @@ -795,7 +793,7 @@ :field {:select [(h/row-to-json :event_data)] :from [[{:select [[(json-agg-row :t) :data] - [(hsql-hash-as-href (su/sql-hash-as-str "hash") :reports :events) :href]] + [(hsql-hash-as-href (hsql-hash-as-str :hash) :reports :events) :href]] :from [[{:select [:re.status :re.timestamp @@ -888,7 +886,7 @@ :queryable? false :field {:select [(h/row-to-json :resource_data)] :from [[{:select [[(json-agg-row :t) :data] - [(hsql-hash-as-href "c.certname" :catalogs :resources) :href]] + [(hsql-hash-as-href :c.certname :catalogs :resources) :href]] :from [[{:select [[(hsql-hash-as-str :cr.resource) :resource] :cr.type :cr.title :cr.tags :cr.exported :cr.file :cr.line [(h/scast :rpc.parameters :json) :parameters]] @@ -903,7 +901,7 @@ :queryable? false :field {:select [(h/row-to-json :edge_data)] :from [[{:select [[(json-agg-row :t) :data] - [(hsql-hash-as-href "c.certname" :catalogs :edges) :href]] + [(hsql-hash-as-href :c.certname :catalogs :edges) :href]] :from [[{:select [[:sources.type :source_type] [:sources.title :source_title] [:targets.type :target_type] [:targets.title :target_title] [:edges.type :relationship]] @@ -1379,7 +1377,7 @@ [:inline "name"] :t.name [:inline "value"] :t.value]] :data] - [(hsql-hash-as-href "fs.certname" :factsets :facts) + [(hsql-hash-as-href :fs.certname :factsets :facts) :href]] :from [[{:select [[:key :name] :value :fs.certname] :from [[{:union-all From 853b806418ad9dc3e760b49f5944b6f0c80e4f83 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 17:10:53 -0500 Subject: [PATCH 15/20] (PDB-5739) fn-binary-expression-hsql: convert to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 47803bc5c2..11f6d92213 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -1593,16 +1593,15 @@ provided value. The operator, function name and its arguments must have already been validated." [op function args] - (let [quoted-args + (let [args (case function - ("sum" "avg" "min" "max" "count" "jsonb_typeof") (mapv jdbc/double-quote args) - "to_char" [(jdbc/double-quote (first args)) (jdbc/single-quote (second args))] + ("sum" "avg" "min" "max" "count" "jsonb_typeof") (map keyword args) + "to_char" [(-> args first keyword) [:inline (second args)]] (throw (IllegalArgumentException. (tru "{0} is not a valid function" (pr-str function)))))] - [:raw (format "%s(%s) %s ?" - function - (str/join ", " quoted-args) - op)])) + [(keyword op) + (apply vector (keyword function) args) + [:raw "?"]])) (defn munge-query-ordering [clauses] From a0bf73b4186796265e5779669e5529dc785458ac Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 17:54:42 -0500 Subject: [PATCH 16/20] (PDB-5739) JsonContainsExpression: convert to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 11f6d92213..2456742e88 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -6,7 +6,7 @@ [clojure.tools.logging :as log] [honey.sql :as sql] [honey.sql.helpers :as hsql] - [honey.sql.pg-ops] ;; require to enable postgres ops + [honey.sql.pg-ops :as pgop] ;; require to enable postgres ops [puppetlabs.kitchensink.core :as ks] [puppetlabs.puppetdb.facts :as facts] [puppetlabs.puppetdb.honeysql :as h] @@ -1651,16 +1651,13 @@ :from [[(-plan->sql subquery options) :sub]]}])) JsonContainsExpression - (-plan->sql [{:keys [field column-data array-in-path]} _opts] - (let [f (if (= :raw (:field-type column-data)) - (-> column-data :field second) - field)] - ;; This distinction is necessary (at least) because @> cannot - ;; traverse into arrays, but should be otherwise preferred - ;; because it can use an index. - (if array-in-path - [:raw (format "%s #> ? = ?" f)] - [:raw (format "%s @> ?" f)]))) + (-plan->sql [{:keys [column-data array-in-path]} _opts] + ;; This distinction is necessary (at least) because @> cannot + ;; traverse into arrays, but should be otherwise preferred + ;; because it can use an index. + (if array-in-path + [:= [:#> (:field column-data) [:raw "?"]] [:raw "?"]] + [pgop/at> (:field column-data) [:raw "?"]])) FnBinaryExpression (-plan->sql [{:keys [function args operator]} _opts] From 979afa86e5df2ffffa2ee8936602a290b59dca10 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Thu, 25 Apr 2024 18:04:43 -0500 Subject: [PATCH 17/20] (PDB-5739) -plan->sql NullExpression: convert to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index 2456742e88..bf8cb169df 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -1724,17 +1724,19 @@ (-plan->sql [{:keys [column] :as expr} options] (s/validate column-schema column) (let [queryable-json? (= :queryable-json (:type column)) + hsql-col? (and (vector? (:field column)) + (not (= :raw (-> column :field first)))) lhs (if queryable-json? - (name (h/extract-sql (:field column))) + (if hsql-col? + (:field column) + (name (h/extract-sql (:field column)))) (-plan->sql (:field column) options)) json? (or queryable-json? (= :jsonb-scalar (:type column)))] - (if (:null? expr) - (if json? - (su/jsonb-null? lhs true) - [:is lhs nil]) - (if json? - (su/jsonb-null? lhs false) - [:is-not lhs nil])))) + (if json? + (if hsql-col? + [(if (:null? expr) := :<>) [:jsonb_typeof lhs] [:inline "null"]] + (su/jsonb-null? lhs (boolean (:null? expr)))) + [(if (:null? expr) :is :is-not) lhs nil]))) AndExpression (-plan->sql [expr options] From f3ecea669e446ee8532287f6f4a10125d0a286c6 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Mon, 29 Apr 2024 14:45:22 -0500 Subject: [PATCH 18/20] (PDB-5739) create-json-path-extraction: convert to hsql --- src/puppetlabs/puppetdb/jdbc.clj | 12 ----- src/puppetlabs/puppetdb/query_eng/engine.clj | 49 +++++++++++--------- test/puppetlabs/puppetdb/query_eng_test.clj | 8 ++-- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/src/puppetlabs/puppetdb/jdbc.clj b/src/puppetlabs/puppetdb/jdbc.clj index 5c78853e44..5eb390dd4e 100644 --- a/src/puppetlabs/puppetdb/jdbc.clj +++ b/src/puppetlabs/puppetdb/jdbc.clj @@ -436,18 +436,6 @@ (.setType "text[]") (.setValue (str \{ (str/join \, quoted) \}))))) -;; Q: move/replace? -(defn create-json-path-extraction - "Given a base json field and a path of keys to traverse, construct the proper - SQL query of the form base->'key'->'key2'..." - [field path] - (str field - (when (seq path) - (->> (map single-quote path) - (str/join "->") - ;; prefix for the first arrow in field->'key1'... - (str "->"))))) - ;; TODO this is no longer used by the main query engine (engine.clj), but it is ;; still used by the legacy query engines and should be removed once it is ;; handled by honeysql everywhere diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index bf8cb169df..e485a75357 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -2270,25 +2270,34 @@ (contains? (ks/keyset user-name->query-rec-name) (first expr))) +(defn create-json-path-extraction + "Given a base json field and a path of keys to traverse, construct the proper + SQL query of the form base->'key'->'key2'..." + [field path] + (when (string? field) ; internal expectation + (throw (ex-info "invalid field type" {:value field}))) + ;; The str is to make sure 5 comes through as '5' -- see comments below. + (apply vector :-> field (map #(vector :inline (str %)) path))) + (defn create-json-subtree-projection [[top & path :as _parsed-field] projections] - (let [stringify (fn [{:keys [field field-type]}] - (case field-type - :keyword (name field) - :raw (second field) - (throw (IllegalArgumentException. - (tru "Cannot determine field type for field ''{0}''" field)))))] - {:type :json - :queryable? false - :field [:raw - (jdbc/create-json-path-extraction - ;; Extract the original field as a string - (-> top :name projections stringify) - (map #(case (:kind %) - ::parse/named-field-part (:name %)) - path))] - ;; json-path will be something like ("facts" "kernel" ...) - :join-deps (get-in projections [(:name top) :join-deps])})) + {:type :json + :queryable? false + :field (create-json-path-extraction + ;; Extract the original field as a string + (let [{:keys [field field-type]} (-> top :name projections)] + (case field-type + (:keyword :raw) field + (do + (when-not (vector? field) + (throw (IllegalArgumentException. + (tru "Cannot determine field type for field ''{0}''" field)))) + field))) + (map #(case (:kind %) + ::parse/named-field-part (:name %)) + path)) + ;; json-path will be something like ("facts" "kernel" ...) + :join-deps (get-in projections [(:name top) :join-deps])}) (defn create-extract-node* "Returns a `query-rec` that has the correct projection for the given @@ -2655,10 +2664,8 @@ cinfo (get-in query-rec [:projections (:name top)])] (if (and (= :queryable-json (:type cinfo)) (seq path)) - (let [field (name (h/extract-sql (:field cinfo))) - json-path (->> (map :name path) - (jdbc/create-json-path-extraction field) - (conj [:raw]))] + (let [field (:field cinfo) + json-path (create-json-path-extraction field (map :name path))] (map->NullExpression {:column (assoc cinfo :field json-path) :null? value})) (map->NullExpression {:column cinfo :null? value}))) diff --git a/test/puppetlabs/puppetdb/query_eng_test.clj b/test/puppetlabs/puppetdb/query_eng_test.clj index 720ef0aa93..336a446b09 100644 --- a/test/puppetlabs/puppetdb/query_eng_test.clj +++ b/test/puppetlabs/puppetdb/query_eng_test.clj @@ -176,26 +176,26 @@ (deftest test-extract-json-subtree-compiles (testing "with differing levels of subtrees" - (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\)->'os' AS \"facts\.os\" .*FROM factsets" + (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\) -> 'os' AS \"facts\.os\" .*FROM factsets" (->> ["extract" "facts.os"] (compile-user-query->sql inventory-query) :results-query first))) - (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\)->'os'->'family' AS \"facts\.os\.family\" .*FROM factsets" + (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\) -> 'os' -> 'family' AS \"facts\.os\.family\" .*FROM factsets" (->> ["extract" "facts.os.family"] (compile-user-query->sql inventory-query) :results-query first)))) (testing "when field is raw sql" - (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\)->'trusted'->'certname' AS \"trusted\.certname\" .*FROM factsets" + (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\)->'trusted' -> 'certname' AS \"trusted\.certname\" .*FROM factsets" (->> ["extract" "trusted.certname"] (compile-user-query->sql inventory-query) :results-query first)))) (testing "when field is a keyword" - (is (re-find #"SELECT rpc.parameters->'foo' AS \"parameters\.foo\" .*FROM catalog_resources" + (is (re-find #"SELECT rpc.parameters -> 'foo' AS \"parameters\.foo\" .*FROM catalog_resources" (->> ["extract" "parameters.foo"] (compile-user-query->sql resources-query) :results-query From e38db4b8ab0e73291e17fee52ef65cbbf5ad5429 Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Mon, 29 Apr 2024 15:17:47 -0500 Subject: [PATCH 19/20] (PDB-5739) inventory-query: convert stable||volatile to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 10 ++++------ test/puppetlabs/puppetdb/query_eng_test.clj | 11 ++++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index e485a75357..fcf9ab8cde 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -281,14 +281,12 @@ "facts" {:type :queryable-json :projectable-json? true :queryable? true - :field [:raw "(fs.stable||fs.volatile)"] - :field-type :raw + :field [:nest [:|| :fs.stable :fs.volatile]] :join-deps #{:fs}} "trusted" {:type :queryable-json :projectable-json? true :queryable? true - :field [:raw "(fs.stable||fs.volatile)->'trusted'"] - :field-type :raw + :field [:-> [:|| :fs.stable :fs.volatile] [:inline "trusted"]] :join-deps #{:fs}}} :selection {:from [[:factsets :fs]] @@ -1666,8 +1664,8 @@ JsonbPathBinaryExpression (-plan->sql [{:keys [field value column-data operator]} _opts] (su/jsonb-path-binary-expression operator - (if (= :raw (:field-type column-data)) - (-> column-data :field second) + (if (vector? (:field column-data)) + (:field column-data) field) value)) diff --git a/test/puppetlabs/puppetdb/query_eng_test.clj b/test/puppetlabs/puppetdb/query_eng_test.clj index 336a446b09..a134a6d028 100644 --- a/test/puppetlabs/puppetdb/query_eng_test.clj +++ b/test/puppetlabs/puppetdb/query_eng_test.clj @@ -176,19 +176,19 @@ (deftest test-extract-json-subtree-compiles (testing "with differing levels of subtrees" - (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\) -> 'os' AS \"facts\.os\" .*FROM factsets" + (is (re-find #"SELECT \(fs.stable \|\| fs.volatile\) -> 'os' AS \"facts\.os\" .*FROM factsets" (->> ["extract" "facts.os"] (compile-user-query->sql inventory-query) :results-query first))) - (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\) -> 'os' -> 'family' AS \"facts\.os\.family\" .*FROM factsets" + (is (re-find #"SELECT \(fs.stable \|\| fs.volatile\) -> 'os' -> 'family' AS \"facts\.os\.family\" .*FROM factsets" (->> ["extract" "facts.os.family"] (compile-user-query->sql inventory-query) :results-query first)))) (testing "when field is raw sql" - (is (re-find #"SELECT \(fs.stable\|\|fs.volatile\)->'trusted' -> 'certname' AS \"trusted\.certname\" .*FROM factsets" + (is (re-find #"SELECT \(\(fs.stable \|\| fs.volatile\) -> 'trusted'\) -> 'certname' AS \"trusted\.certname\" .*FROM factsets" (->> ["extract" "trusted.certname"] (compile-user-query->sql inventory-query) :results-query @@ -201,14 +201,15 @@ :results-query first))))) +;; ??? (and check all other || and/or jsonb_each in tests) (deftest index-hit-for-trusted-facts-on-inventory (testing "facts.trusted.extensions and trusted.extensions should generate the same SQL" - (is (re-find #"WHERE \(fs.stable\|\|fs.volatile\) @> ?" + (is (re-find #"WHERE \(fs.stable \|\| fs.volatile\) @> ?" (->> ["extract" [] ["=" "facts.trusted.extensions.foo" "bar"]] (compile-user-query->sql inventory-query) :results-query first))) - (is (re-find #"WHERE \(fs.stable\|\|fs.volatile\) @> ?" + (is (re-find #"WHERE \(fs.stable \|\| fs.volatile\) @> ?" (->> ["extract" [] ["=" "trusted.extensions.foo" "bar"]] (compile-user-query->sql inventory-query) :results-query From 9df5ae255014e0f0b838014c3a0bcb15ba1d189c Mon Sep 17 00:00:00 2001 From: Rob Browning <rlb@puppet.com> Date: Mon, 13 May 2024 16:06:01 -0500 Subject: [PATCH 20/20] (PDB-5745) rewrite-fact-query: convert to hsql --- src/puppetlabs/puppetdb/query_eng/engine.clj | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/puppetlabs/puppetdb/query_eng/engine.clj b/src/puppetlabs/puppetdb/query_eng/engine.clj index fcf9ab8cde..a34c4ea83c 100644 --- a/src/puppetlabs/puppetdb/query_eng/engine.clj +++ b/src/puppetlabs/puppetdb/query_eng/engine.clj @@ -3017,15 +3017,16 @@ ;; and thus still have no results. (let [fact-name (name-constraint clause)] [(update facts-query :selection assoc - :from [[[:raw (str "(select certname," - " environment_id, " - " ?::text as key, " - " (stable||volatile)->? as value" - " from factsets" - " where (stable||volatile) ?? ?" - ")")] + :from [[{:select [:certname :environment_id + [[:cast [:raw "?" ] :text] :key] + [[[:coalesce + [:-> :volatile [:raw "?"]] + [:-> :stable [:raw "?"]]]] + :value]] + :from :factsets + :where [[:? [:|| :stable :volatile] [:raw "?"]]]} :fs]] - :selection-params [fact-name fact-name fact-name]) + :selection-params [fact-name fact-name fact-name fact-name]) user-query]) :else [query-rec user-query])))