diff --git a/source/AirGQL/Lib.hs b/source/AirGQL/Lib.hs
index 4398b1c..02dd498 100644
--- a/source/AirGQL/Lib.hs
+++ b/source/AirGQL/Lib.hs
@@ -738,7 +738,10 @@ lintTable allEntries parsed =
       CreateTable names _ _
         | Just name <- getFirstName (Just names)
         , "_by_pk" `isInfixOf` name ->
-            pure "Table names cannot contain \"_by_pk\""
+            pure $
+              "Table names shouldn't contain \"_by_pk\", yet \""
+                <> name
+                <> "\" does"
       _ -> []
   in
     rowidReferenceWarnings <> withoutRowidWarning <> illegalName
diff --git a/tests/Spec.hs b/tests/Spec.hs
index 9e7bef4..98e0969 100644
--- a/tests/Spec.hs
+++ b/tests/Spec.hs
@@ -15,7 +15,6 @@ import Protolude (
   Maybe (Just, Nothing),
   Monoid (mempty),
   fromMaybe,
-  show,
   ($),
   (&),
   (<>),
@@ -27,7 +26,6 @@ import Data.Aeson qualified as Ae
 import Data.Aeson.KeyMap qualified as KeyMap
 import Data.Aeson.Types (Object)
 import Data.List qualified as List
-import Data.Text (pack)
 import Data.Text qualified as T
 import Database.SQLite.Simple (
   Query,
@@ -107,11 +105,11 @@ import AirGQL.Utils (
   withRetryConn,
  )
 import Tests.IntrospectionSpec qualified
+import Tests.QuerySpec qualified
 import Tests.Utils (
   dbPath,
   fixtureDbId,
   rmSpaces,
-  shouldSaveDbs,
   testRoot,
   unorderedShouldBe,
   withDataDbConn,
@@ -669,6 +667,22 @@ testSuite = do
 
           result.errors `shouldBe` [expectedMessage]
 
+      it "should not allow '_by_pk' in table names" $ do
+        let dbId = "api-sql-by-pk-name"
+        let query = "CREATE TABLE foo_by_pk ( bar TEXT )"
+        withDataDbConn dbId $ \_ -> do
+          Right result <-
+            runHandler $
+              sqlQueryPostHandler
+                PragmaConf.defaultConf
+                ("_TEST_" <> dbId)
+                SQLPost{query = query}
+
+          let expectedMessage =
+                [raw|Table names shouldn't contain "_by_pk", yet "foo_by_pk" does|]
+
+          result.errors `shouldBe` [expectedMessage]
+
       it "should return no affected tables on a simple select" $ do
         let dbId = "api-sql-simple-select"
         withDataDbConn dbId $ \conn -> do
@@ -760,723 +774,6 @@ testSuite = do
 
           result.affectedTables `shouldMatchList` ["notes", "todos"]
 
-  describe "Queries" $ do
-    it "supports retrieving data" $ do
-      conn <- SS.open dbPath
-      execute_
-        conn
-        [sql|
-          insert into users (name, email, created_utc)
-          values ('Adrian', 'adrian@example.com', '2021-01-01T00:00Z')
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <-
-        getDerivedSchema
-          defaultSchemaConf
-          conn
-          fixtureDbId
-          tables
-
-      Right result <- graphql schema Nothing mempty "{ users { name } }"
-      Ae.encode result
-        `shouldBe` [gql|
-        {"data":{"users":[{"name":"Adrian"}]}}
-      |]
-
-    it "supports retrieving data from tables with special names" $ do
-      let testDbPath = testRoot </> "special_table_name.db"
-      removeIfExists testDbPath
-      let dbPathNorm = if shouldSaveDbs then testDbPath else ":memory:"
-
-      withRetryConn dbPathNorm $ \conn -> do
-        execute_
-          conn
-          [sql|
-            CREATE TABLE "name with-special$chars" (
-              name TEXT
-            )
-          |]
-        execute_
-          conn
-          [sql|
-            INSERT INTO "name with-special$chars" (name)
-            VALUES ('John')
-          |]
-
-        Right tables <- getEnrichedTables conn
-        schema <-
-          getDerivedSchema
-            defaultSchemaConf
-            conn
-            fixtureDbId
-            tables
-
-        Right result <-
-          graphql
-            schema
-            Nothing
-            mempty
-            "{ nameXX0withXXDspecialXX4chars { name } }"
-        Ae.encode result
-          `shouldBe` [gql|
-          {"data":{"nameXX0withXXDspecialXX4chars":[{"name":"John"}]}}
-        |]
-
-    describe "column names with special characters" $ do
-      let
-        dbPathSpaces :: FilePath = testRoot </> "spaces-test.db"
-        setupDatabaseSpaces = do
-          removeIfExists dbPathSpaces
-          conn <- open dbPathSpaces
-          execute_
-            conn
-            [sql|
-              CREATE TABLE IF NOT EXISTS test_entries (
-                id INTEGER PRIMARY KEY,
-                `column with spaces` TEXT
-              )
-
-            |]
-          execute_
-            conn
-            [sql|
-            INSERT INTO test_entries (id, `column with spaces`)
-            VALUES (0, 'Just a test')
-          |]
-
-      before_ setupDatabaseSpaces $ it "supports column names with spaces" $ do
-        conn <- open dbPathSpaces
-        Right tables <- getEnrichedTables conn
-        schema <-
-          getDerivedSchema
-            defaultSchemaConf
-            conn
-            (pack dbPathSpaces)
-            tables
-        Right result <-
-          graphql
-            schema
-            Nothing
-            mempty
-            "{ test_entries { columnXX0withXX0spaces } }"
-        Ae.encode result
-          `shouldBe` rmSpaces
-            [raw|
-          { "data": {
-              "test_entries": [
-                { "columnXX0withXX0spaces": "Just a test" }
-              ]
-          }}
-        |]
-
-      before_ setupDatabaseSpaces $
-        it "generates introspection schema for column names with spaces" $
-          do
-            conn <- open dbPathSpaces
-            Right tables <- getEnrichedTables conn
-            schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-            Right result <-
-              graphql
-                schema
-                Nothing
-                mempty
-                "{ __schema{ types { name fields { name } } } } }"
-            show (Ae.encode result) `shouldContain` "columnXX0withXX0spaces"
-
-    it "avoids column name remapping collisions" $ do
-      let dbPathSpaces = testRoot </> "spaces-collision-test.db"
-      removeIfExists dbPathSpaces
-      conn <- open dbPathSpaces
-      execute_
-        conn
-        [sql|
-          CREATE TABLE IF NOT EXISTS test_entries (
-            id INTEGER PRIMARY KEY,
-            `the column` TEXT,
-            `the_column` TEXT,
-            `the_column_1` TEXT,
-            `the_column_2` TEXT
-          )
-        |]
-      execute_
-        conn
-        [sql|
-        INSERT INTO test_entries
-          (id, `the column`, the_column, the_column_1, the_column_2)
-        VALUES
-          (0, 'with spaces', 'no spaces', 'no spaces 1', 'no spaces 2')
-      |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|
-          { test_entries {
-              theXX0column
-              the_column
-              the_column_1
-              the_column_2
-            }
-          }
-        |]
-      Ae.encode result
-        `shouldBe` rmSpaces
-          [raw|
-        { "data": {
-            "test_entries": [
-              { "theXX0column": "with spaces",
-                "the_column":   "no spaces",
-                "the_column_1": "no spaces 1",
-                "the_column_2": "no spaces 2"
-              }
-            ]
-        }}
-      |]
-
-    it "includes rowid and sorts by rowid" $ do
-      let dbPathSpaces = testRoot </> "rowid_test.db"
-      removeIfExists dbPathSpaces
-      conn <- open dbPathSpaces
-
-      execute_
-        conn
-        [sql|
-          CREATE TABLE IF NOT EXISTS "users" (
-            "email"             TEXT NOT NULL UNIQUE PRIMARY KEY,
-            "number_of_logins"  INTEGER
-          );
-        |]
-      execute_
-        conn
-        [sql|
-          INSERT INTO users (email, number_of_logins)
-          VALUES ('john@example.com', 0), ('eve@example.com', 4);
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|
-          {
-            users {
-              rowid
-            }
-          }
-        |]
-      Ae.encode result
-        `shouldBe` rmSpaces
-          [raw|
-        {
-          "data": {
-            "users": [
-              { "rowid": 1 },
-              { "rowid": 2 }
-            ]
-          }
-        }
-      |]
-
-    it "supports aliases" $ do
-      conn <- open dbPath
-      execute_
-        conn
-        [sql|
-        insert into users (name, email, created_utc)
-        values ('Adrian', 'adrian@example.com', '2021-01-01T00:00Z')
-      |]
-
-      let
-        query =
-          [gql|
-            {
-              userA: users { name }
-              userB: users { email }
-            }
-          |]
-        expected =
-          rmSpaces
-            [raw|
-          {"data":{
-            "userB":[{"email":"adrian@example.com"}],
-            "userA":[{"name":"Adrian"}]
-          }}
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <- graphql schema Nothing mempty query
-      Ae.encode result `shouldBe` expected
-
-    it "supports fragments" $ do
-      conn <- SS.open dbPath
-      execute_
-        conn
-        [sql|
-          insert into users (name, email, created_utc)
-          values ('Adrian', 'adrian@example.com', '2021-01-01T00:00Z')
-        |]
-
-      let
-        query =
-          [gql|
-            {
-              userA: users { ...basicFields }
-              userB: users { ...basicFields }
-            }
-
-            fragment basicFields on users_row {
-              name
-              email
-            }
-          |]
-        expected =
-          rmSpaces
-            [raw|
-              { "data": {
-                "userB":[{"email":"adrian@example.com","name":"Adrian"}],
-                "userA":[{"email":"adrian@example.com","name":"Adrian"}]
-              }}
-            |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <- graphql schema Nothing mempty query
-      Ae.encode result `shouldBe` expected
-
-    it "supports directives" $ do
-      conn <- SS.open dbPath
-      execute_
-        conn
-        [sql|
-          insert into users (name, email, created_utc)
-          values
-          ('John', 'john@example.com', '2021-01-01T00:00Z'),
-          ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
-        |]
-
-      let
-        query =
-          [gql|
-            query DirectiveTest ($withName: Boolean!) {
-              users {
-                name @include(if: $withName)
-              }
-            }
-          |]
-
-        variables :: Object
-        variables =
-          fromMaybe mempty $ Ae.decode "{ \"withName\": true }"
-
-        expected =
-          rmSpaces
-            [raw|
-            { "data": {
-              "users": [
-                { "name": "John" },
-                { "name": "Eve" }
-              ]
-            }}
-          |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <- graphql schema Nothing variables query
-
-      Ae.encode result `shouldBe` expected
-
-    it "supports retrieving records with a filter" $ do
-      conn <- SS.open dbPath
-      execute_
-        conn
-        [sql|
-          insert into users (name, email, created_utc)
-          values
-          ('John', 'john@example.com', '2021-01-01T00:00Z'),
-          ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|
-          {
-            users (filter: {email: {eq: "eve@example.com"}}) {
-              name
-            }
-          }
-        |]
-
-      Ae.encode result
-        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
-
-    it "supports retrieving records with a filter over int and float" $ do
-      conn <- SS.open dbPath
-      execute_
-        conn
-        [sql|
-          insert into users (name, email, created_utc, progress)
-          values
-          ('John', 'john@example.com', '2021-01-01T00:00Z', 0.7),
-          ('Eve', 'eve@example.com', '2019-01-01T00:00Z', 0.4)
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result1 <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|{
-          users (filter: {rowid: {eq: 2}}) {
-            name
-          }
-        }|]
-
-      Ae.encode result1
-        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
-
-      Right result2 <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|{
-          users (filter: {progress: {eq: 0.4}}) {
-            name
-          }
-        }|]
-
-      Ae.encode result2
-        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
-
-    it "supports retrieving records with a filter over boolean and null" $ do
-      let dbPathFilter = testRoot </> "filter_eq_boolean.db"
-      removeIfExists dbPathFilter
-      conn <- open dbPathFilter
-      execute_
-        conn
-        [sql|
-          CREATE TABLE IF NOT EXISTS "users" (
-            "name"              TEXT,
-            "is_admin"          BOOLEAN
-          );
-        |]
-      execute_
-        conn
-        [sql|
-          INSERT INTO USERS (name, is_admin)
-          VALUES
-          ('John', TRUE),
-          ('Eve', FALSE),
-          ('Anna', NULL)
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result1 <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|{
-          users (filter: {is_admin: {eq: false}}) {
-            name
-          }
-        }|]
-
-      Ae.encode result1
-        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
-
-      Right result2 <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|{
-          users (filter: {is_admin: {eq: null}}) {
-            name
-          }
-        }|]
-
-      Ae.encode result2
-        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Anna\"}]}}"
-
-    it "supports retrieving records with like and ilike filter" $ do
-      withRetryConn dbPath $ \conn -> do
-        execute_
-          conn
-          [sql|
-            insert into users (name, email, created_utc)
-            values
-            ('John', 'john@example.com', '2021-01-01T00:00Z'),
-            ('Anna', 'anna@EXAMPLE.COM', '2019-01-01T00:00Z'),
-            ('Eve', 'eve@evil.com', '2019-01-01T00:00Z')
-          |]
-
-        Right tables <- getEnrichedTables conn
-        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-        Right likeResult <-
-          graphql
-            schema
-            Nothing
-            mempty
-            [gql|
-              {
-                users (filter: {email: {like: "%example%"}}) {
-                  name
-                }
-              }
-            |]
-
-        Ae.encode likeResult
-          `shouldBe` "{\"data\":{\"users\":[{\"name\":\"John\"}]}}"
-
-        Right ilikeResult <-
-          graphql
-            schema
-            Nothing
-            mempty
-            [gql|
-              {
-                users (filter: {email: {ilike: "%example%"}}) {
-                  name
-                }
-              }
-            |]
-
-        Ae.encode ilikeResult
-          `shouldBe` "{\"data\":{\"users\":\
-                     \[{\"name\":\"John\"},{\"name\":\"Anna\"}]}}"
-
-    it "supports retrieving records with several filters" $ do
-      withRetryConn dbPath $ \conn -> do
-        execute_
-          conn
-          [sql|
-            insert into users (name, email, created_utc)
-            values
-            ('John', 'john@example.com', '2021-01-01T00:00Z'),
-            ('Anna', 'anna@example.com', '2019-01-01T00:00Z'),
-            ('Eve', 'eve@evil.com', '2019-01-01T00:00Z')
-          |]
-
-        Right tables <- getEnrichedTables conn
-        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-        Right likeResult <-
-          graphql
-            schema
-            Nothing
-            mempty
-            [gql|
-              {
-                users (
-                  filter: {
-                    email: { like: "%example%" },
-                    name: { gt: "B" }
-                  }
-                ) {
-                  name
-                }
-              }
-            |]
-
-        Ae.encode likeResult
-          `shouldBe` "{\"data\":{\"users\":[{\"name\":\"John\"}]}}"
-
-    it "supports mutating records with several filters" $ do
-      withRetryConn dbPath $ \conn -> do
-        execute_
-          conn
-          [sql|
-            insert into users (name, email, created_utc)
-            values
-            ('John', 'john@example.com', '2021-01-01T00:00Z'),
-            ('Anna', 'anna@example.com', '2019-01-01T00:00Z'),
-            ('Eve', 'eve@evil.com', '2019-01-01T00:00Z')
-          |]
-
-        Right tables <- getEnrichedTables conn
-        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-        Right likeResult <-
-          graphql
-            schema
-            Nothing
-            mempty
-            [gql|
-              mutation UpdateEmailAddress {
-                update_users (
-                  filter: {
-                    email: { like: "%example%" }
-                    name: { gt: "B" }
-                  }
-                  set: { email: "john@new.com" }
-                ) {
-                  affected_rows
-                  returning {
-                    email
-                  }
-                }
-              }
-            |]
-
-        Ae.encode likeResult
-          `shouldBe` "{\"data\":{\"update_users\":\
-                     \{\"affected_rows\":1,\
-                     \\"returning\":[{\"email\":\"john@new.com\"}]}}}"
-
-    it "supports retrieving multi-type columns" $ do
-      withTestDbConn "multi-type_columns.db" $ \conn -> do
-        execute_
-          conn
-          [sql|
-            CREATE VIEW "multi_type_column" AS
-            SELECT 1 AS col UNION
-            SELECT 2.2 AS col UNION
-            SELECT 'three' AS col UNION
-            SELECT NULL AS col
-          |]
-
-        Right tables <- getEnrichedTables conn
-        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-        Right result1 <-
-          graphql
-            schema
-            Nothing
-            mempty
-            [gql|{
-          multi_type_column { col }
-        }|]
-
-        let expected =
-              rmSpaces
-                [raw|
-              {
-                "data": {
-                  "multi_type_column": [
-                    { "col": null },
-                    { "col": "1" },
-                    { "col": "2.2" },
-                    { "col": "three" }
-                  ]
-                }
-              }
-            |]
-
-        Ae.encode result1 `shouldBe` expected
-
-    it "supports querying a single entry" $ do
-      conn <- SS.open dbPath
-      execute_
-        conn
-        [sql|
-          insert into users (name, email, created_utc)
-          values
-          ('John', 'john@example.com', '2021-01-01T00:00Z'),
-          ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
-        |]
-
-      Right tables <- getEnrichedTables conn
-      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-      Right result <-
-        graphql
-          schema
-          Nothing
-          mempty
-          [gql|
-          {
-            users_by_pk (email: "eve@example.com") {
-              name
-            }
-          }
-        |]
-
-      pendingWith "Not implemented yet"
-
-      Ae.encode result
-        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
-
-    it "errors out on integer overflows" $ do
-      withTestDbConn "integer-overflows.db" $ \conn -> do
-        execute_
-          conn
-          [sql|
-            CREATE TABLE test (
-              big INTEGER,
-              alsobig INTEGER
-            )
-          |]
-
-        execute_
-          conn
-          [sql|
-            INSERT INTO test(big, alsobig)
-            VALUES (8000000000, 9000000000)
-          |]
-
-        Right tables <- getEnrichedTables conn
-        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
-
-        Right result <-
-          graphql
-            schema
-            Nothing
-            mempty
-            [gql|
-              {
-                test {
-                  big,
-                  alsobig
-                }
-              }
-            |]
-
-        let expected =
-              rmSpaces
-                [raw|
-                  {
-                    "data": null,
-                    "errors": [{
-                      "locations": [{
-                        "column": 3,
-                        "line": 2
-                      }],
-                      "message":
-                        "user error (Multiple errors occurred:\nOn column \"big\": Integer 8000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.\nOn column \"alsobig\": Integer 9000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.\n)",
-                      "path": ["test"]
-                    }]
-                  }
-                |]
-
-        Ae.encode result `shouldBe` expected
-
   describe "Mutations" $ do
     it "supports inserting data" $ do
       let
@@ -2663,6 +1960,7 @@ testSuite = do
 
           Ae.encode result `shouldBe` expected
 
+  describe "Query" Tests.QuerySpec.main
   describe "Introspection" Tests.IntrospectionSpec.main
 
 
diff --git a/tests/Tests/QuerySpec.hs b/tests/Tests/QuerySpec.hs
new file mode 100644
index 0000000..f0294b2
--- /dev/null
+++ b/tests/Tests/QuerySpec.hs
@@ -0,0 +1,838 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+{-# OPTIONS_GHC -Wno-missing-local-signatures #-}
+
+module Tests.QuerySpec (main) where
+
+import Protolude (
+  Either (Right),
+  FilePath,
+  Maybe (Nothing),
+  Monoid (mempty),
+  bracket_,
+  finally,
+  flip,
+  fromMaybe,
+  show,
+  void,
+  ($),
+ )
+
+import Data.Aeson qualified as Ae
+import Database.SQLite.Simple qualified as SS
+import Database.SQLite.Simple.QQ (sql)
+import Language.GraphQL.JSON (graphql)
+import Language.GraphQL.TH (gql)
+import System.FilePath ((</>))
+import Test.Hspec (Spec, before_, describe, it, pendingWith, shouldBe, shouldContain)
+
+import AirGQL.GraphQL (getDerivedSchema)
+import AirGQL.Lib (getEnrichedTables)
+import AirGQL.Raw (raw)
+import AirGQL.Types.SchemaConf (defaultSchemaConf)
+import AirGQL.Utils (removeIfExists, withRetryConn)
+import Data.Text qualified as T
+import Tests.Utils (dbPath, fixtureDbId, rmSpaces, shouldSaveDbs, testRoot, unorderedShouldBe, withTestDbConn)
+
+
+main :: Spec
+main = void $ do
+  it "supports retrieving data" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc)
+        values ('Adrian', 'adrian@example.com', '2021-01-01T00:00Z')
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <-
+      getDerivedSchema
+        defaultSchemaConf
+        conn
+        fixtureDbId
+        tables
+
+    Right result <- graphql schema Nothing mempty "{ users { name } }"
+    Ae.encode result
+      `shouldBe` [gql|
+        {"data":{"users":[{"name":"Adrian"}]}}
+      |]
+
+  it "supports retrieving data from tables with special names" $ do
+    let testDbPath = testRoot </> "special_table_name.db"
+    removeIfExists testDbPath
+    let dbPathNorm = if shouldSaveDbs then testDbPath else ":memory:"
+
+    withRetryConn dbPathNorm $ \conn -> do
+      SS.execute_
+        conn
+        [sql|
+          CREATE TABLE "name with-special$chars" (
+            name TEXT
+          )
+        |]
+      SS.execute_
+        conn
+        [sql|
+          INSERT INTO "name with-special$chars" (name)
+          VALUES ('John')
+        |]
+
+      Right tables <- getEnrichedTables conn
+      schema <-
+        getDerivedSchema
+          defaultSchemaConf
+          conn
+          fixtureDbId
+          tables
+
+      Right result <-
+        graphql
+          schema
+          Nothing
+          mempty
+          "{ nameXX0withXXDspecialXX4chars { name } }"
+      Ae.encode result
+        `shouldBe` [gql|
+          {"data":{"nameXX0withXXDspecialXX4chars":[{"name":"John"}]}}
+        |]
+
+  describe "column names with special characters" $ do
+    let
+      dbPathSpaces :: FilePath = testRoot </> "spaces-test.db"
+      setupDatabaseSpaces = do
+        removeIfExists dbPathSpaces
+        conn <- SS.open dbPathSpaces
+        SS.execute_
+          conn
+          [sql|
+            CREATE TABLE IF NOT EXISTS test_entries (
+              id INTEGER PRIMARY KEY,
+              `column with spaces` TEXT
+            )
+          |]
+        SS.execute_
+          conn
+          [sql|
+            INSERT INTO test_entries (id, `column with spaces`)
+            VALUES (0, 'Just a test')
+          |]
+
+    before_ setupDatabaseSpaces $ it "supports column names with spaces" $ do
+      conn <- SS.open dbPathSpaces
+      Right tables <- getEnrichedTables conn
+      schema <-
+        getDerivedSchema
+          defaultSchemaConf
+          conn
+          (T.pack dbPathSpaces)
+          tables
+      Right result <-
+        graphql
+          schema
+          Nothing
+          mempty
+          "{ test_entries { columnXX0withXX0spaces } }"
+      Ae.encode result
+        `shouldBe` rmSpaces
+          [raw|
+            { "data": {
+                "test_entries": [
+                  { "columnXX0withXX0spaces": "Just a test" }
+                ]
+            }}
+          |]
+
+    before_ setupDatabaseSpaces $
+      it "generates introspection schema for column names with spaces" $ do
+        conn <- SS.open dbPathSpaces
+        Right tables <- getEnrichedTables conn
+        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+        Right result <-
+          graphql
+            schema
+            Nothing
+            mempty
+            "{ __schema{ types { name fields { name } } } } }"
+        show (Ae.encode result) `shouldContain` "columnXX0withXX0spaces"
+
+  it "avoids column name remapping collisions" $ do
+    let dbPathSpaces = testRoot </> "spaces-collision-test.db"
+    removeIfExists dbPathSpaces
+    conn <- SS.open dbPathSpaces
+    SS.execute_
+      conn
+      [sql|
+        CREATE TABLE IF NOT EXISTS test_entries (
+          id INTEGER PRIMARY KEY,
+          `the column` TEXT,
+          `the_column` TEXT,
+          `the_column_1` TEXT,
+          `the_column_2` TEXT
+        )
+      |]
+    SS.execute_
+      conn
+      [sql|
+        INSERT INTO test_entries
+          (id, `the column`, the_column, the_column_1, the_column_2)
+        VALUES
+          (0, 'with spaces', 'no spaces', 'no spaces 1', 'no spaces 2')
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|
+          { test_entries {
+              theXX0column
+              the_column
+              the_column_1
+              the_column_2
+            }
+          }
+        |]
+    Ae.encode result
+      `shouldBe` rmSpaces
+        [raw|
+        { "data": {
+            "test_entries": [
+              { "theXX0column": "with spaces",
+                "the_column":   "no spaces",
+                "the_column_1": "no spaces 1",
+                "the_column_2": "no spaces 2"
+              }
+            ]
+        }}
+      |]
+
+  it "includes rowid and sorts by rowid" $ do
+    let dbPathSpaces = testRoot </> "rowid_test.db"
+    removeIfExists dbPathSpaces
+    conn <- SS.open dbPathSpaces
+
+    SS.execute_
+      conn
+      [sql|
+        CREATE TABLE IF NOT EXISTS "users" (
+          "email"             TEXT NOT NULL UNIQUE PRIMARY KEY,
+          "number_of_logins"  INTEGER
+        );
+      |]
+    SS.execute_
+      conn
+      [sql|
+        INSERT INTO users (email, number_of_logins)
+        VALUES ('john@example.com', 0), ('eve@example.com', 4);
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|
+          {
+            users {
+              rowid
+            }
+          }
+        |]
+    Ae.encode result
+      `shouldBe` rmSpaces
+        [raw|
+        {
+          "data": {
+            "users": [
+              { "rowid": 1 },
+              { "rowid": 2 }
+            ]
+          }
+        }
+      |]
+
+  it "supports aliases" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc)
+        values ('Adrian', 'adrian@example.com', '2021-01-01T00:00Z')
+      |]
+
+    let
+      query =
+        [gql|
+            {
+              userA: users { name }
+              userB: users { email }
+            }
+          |]
+      expected =
+        rmSpaces
+          [raw|
+          {"data":{
+            "userB":[{"email":"adrian@example.com"}],
+            "userA":[{"name":"Adrian"}]
+          }}
+        |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <- graphql schema Nothing mempty query
+    Ae.encode result `shouldBe` expected
+
+  it "supports fragments" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc)
+        values ('Adrian', 'adrian@example.com', '2021-01-01T00:00Z')
+      |]
+
+    let
+      query =
+        [gql|
+            {
+              userA: users { ...basicFields }
+              userB: users { ...basicFields }
+            }
+
+            fragment basicFields on users_row {
+              name
+              email
+            }
+          |]
+      expected =
+        rmSpaces
+          [raw|
+              { "data": {
+                "userB":[{"email":"adrian@example.com","name":"Adrian"}],
+                "userA":[{"email":"adrian@example.com","name":"Adrian"}]
+              }}
+            |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <- graphql schema Nothing mempty query
+    Ae.encode result `shouldBe` expected
+
+  it "supports directives" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc)
+        values
+        ('John', 'john@example.com', '2021-01-01T00:00Z'),
+        ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
+      |]
+
+    let
+      query =
+        [gql|
+            query DirectiveTest ($withName: Boolean!) {
+              users {
+                name @include(if: $withName)
+              }
+            }
+          |]
+
+      variables :: Ae.Object
+      variables =
+        fromMaybe mempty $ Ae.decode "{ \"withName\": true }"
+
+      expected =
+        rmSpaces
+          [raw|
+            { "data": {
+              "users": [
+                { "name": "John" },
+                { "name": "Eve" }
+              ]
+            }}
+          |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <- graphql schema Nothing variables query
+
+    Ae.encode result `shouldBe` expected
+
+  it "supports retrieving records with a filter" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc)
+        values
+        ('John', 'john@example.com', '2021-01-01T00:00Z'),
+        ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|
+          {
+            users (filter: {email: {eq: "eve@example.com"}}) {
+              name
+            }
+          }
+        |]
+
+    Ae.encode result
+      `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
+
+  it "supports retrieving records with a filter over int and float" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc, progress)
+        values
+        ('John', 'john@example.com', '2021-01-01T00:00Z', 0.7),
+        ('Eve', 'eve@example.com', '2019-01-01T00:00Z', 0.4)
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result1 <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|{
+          users (filter: {rowid: {eq: 2}}) {
+            name
+          }
+        }|]
+
+    Ae.encode result1
+      `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
+
+    Right result2 <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|{
+          users (filter: {progress: {eq: 0.4}}) {
+            name
+          }
+        }|]
+
+    Ae.encode result2
+      `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
+
+  it "supports retrieving records with a filter over boolean and null" $ do
+    let dbPathFilter = testRoot </> "filter_eq_boolean.db"
+    removeIfExists dbPathFilter
+    conn <- SS.open dbPathFilter
+    SS.execute_
+      conn
+      [sql|
+        CREATE TABLE IF NOT EXISTS "users" (
+          "name"              TEXT,
+          "is_admin"          BOOLEAN
+        );
+      |]
+    SS.execute_
+      conn
+      [sql|
+        INSERT INTO USERS (name, is_admin)
+        VALUES
+        ('John', TRUE),
+        ('Eve', FALSE),
+        ('Anna', NULL)
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result1 <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|{
+          users (filter: {is_admin: {eq: false}}) {
+            name
+          }
+        }|]
+
+    Ae.encode result1
+      `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Eve\"}]}}"
+
+    Right result2 <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|{
+          users (filter: {is_admin: {eq: null}}) {
+            name
+          }
+        }|]
+
+    Ae.encode result2
+      `shouldBe` "{\"data\":{\"users\":[{\"name\":\"Anna\"}]}}"
+
+  it "supports retrieving records with like and ilike filter" $ do
+    withRetryConn dbPath $ \conn -> do
+      SS.execute_
+        conn
+        [sql|
+          insert into users (name, email, created_utc)
+          values
+          ('John', 'john@example.com', '2021-01-01T00:00Z'),
+          ('Anna', 'anna@EXAMPLE.COM', '2019-01-01T00:00Z'),
+          ('Eve', 'eve@evil.com', '2019-01-01T00:00Z')
+        |]
+
+      Right tables <- getEnrichedTables conn
+      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+      Right likeResult <-
+        graphql
+          schema
+          Nothing
+          mempty
+          [gql|
+              {
+                users (filter: {email: {like: "%example%"}}) {
+                  name
+                }
+              }
+            |]
+
+      Ae.encode likeResult
+        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"John\"}]}}"
+
+      Right ilikeResult <-
+        graphql
+          schema
+          Nothing
+          mempty
+          [gql|
+            {
+              users (filter: {email: {ilike: "%example%"}}) {
+                name
+              }
+            }
+          |]
+
+      Ae.encode ilikeResult
+        `shouldBe` "{\"data\":{\"users\":\
+                   \[{\"name\":\"John\"},{\"name\":\"Anna\"}]}}"
+
+  it "supports retrieving records with several filters" $ do
+    withRetryConn dbPath $ \conn -> do
+      SS.execute_
+        conn
+        [sql|
+          insert into users (name, email, created_utc)
+          values
+          ('John', 'john@example.com', '2021-01-01T00:00Z'),
+          ('Anna', 'anna@example.com', '2019-01-01T00:00Z'),
+          ('Eve', 'eve@evil.com', '2019-01-01T00:00Z')
+        |]
+
+      Right tables <- getEnrichedTables conn
+      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+      Right likeResult <-
+        graphql
+          schema
+          Nothing
+          mempty
+          [gql|
+              {
+                users (
+                  filter: {
+                    email: { like: "%example%" },
+                    name: { gt: "B" }
+                  }
+                ) {
+                  name
+                }
+              }
+            |]
+
+      Ae.encode likeResult
+        `shouldBe` "{\"data\":{\"users\":[{\"name\":\"John\"}]}}"
+
+  it "supports mutating records with several filters" $ do
+    withRetryConn dbPath $ \conn -> do
+      SS.execute_
+        conn
+        [sql|
+          insert into users (name, email, created_utc)
+          values
+          ('John', 'john@example.com', '2021-01-01T00:00Z'),
+          ('Anna', 'anna@example.com', '2019-01-01T00:00Z'),
+          ('Eve', 'eve@evil.com', '2019-01-01T00:00Z')
+        |]
+
+      Right tables <- getEnrichedTables conn
+      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+      Right likeResult <-
+        graphql
+          schema
+          Nothing
+          mempty
+          [gql|
+              mutation UpdateEmailAddress {
+                update_users (
+                  filter: {
+                    email: { like: "%example%" }
+                    name: { gt: "B" }
+                  }
+                  set: { email: "john@new.com" }
+                ) {
+                  affected_rows
+                  returning {
+                    email
+                  }
+                }
+              }
+            |]
+
+      Ae.encode likeResult
+        `shouldBe` "{\"data\":{\"update_users\":\
+                   \{\"affected_rows\":1,\
+                   \\"returning\":[{\"email\":\"john@new.com\"}]}}}"
+
+  it "supports retrieving multi-type columns" $ do
+    withTestDbConn "multi-type_columns.db" $ \conn -> do
+      SS.execute_
+        conn
+        [sql|
+          CREATE VIEW "multi_type_column" AS
+          SELECT 1 AS col UNION
+          SELECT 2.2 AS col UNION
+          SELECT 'three' AS col UNION
+          SELECT NULL AS col
+        |]
+
+      Right tables <- getEnrichedTables conn
+      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+      Right result1 <-
+        graphql
+          schema
+          Nothing
+          mempty
+          [gql|{
+          multi_type_column { col }
+        }|]
+
+      let expected =
+            rmSpaces
+              [raw|
+              {
+                "data": {
+                  "multi_type_column": [
+                    { "col": null },
+                    { "col": "1" },
+                    { "col": "2.2" },
+                    { "col": "three" }
+                  ]
+                }
+              }
+            |]
+
+      Ae.encode result1 `shouldBe` expected
+
+  it "supports querying a single entry by inferred pk" $ do
+    conn <- SS.open dbPath
+    SS.execute_
+      conn
+      [sql|
+        insert into users (name, email, created_utc)
+        values
+        ('John', 'john@example.com', '2021-01-01T00:00Z'),
+        ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
+      |]
+
+    Right tables <- getEnrichedTables conn
+    schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+    Right result <-
+      graphql
+        schema
+        Nothing
+        mempty
+        [gql|
+          {
+            users_by_pk (email: "eve@example.com") {
+              name
+            }
+          }
+        |]
+
+    let expected =
+          [raw|
+            {
+              "data": {
+                "users_by_pk": {
+                  "name":"Eve"
+                }
+              }
+            }
+          |]
+
+    result `unorderedShouldBe` expected
+
+  it "supports querying a single entry by composite pk" $ do
+    conn <- SS.open dbPath
+    -- The changes we make here influence other tests. At some point we
+    -- should probably come up with a better system, but for now I'm just
+    -- rollbacking so the creation of the table does not influence
+    -- introspection tests and whatnot.
+    bracket_
+      (SS.execute_ conn [sql| begin transaction |])
+      (SS.execute_ conn [sql| rollback |])
+      $ do
+        SS.execute_
+          conn
+          [sql|
+            create table users_liked_songs (
+              email TEXT REFERENCES users,
+              song_id INT REFERENCES songs,
+              rating INT NOT NULL,
+              PRIMARY KEY (email, song_id)
+            )
+          |]
+
+        SS.execute_
+          conn
+          [sql|
+            insert into users (name, email, created_utc)
+            values
+            ('John', 'john@example.com', '2021-01-01T00:00Z'),
+            ('Eve', 'eve@example.com', '2019-01-01T00:00Z')
+          |]
+
+        SS.execute_
+          conn
+          [sql|
+            insert into songs (name, duration_seconds)
+            values
+              ('Never Gonna Give You Up', 215),
+              ('Beethoven — Symphony No. 9', 3600);
+          |]
+
+        SS.execute_
+          conn
+          [sql|
+            insert into users_liked_songs (email, song_id, rating)
+            values
+              ('eve@example.com', 1, 8),
+              ('john@example.com', 2, 9),
+              ('eve@example.com', 2, 7);
+          |]
+
+        Right tables <- getEnrichedTables conn
+        schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+        Right result <-
+          graphql
+            schema
+            Nothing
+            mempty
+            [gql|
+              {
+                users_liked_songs_by_pk (email: "eve@example.com", song_id: 2) {
+                  rating
+                }
+              }
+            |]
+
+        let expected =
+              [raw|
+                {
+                  "data": {
+                    "users_liked_songs_by_pk": {
+                      "rating": 7
+                    }
+                  }
+                }
+              |]
+
+        result `unorderedShouldBe` expected
+
+  it "errors out on integer overflows" $ do
+    withTestDbConn "integer-overflows.db" $ \conn -> do
+      SS.execute_
+        conn
+        [sql|
+            CREATE TABLE test (
+              big INTEGER,
+              alsobig INTEGER
+            )
+          |]
+
+      SS.execute_
+        conn
+        [sql|
+          INSERT INTO test(big, alsobig)
+          VALUES (8000000000, 9000000000)
+        |]
+
+      Right tables <- getEnrichedTables conn
+      schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
+
+      Right result <-
+        graphql
+          schema
+          Nothing
+          mempty
+          [gql|
+              {
+                test {
+                  big,
+                  alsobig
+                }
+              }
+            |]
+
+      let expected =
+            rmSpaces
+              [raw|
+                  {
+                    "data": null,
+                    "errors": [{
+                      "locations": [{
+                        "column": 3,
+                        "line": 2
+                      }],
+                      "message":
+                        "user error (Multiple errors occurred:\nOn column \"big\": Integer 8000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.\nOn column \"alsobig\": Integer 9000000000 would overflow. This happens because SQLite uses 64-bit ints, but GraphQL uses 32-bit ints. Use a Number (64-bit float) or Text column instead.\n)",
+                      "path": ["test"]
+                    }]
+                  }
+                |]
+
+      Ae.encode result `shouldBe` expected