 and their parameters which match."
   [[sql & params]]
   {:pre [(string? sql)]}
-  (let [query         (str "SELECT certname_catalogs.certname, cr.resource, cr.type, cr.title,"
-                           "cr.tags, cr.exported, cr.sourcefile, cr.sourceline, rp.name, rp.value "
-                           "FROM catalog_resources cr "
-                           "JOIN certname_catalogs USING(catalog) "
-                           "LEFT OUTER JOIN resource_params rp "
-                           "ON cr.resource = rp.resource "
-                           "WHERE (cr.catalog,cr.resource) IN "
-                           sql)
-        results       (apply query-to-vec query params)
+  (let [query         (format (str "SELECT certname_catalogs.certname, cr.resource, cr.type, cr.title,"
+                                   "cr.tags, cr.exported, cr.sourcefile, cr.sourceline, rp.name, rp.value "
+                                   "FROM catalog_resources cr "
+                                   "JOIN certname_catalogs USING(catalog) "
+                                   "LEFT OUTER JOIN resource_params rp "
+                                   "ON cr.resource = rp.resource "
+                                   "WHERE (cr.catalog,cr.resource) IN (%s)")
+                              sql)
+        results (apply query-to-vec query params)
         metadata_cols [:certname :resource :type :title :tags :exported :sourcefile :sourceline]
         metadata      (apply juxt metadata_cols)]
     (vec (for [[resource params] (group-by metadata results)]

;; ## SQL query compiler

(ns com.puppetlabs.puppetdb.query.resource
  (:refer-clojure :exclude [case compile conj! distinct disj! drop sort take])
  (:require [com.puppetlabs.utils :as utils]
            [cheshire.core :as json]
            [clojure.string :as string])
  (:use clojureql.core
        [com.puppetlabs.jdbc :only [query-to-vec convert-result-arrays with-transacted-connection]]
        [com.puppetlabs.puppetdb.scf.storage :only [db-serialize sql-array-query-string]]
        [clojure.core.match :only [match]]))

(defmulti compile-query->sql
  "Recursively compile a query into a collection of SQL operations."
  (fn [query]
    (string/lower-case (first query))))

(defn query->sql
  "Compile a vector-structured query into an SQL expression.

;; will produce a query that selects a set of hashes matching the
;; predicate, which can then be combined with connectives to build
;; complex queries.
(defmethod compile-term "="
  [[op path value :as term]]
  (let [count (count term)]
    (if (not (= 3 count))
      (throw (IllegalArgumentException.
              (format "%s requires exactly two arguments, but we found %d" op (dec count))))))
  (match [path]
         ;; tag join.
         ["tag"]
         {:where (sql-array-query-string "tags")
          :params [value]}

         ;; node join.
         [["node" "name"]]
         {:joins [:certname_catalogs]
          :where "certname_catalogs.certname = ?"
          :params [value]}

         ;; {in,}active nodes.
         [["node" "active"]]
         {:joins [:certname_catalogs :certnames]
          :where (format "certnames.deactivated IS %s" (if value "NULL" "NOT NULL"))}

         ;; param joins.
         [["parameter" (name :when string?)]]
         {:where "EXISTS (SELECT rp.resource FROM resource_params rp WHERE rp.name = ? AND rp.value = ? AND rp.resource = catalog_resources.resource)"
          :params [name (db-serialize value)]}

         ;; metadata match.
         [(metadata :when string?)]
         (if (re-matches #"(?i)[a-z_][a-z0-9_]*" metadata)
           {:where (format "catalog_resources.%s = ?" metadata)
            :params [value]}
           (throw (IllegalArgumentException. "illegal metadata column name %s" metadata)))

         ;; ...else, failure
         :else (throw (IllegalArgumentException.
                        (str term " is not a valid query term")))))

(defn- alias-subqueries
  "Produce distinct aliases for a list of queries, suitable for a join
