Intro to S-SQL

A B C D E F G H I J K L M N O P R S T U V W Special Characters Calling Postgresql Stored Functions and Procedures

Intro to S-SQL

Postmodern can use any sql string in a query. It also has its own lispy syntax called s-sql. Various examples using postmodern will be given in both standard sql and s-sql. Note that not all of sql has been implemented in the s-sql syntax. Postmodern is fully capable of handling any sql string. It just looks a little ugly once you get used to looking at lisp type syntax.

Consider the following database calls and the return. Note that all query functions are postmodern functions, after this next example, I'm going to shorten the function call and drop the "postmodern:".

(postmodern:query "select id, name from countries where name=$1" "Vietnam")

((68 "Vietnam"))

This can be rephrased in s-sql as:

(query (:select 'id 'name
                :from 'countries
                :where (:= 'name '$1))

        "Vietnam")

You will notice that the commas have dropped out, columns and table names are inital-quoted and the sql operators have colons in from. It does look more "lispy" doesn't it?

It can be handy to note that replacing "query" with "sql" returns the sql statement rather than trying to execute the query. This can be helpful in designing s-sql queries. Thus:

(sql (:select 'countries.name 'regions.name :distinct
              :from 'regions 'countries
              :where (:= 'regions.id 'countries.region_id)))

 "(SELECT DISTINCT countries.name, regions.name FROM regions, countries WHERE (regions.id = countries.region_id))"

Sql-escape, Sql-escape-string

Does what it says on the tin. It escapes a string so that you can safely include the string in an sql query.

(let ((x "\#udlsh29c#^"))
   (sql-escape x))

"E'#udlsh29c#^'"

(sql-escape-string "\#udlsh29c#^")

"E'#udlsh29c#^'"

Sql-compile

sql-compile is the run-time version of the sql macro, which means that it converts a list into an sql query. See the following as an example. Note carefully the backquotes and commas.

(defun create-table1 (table-name-string &rest rest)
  "Each of the parameters after the table-name must be in the form ofa two parameter list - the column name as a string and the type as a symbol. See the following as an example"
  (query (postmodern:sql-compile
          `(:create-table ,table-name-string ,(loop for y in rest collect
                                                    (list (first y)
                                                          :type (second y)))))))

(create-table1 "test25" (list "name" 'string) (list "address" 'string))

You also can see how it is used in the following queries handling some insertions and updates in which plists were providing the source of columns and values.

(query
 (sql-compile
  (append `(:insert-into ,table :set) plst)))

(query
 (sql-compile
  (append (append `(:update ,table :set)
                  plst)
          `(:where (:= 'id ,id)))))

Remember, if you are using sql-compile or any other method to create dynamic queries, you are responsible for ensuring the security. All user input should be sanitized.

Return Types

You can give postmodern various directions, using keywords, for way that values get returned. Some of these keywords will be used in various examples to follow.

:none Ignore the result values.
:lists, :rows Return a list of lists, each list containing the values for a row.
:list, :row Return a single row as a list.
:alists Return a list of alists which map column names to values, with the names represented as keywords.
:alist Return a single row as an alist.
:str-alists Like :alists, but use the original column names.
:str-alist Return a single row as an alist, with strings for names.
:plists Return a list of plists which map column names to values,with the names represented as keywords.
:plist Return a single row as a plist.
:column Return a single column as a list.
:single Return a single value. Will raise an error if the query returns more than one field. If the query returns more than one row, it returns the first row.
:single! Like :single except that it will throw an error when the number of selected rows is not equal to 1.
:vectors Return a vector of vectors, each vector containing the values for a row. (This is only the plural)
:array-hash Return an array of hashtables which map column names to hash table keys
:json-strs Return a list of strings where each row is a json object expressed as a string
:json-str Return a single string where the row returned is a json object expressed as a string
:json-array-str Return a string containing a json array, each element in the array is a selected row expressed as a json object
(:dao type) Return a list of DAOs of the given type. The names of the fields returned by the query must match slots in the DAO class the same way as with query-dao.
(:dao type :single) Return a single DAO of the given type.

Consider the following database calls, written in s-sql and the return, noting how the ending keywords affect the type of return:

(query (:select 'name
        :from 'countries
        :where (:> 'latitude '$1))
       60)

(("Faroe Islands") ("Finland") ("Greenland") ("Iceland") ("Norway") ("Sweden"))

:none

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :none)

NIL

:lists

Return a list of lists, each list containing the values for a row.

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :lists)

(("Faroe Islands") ("Iceland") ("Greenland") ("Sweden") ("Norway") ("Finland"))

:rows

Same as lists - Return a list of lists, each list containing the values for a row.

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :rows)

(("Faroe Islands") ("Iceland") ("Greenland") ("Sweden") ("Norway") ("Finland"))

:alist

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :alist)

((:NAME . "Faroe Islands"))

:str-alist

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :str-alist)

(("name" . "Faroe Islands"))

:alists

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :alists)

(((:NAME . "Faroe Islands")) ((:NAME . "Finland")) ((:NAME . "Greenland"))  ((:NAME . "Iceland")) ((:NAME . "Norway")) ((:NAME . "Sweden")))

:str-alists

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :str-alists)

((("name" . "Faroe Islands")) (("name" . "Finland")) (("name" . "Greenland"))  (("name" . "Iceland")) (("name" . "Norway")) (("name" . "Sweden")))

:plist

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :plist)

(:NAME "Faroe Islands")

:plists

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :plists)

((:NAME "Faroe Islands") (:NAME "Iceland") (:NAME "Greenland")  (:NAME "Sweden") (:NAME "Norway") (:NAME "Finland"))

:array-hash

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :array-hash)

#(#<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFB5A3}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFBB63}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFC123}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFC6E3}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFCCA3}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFD263}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFD823}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFDDE3}>   #<HASH-TABLE :TEST EQUAL :COUNT 1 {1005DFE3A3}>)

:single

Returns a single value. Will raise an error if the query returns more than one field. If the query returns more than one row, it returns the first row.

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :single)

"Faroe Islands"

:single!

Like :single except that it will throw an error when the number of selected rows is not equal to 1.

(query (:select 'name
                :from 'countries
                :where (:> 'latitude '$1))
       60 :single!)

; Evaluation aborted on #<CL-POSTGRES:DATABASE-ERROR {100E83B813}>. LISP-TAX-TEST> Database error: Query for a single row returned 6 rows.    [Condition of type DATABASE-ERROR]

:column

:PROPERTIES: :CUSTOM_ID: return-type-column :END Returns a single column as a list. The first example shows the default value returned without the :column qualifier. The second example shows the result with the column qualifier. :

(query (:select 'name

                :from 'countries

                :where (:> 'latitude '$1))

       60 :column)

("Faroe Islands" "Finland" "Greenland" "Iceland" "Norway" "Sweden")

:vectors

:PROPERTIES: :CUSTOM_ID: return-type-vector :END

(query (:select 'id 'int4 'text :from 'test-data)
       :vectors)
#(#(1 2147483645 "text one")
  #(2 0 "text two")
  #(3 3 "text three"))

(query (:select 'id 'int4 'text :from 'test-data :where (:< 'id 1))
       :vectors)
#()

(:dao dao-type)

This assumes you have already created a class for this table.

(query (:select '* :from 'countries)
       (:dao country))

(#<COUNTRY {1004F1BAF3}> #<COUNTRY {1004F1BD73}> #<COUNTRY {1004F1BFF3}>)

(:dao dao-type :single)

(query (:select '* :from 'countries :where (:= 'name "Iceland")
       (:dao country))

#<COUNTRY {1004F1BAF3}>

Json-strs

Return a list of strings where the row returned is a json object expressed as a string

(query (:select 'id 'int4 'text :from 'short-data-type-tests :where (:< 'id 3)) :json-strs)
("{\"id\":1,\"int4\":2147483645,\"text\":\"text one\"}"
 "{\"id\":2,\"int4\":0,\"text\":\"text two\"}")

This will also handle local-time timestamps and simple-date timestamps, time-of-day and date. E.g. (with a local-time timestamp)

(query (:select 'timestamp-with-time-zone
        :from 'test-data
        :where (:< 'id 3))
       :json-strs)

'("{\"timestampWithTimeZone\":\"{2019-12-30T13:30:54.000000-05:00}\"}"
  "{\"timestampWithTimeZone\":\"{1919-12-30T13:30:54.000000-05:00}\"}")

The following is an example with a simple-date timestamp.

(query (:select 'timestamp-with-time-zone
        :from 'test-data
        :where (:< 'id 3)) :json-strs)
'("{\"timestampWithTimeZone\":\"2019-12-30 18:30:54:0\"}"
  "{\"timestampWithTimeZone\":\"1919-12-30 18:30:54:0\"}")

Json-str

Return a single string where the row returned is a json object expressed as a string

(query (:select 'id 'int4 'text :from 'short-data-type-tests :where (:= 'id 3)) :json-str)
"{\"id\":3,\"int4\":3,\"text\":\"text three\"}"

As with :json-strs, this will also work for either simple-date or local-time timestamps

Json-array-str

Return a string containing a json array, each element in the array is a selected row expressed as a json object. NOTE: If there is no result, this will return a string with an empty json array.

(query (:select 'id 'int4 'text :from 'short-data-type-tests :where (:< 'id 3)) :json-array-str)
"[{\"id\":1,\"int4\":2147483645,\"text\":\"text one\"}, {\"id\":2,\"int4\":0,\"text\":\"text two\"}]"

(query (:select 'id 'int4 'text :from 'test-data :where (:< 'id 1)) :json-array-str)
"[]"

As with :json-strs, this will also work for either simple-date or local-time timestamps

As or Alias

Suppose you want to return an identifier as a key with the value, but you don't want to use the column name. You can use the as keyword, or as you would expect having just seen a little s-sql, the :as keyword.

(first (query (:order-by
               (:select (:as 'countries.name 'countryname)
                        :from 'countries)
               'countryname )
              :alists))

((:COUNTRYNAME . "Afghanistan"))

You can also do this with table names.

(first (query (:order-by
               (:select 't1.name
                        :from (:as 'countries 't1))
               'name )
              :alists))

((:NAME . "Afghanistan"))

:|| Concatenating Columns

The concatenation operator combines two or more columns into a single column return. First, consider the query on a raw sql string:

(query "(SELECT countries.id, (countries.name || '-' || regions.name)
         FROM countries, regions
         WHERE ((regions.id = countries.region_id) and (countries.name = 'US')))")

((21 "US-North America"))

Now consider the result using s-sql.

(query (:select 'countries.id (:|| 'countries.name "-" 'regions.name)
                :from 'countries 'regions
                :where (:and (:= 'regions.id 'countries.region-id)
                             (:= 'countries.name "US"))))

((21 "US-North America"))