Kirill Simonov avatar Kirill Simonov committed 4c8607c

Refactored the test suite; other minor fixes and updates.

Split the regression tests into separate files: regression schema,
tutorial examples, functions and operators, SQL generation,
formatting output. Use the same tests for both PGSQL and SQLite.
Added ability to conditionally skip tests using `ifdef`, `ifndef`
directives.
Fixed the `!==` operator for SQLite.
Fixed collapsing `AND` and `OR` operators.

Comments (0)

Files changed (23)

 # This makefile provides various build, installation and testing tasks.
 
 .PHONY: default build install develop doc dist windist pypi \
-	test cleanup train train-ctl train-sqlite train-pgsql purge-test \
+	test cleanup train train-routine train-sqlite train-pgsql purge-test \
 	demo-htraf demo-ssi
 
 
 	@echo "  test: to run HTSQL regression tests"
 	@echo "  cleanup: to drop users and databases deployed by regression tests"
 	@echo "  train: to run all HTSQL tests in the train mode"
-	@echo "  train-ctl: to run tests for htsql-ctl routines in the train mode"
+	@echo "  train-routine: to run tests for htsql-ctl tool in the train mode"
 	@echo "  train-sqlite: to run SQLite-specific tests in the train mode"
 	@echo "  train-pgsql: to run PostgreSQL-specific tests in the train mode"
 	@echo "  purge-test: to purge state test output data"
 train:
 	htsql-ctl regress -i test/regress.yaml --train
 
-# Run regression tests for htsql-ctl routines in the train mode.
-train-ctl:
-	htsql-ctl regress -i test/regress.yaml --train ctl
+# Run regression tests for htsql-ctl tool in the train mode.
+train-routine:
+	htsql-ctl regress -i test/regress.yaml --train routine
 
 # Run SQLite-specific regression tests in the train mode.
 train-sqlite:

src/htsql/ctl/regress.py

         return None
 
 
-class RunAndCompareTestCase(TestCase):
+class SkipTestCase(TestCase):
+    """
+    Implements a skippable test case.
+
+    This is an abstract mixin class; subclasses should call :meth:`skipped`
+    to check if the test case is enabled or not.
+    """
+
+    class Input(TestData):
+        fields = [
+                Field('skip', BoolVal(), False,
+                      hint="""do not run the test"""),
+                Field('ifdef', SeqVal(StrVal()), None,
+                      hint="""run only if a given toggle is active"""),
+                Field('ifndef', SeqVal(StrVal()), None,
+                      hint="""run only if a given toggle is inactive"""),
+        ]
+
+    def skipped(self):
+        """
+        Checks if the test is disabled.
+        """
+        # Verify if the test is unconditionally disabled.
+        if self.input.skip:
+            return True
+        # If a positive guard is set, check that at least one of the required
+        # toggles is active.
+        if self.input.ifdef is not None:
+            if not (self.state.toggles & set(self.input.ifdef)):
+                return True
+        # If a negative guard is set, check that none of the suppressed
+        # toggles is active.
+        if self.input.ifndef is not None:
+            if self.state.toggles & set(self.input.ifndef):
+                return True
+        # The test is not skipped.
+        return False
+
+
+class DefineTestCase(SkipTestCase):
+    """
+    Activates a named toggle.
+    """
+
+    name = "define"
+    hint = """activates a toggle"""
+    help = """
+    This test case activates a toggle variable.  A toggle allows one
+    to conditionally enable or disable some test cases using `ifdef`
+    and `ifndef` directives.
+    """
+
+    class Input(TestData):
+        fields = [
+                Field('define', SeqVal(StrVal()),
+                      hint="""activate the given toggles"""),
+        ] + SkipTestCase.Input.fields
+
+    def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
+        # Activates the toggles.
+        for toggle in self.input.define:
+            self.state.toggles.add(toggle)
+
+
+class RunAndCompareTestCase(SkipTestCase):
     """
     Implements common methods for a broad category of test cases.
 
         raise NotImplementedError()
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
+
         # Display the header.
         self.out_header()
 
         return self.passed()
 
     def train(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return self.output
+
         # Display the header.
         self.out_header()
 
         return self.output
 
 
-class SkipTestCase(TestCase):
-    """
-    Disables an existing test case.
-    """
-
-    name = "skip"
-    hint = """disable a test case"""
-    help = """
-    Add a `skip` field to any input test record to disable the test.
-    """
-
-    class Input(TestData):
-        fields = [
-                Field('skip', AnyVal(),
-                      hint="""indicates that the test is disabled"""),
-                AnyField(),
-        ]
-
-    def verify(self):
-        return 
-
-
-class AppTestCase(TestCase):
+class AppTestCase(SkipTestCase):
     """
     Configures the HTSQL application.
     """
         fields = [
                 Field('db', DBVal(),
                       hint="""the connection URI"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def out_header(self):
         # Overriden to avoid printing the password to the database.
             self.out("(%s)" % self.input.location, indent=2)
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
+
         # Display the header.
         self.out_header()
 
         return self.passed()
 
 
-class IncludeTestCase(TestCase):
+class IncludeTestCase(SkipTestCase):
     """
     Loads input test data from a file.
     """
         fields = [
                 Field('include', StrVal(),
                       hint="""file containing input test data"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     class Output(TestData):
         fields = [
                 Field('include', StrVal(),
                       hint="""file containing input test data"""),
-                Field('output', SeqVal(ClassVal(TestData)),
+                Field('output', ClassVal(TestData),
                       hint="""the corresponding output test data"""),
         ]
 
         return self.case.get_suites()
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
         # Run the included test.
         self.case.verify()
 
     def train(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return self.output
+
         # Run the included test; get the output.
         new_output = self.case.train()
 
         return output
 
 
-class SuiteTestCase(TestCase):
+class SuiteTestCase(SkipTestCase):
     """
     Implements a container of test cases.
     """
                       hint="""file to save the output of the tests"""),
                 Field('tests', SeqVal(ClassVal(TestData)),
                       hint="""a list of test inputs"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
         def init_attributes(self):
             # When `id` is not specified, generate it from the title.
     def skipped(self):
         # Check if the suite should not be executed.
 
+        # Check if the test case was explicitly disabled.
+        if super(SuiteTestCase, self).skipped():
+            return True
+
         # The suite is skipped when:
         # - the user specified an explicit list of the suites to run;
         # - and the suite is not one of them;
 
         # Push the current state to the cases state.
         self.state.push(self.cases_state)
-        # Check if the user specified the suites to run and
-        # this one is not among them.
+        # Check if the suite is disabled or if the user specified
+        # the suites to run and this one is not among them.
         if self.skipped():
             return
         # Display the headers.
 
         # Push the current state to the cases state.
         self.state.push(self.cases_state)
-        # Check if the user specified the suites to run and
-        # this one is not among them.
+        # Check if the suite is disabled or if the user specified
+        # the suites to run and this one is not among them.
         if self.skipped():
             return self.output
         # A dictionary containing the output (or `None`) generated by test
                       hint="""the HTTP status code to expect"""),
                 Field('ignore', BoolVal(), False,
                       hint="""ignore the response body"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
         def init_attributes(self):
             # Check that `content-type` and `content-body` are set only if
                     raise ValueError("no expected content-body parameter"
                                      " for a POST request")
 
-
     class Output(TestData):
         fields = [
                 Field('uri', StrVal(),
                       hint="""the exit code to expect"""),
                 Field('ignore', BoolVal(), False,
                       hint="""ignore the exit code and the standard output"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     class Output(TestData):
         fields = [
         return output
 
 
-class StartCtlTestCase(TestCase):
+class StartCtlTestCase(SkipTestCase):
     """
     Starts a long-running routine.
     """
                       hint="""the content of the standard output"""),
                 Field('sleep', UFloatVal(), 0,
                       hint="""sleep for the specified number of seconds"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def verify(self):
         # Execute the test.
 
+        # Check if the test case is skipped.
+        if self.skipped():
+            return
+
         # Check if an application with the same command-line parameters
         # has already been started.
         key = tuple(self.input.start_ctl)
                       hint="""a list of command-line parameters"""),
                 Field('ignore', BoolVal(), False,
                       hint="""ignore the exit code and the standard output"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     class Output(TestData):
         fields = [
                       hint="""the name of an exception to expect"""),
                 Field('ignore', BoolVal(), False,
                       hint="""ignore the standard output"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     class Output(TestData):
         fields = [
         return new_output
 
 
-class SQLTestCase(TestCase):
+class SQLTestCase(SkipTestCase):
     """
     Executes a SQL query.
     """
-    # TODO: Can't implement until the SQL splitter is done.
 
     name = "sql"
     hint = """execute a SQL statement"""
                       hint="""use the auto-commit mode"""),
                 Field('ignore', BoolVal(), False,
                       hint="""ignore any errors"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def out_header(self):
         # Print:
             self.out("(%s)" % self.input.location, indent=2)
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
+
         # Display the header.
         self.out_header()
 
                       hint="""use the auto-commit mode"""),
                 Field('ignore', BoolVal(), False,
                       hint="""ignore any errors"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def out_header(self):
         # Print:
         return sql
 
 
-class WriteToFileTestCase(TestCase):
+class WriteToFileTestCase(SkipTestCase):
     """
     Writes some data to a file.
     """
                       hint="""the file name"""),
                 Field('data', StrVal(),
                       hint="""the data to write"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
         # Display the header.
         self.out_header()
         # Write the data to the file.
         fields = [
                 Field('read', StrVal(),
                       hint="""the file name"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     class Output(TestData):
         fields = [
         fields = [
                 Field('remove', SeqVal(StrVal()),
                       hint="""a list of files to remove"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
         # Display the header.
         self.out_header()
         # Remove the given files.
                 os.unlink(path)
 
 
-class MakeDirTestCase(TestCase):
+class MakeDirTestCase(SkipTestCase):
     """
     Creates a directory.
     """
         fields = [
                 Field('mkdir', StrVal(),
                       hint="""the directory name"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
         # Display the header.
         self.out_header()
         # Create the directory if it does not already exist.
             os.makedirs(self.input.mkdir)
 
 
-class RemoveDirTestCase(TestCase):
+class RemoveDirTestCase(SkipTestCase):
     """
     Removes a directory.
     """
         fields = [
                 Field('rmdir', StrVal(),
                       hint="""the directory name"""),
-        ]
+        ] + SkipTestCase.Input.fields
 
     def verify(self):
+        # Check if the test is skipped.
+        if self.skipped():
+            return
         # Display the header.
         self.out_header()
         # Remove the directory with all its content (DANGEROUS!).
         A mapping from command-line parameters to :class:`Fork`
         instances; contains long-running applications.
 
+    `toggles`
+        A set of active named toggles.
+
     `with_all_suites`
         Indicates that the current suite or one of its ancestors
         was explicitly selected by the user.
         Indicates whether the user asked to halt the testing.
     """
 
-    def __init__(self, app=None, forks=None, with_all_suites=False,
-                 passed=0, failed=0, updated=0, is_exiting=False):
+    def __init__(self, app=None, forks=None, toggles=None,
+                 with_all_suites=False, passed=0, failed=0, updated=0,
+                 is_exiting=False):
         self.app = app
         self.forks = forks or {}
+        self.toggles = toggles or set()
         self.with_all_suites = with_all_suites
         self.passed = passed
         self.failed = failed
             the suite test cases.
         """
         other.app = self.app
-        other.forks = self.forks
+        other.forks = self.forks.copy()
+        other.toggles = self.toggles.copy()
         other.with_all_suites = self.with_all_suites
         other.passed = self.passed
         other.failed = self.failed
 
     # List of supported types of test cases.
     cases = [
-            SkipTestCase,
             AppTestCase,
+            DefineTestCase,
             IncludeTestCase,
             SuiteTestCase,
             QueryTestCase,

src/htsql/tr/dump.py

         # Dump:
         #   <frame> AS <alias>
         alias = self.state.frame_alias_by_tag[self.clause.frame.tag]
+        self.state.push_hook(with_aliases=True)
         self.format("{frame} AS {alias:name}",
                     frame=self.clause.frame, alias=alias)
+        self.state.pop_hook()
 
 
 class DumpAnchor(Dump):

src/htsql/tr/fn/bind.py

             return False
         if issubclass(component, other):
             return True
-        if issubclass(component.input_signature, other.input_signature):
+        if (issubclass(component.input_signature, other.input_signature)
+            and component.input_signature is not other.input_signature):
             return True
         return False
 

src/htsql/tr/reduce.py

                 for nop in op.ops:
                     if nop in duplicates:
                         continue
-                    ops.extend(nop)
+                    ops.append(nop)
                     duplicates.add(nop)
                 continue
             ops.append(op)
                 for nop in op.ops:
                     if nop in duplicates:
                         continue
-                    ops.extend(nop)
+                    ops.append(nop)
                     duplicates.add(nop)
                 continue
             ops.append(op)

src/htsql_sqlite/export.py

 import htsql_sqlite.connect
 import htsql_sqlite.split_sql
 import htsql_sqlite.introspect
+import htsql_sqlite.tr.bind
 import htsql_sqlite.tr.dump
 
 

src/htsql_sqlite/tr/bind.py

+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_sqlite.tr.bind`
+===========================
+
+This module adapts HTSQL binder for SQLite.
+"""
+
+
+from htsql.domain import FloatDomain
+from htsql.tr.fn.bind import CorrelateDecimalAvg
+
+
+class SQLiteCorrelateDecimalAvg(CorrelateDecimalAvg):
+
+    domains = [FloatDomain()]
+    codomain = FloatDomain()
+
+

src/htsql_sqlite/tr/dump.py

 class SQLiteDumpIsTotallyEqual(DumpIsTotallyEqual):
 
     def __call__(self):
-        self.format("(CASE WHEN ({lop} {polarity:switch{=|<>}} {rop}) OR"
-                    " {polarity:not}({lop} IS NULL AND {rop} IS NULL)"
+        self.format("({polarity:not}CASE WHEN ({lop} = {rop}) OR"
+                    " ({lop} IS NULL AND {rop} IS NULL)"
                     " THEN 1 ELSE 0 END)",
                     self.arguments, self.signature)
 

test/input/ctl.yaml

-#
-# Copyright (c) 2006-2010, Prometheus Research, LLC
-# Authors: Clark C. Evans <cce@clarkevans.com>,
-#          Kirill Simonov <xi@resolvent.net>
-#
-
-title: Regression tests for htsql-ctl routines
-id: ctl
-output: test/output/ctl.yaml
-tests:
-- ctl: []
-- ctl: [help]
-- ctl: [help, help]
-# FIXME: need comprehensive tests for each routine.
-

test/input/format.yaml

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+title: Formatting Output Data
+id: format
+tests:
+
+- title: Supported Output Formats
+  tests:
+  - uri: /school/:json
+  - uri: /school/:csv
+  - uri: /school/:txt
+  - uri: /school/:html
+  - uri: /school/:unknown
+    expect: 400
+
+- title: Format Selection by `Accept`
+  tests:
+  - uri: /school
+    headers:
+      Accept: application/json
+  - uri: /school
+    headers:
+      Accept: text/csv
+  - uri: /school
+    headers:
+      Accept: text/plain
+  - uri: /school
+    headers:
+      Accept: text/html
+
+- title: The `AS` Decorator
+  tests:
+    - uri: /(school as 'List of Schools')
+            {name as Name, count(department) as '# of Departments'}
+            /:json
+    - uri: /(school as 'List of Schools')
+            {name as Name, count(department) as '# of Departments'}
+            /:csv
+    - uri: /(school as 'List of Schools')
+            {name as Name, count(department) as '# of Departments'}
+            /:txt
+    - uri: /(school as 'List of Schools')
+            {name as Name, count(department) as '# of Departments'}
+            /:html
+

test/input/library.yaml

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+title: Standard Data Types, Functions, and Operations
+id: library
+tests:
+
+# FIXME: update and refurbish!
+
+- title: Literals
+  tests:
+  # String literals.
+  - uri: /{'','HTSQL','O''Reilly','λόγος',
+           '%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F',
+           '%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'}
+  # Integer literals.
+  - uri: /{0,1,100,65536}
+  # Invalid integer literal (must be in range from -2**63 to 2**63-1).
+  - uri: /{115792089237316195423570985008687907853269984665640564039457584007913129639936}
+    expect: 400
+  # Decimal literals.
+  - uri: /{0.0,1.0,3.14,0.00000000001,1234567890.0987654321}
+  # Decimal values are of arbitrary size.
+  - uri: /{10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1}
+  # Float literals.
+  - uri: /{0e0,1e1,0.31415926535897931e1,2718281828459045e-16}
+  # Non-nuleric float values.
+  - uri: /{1e1024,-1e1024,1e1024/1e1024}
+
+- title: Scalar functions
+  tests:
+  - title: Boolean constants and logical operators
+    tests:
+    # Boolean constants.
+    - uri: /{true(),false()}
+    # Boolean->Boolean cast (identity).
+    - uri: /{boolean(null()),boolean(true()),boolean(false())}
+    # Untyped boolean values.
+    - uri: /{boolean('true'),boolean('false')}
+    # Invalid untyped boolean values.
+    - uri: /{boolean('')}
+      expect: 400
+    - uri: /{boolean('X')}
+      expect: 400
+    # Number->Boolean cast (null => false, otherwise => true).
+    - uri: /{boolean(integer(null())),boolean(0),boolean(1),
+             boolean(0.0),boolean(1.0),boolean(0e0),boolean(1e0)}
+    # String->Boolean cast (null, '' => false, otherwise => true).
+    - uri: /{boolean(string(null())),boolean(string('')),boolean(string('X')),
+             boolean(string('true')),boolean(string('false'))}
+           # Note: boolean(string('false')) => true
+    # The AND operator.
+    - uri: /{false()&false(),false()&true(),true()&false(),true()&true(),
+             null()&false(),null()&true(),null()&null()}
+    # Auto-cast of arguments (false,true).
+    - uri: /{string('')&string('X'),0&1}
+    # Auto-cast of NULL values (null,false,false).
+    - uri: /{null()&null(),integer(null())&null(),integer(null())&integer(null())}
+    # The OR operator.
+    - uri: /{false()|false(),false()|true(),true()|false(),true()|true(),
+             null()|false(),null()|true(),null()|null()}
+    # Auto-cast of arguments (true,true,false).
+    - uri: /{string('')|string('X'),0|1,integer(null())|string(null())}
+    # The NOT operator.
+    - uri: /{!true(),!false(),!null()}
+    # Auto-cast of arguments (true,false,false,true).
+    - uri: /{!string(''),!string('X'),!1,!integer(null())}
+
+  - title: Comparison functions and operators
+    tests:
+    # Is NULL function (null => true, otherwise => false).
+    - uri: /{is_null(null()),is_null(true()),is_null(''),is_null(0)}
+    # Null If method (`this` is equal to one of the arguments => null, otherwise => `this`).
+    - uri: /{'Y':null_if('X'),'Y':null_if('Y'),
+             (5):null_if(1,2,3),(5):null_if(1,2,3,4,5,6,7,8,9,10),
+             null():null_if(null()),null():null_if('X')}
+      skip: true
+    - uri: /{'Y':null_if('X'),'Y':null_if('Y'),
+             null():null_if(null()),null():null_if('X')}
+    # If Null method (`this` is null => the first non-null argument, otherwise => `this`).
+    - uri: /{'X':if_null('Y'),null():if_null('X'),null():if_null(null()),
+             null():if_null(null(),null(),null()),
+             null():if_null(null(),null(),0),
+             null():if_null(0,1,2,3,null())}
+      skip: true
+    - uri: /{'X':if_null('Y'),null():if_null('X'),null():if_null(null())}
+    # If function (if(`cond1`,`then1`,[`cond2`,`then2`,...],[`else`])).
+    - uri: /{if(true(),1),if(false(),1),if(null(),1),
+             if(true(),1,0),if(false(),1,0),if(null(),1,0),
+             if(true(),1,true(),2),if(true(),1,false(),2),
+             if(false(),1,true(),2),if(false(),1,false(),2),
+             if(false(),1,false(),2,0)}
+    # Switch function (switch(`token`,`case1`,`then1`,[`case2`,`then2`,...],[`else`])).
+    - uri: /{switch('Y','X',1),switch('Y','Y',1),
+             switch('Y','X',1,0),switch('Y','Y',1,0),
+             switch(null(),null(),1,0),
+             switch('Y','X',1,'Y',2,'Z',3),
+             switch('Y','A',1,'B',2,'C',3,0)}
+    # Equality/Inequality.
+    - uri: /{1=1,1=0,1=null(),null()=null(),
+             1!=1,1!=0,1!=null(),null()!=null(),
+             1==1,1==0,1==null(),null()==null(),
+             1!==1,1!==0,1!==null(),null()!==null()}
+    - uri: /{'X'='X',1=1.0,1=1e0,1.0=1e0,1='1'}
+    - uri: /{0={1,2,3},2={1,2,3},0!={1,2,3},2!={1,2,3}}
+    - uri: /{'X'!={'A','B','C'},1.0={0,1,2}}
+    - uri: /{integer('1')=string('1')}
+      expect: 400
+    # Less Than/Greater Than.
+    - uri: /{'X'<'Y','X'<='Y','X'>'Y','X'>='Y',
+             'X'<null(),'X'<=null(),'X'>null(),'X'>=null()}
+    - uri: /{1<2,1<=2,1>2,1>=2,
+             1<2.0,1<=2.0,1>2.0,1>=2.0,
+             1e0<2.0,1e0<=2.0,1e0>2.0,1e0>2.0}
+    - uri: /{integer('1')<string('1')}
+      expect: 400
+
+  - title: Numeric functions and operators
+    tests:
+    # Integer cast (from untyped, string, integer, decimal and float).
+    - uri: /{integer('1'),integer(string('1')),
+             integer(1),integer(1.0),integer(1e0)}
+    # Decimal->Integer and Float->Decimal casts.
+    - uri: /{integer(65536.0),integer(65.536),integer(655.36),
+             integer(65536e0),integer(65536e-3),integer(65535e-2)}
+    # Decimal->Integer cast overflow.
+    - uri: /{integer(100000000000000000000.0)}
+      expect: 409
+      ignore: true
+    # Float->Integer cast overflow.
+    - uri: /{integer(1e100)}
+      expect: 409
+      ignore: true
+    # Invalid Untyped->Integer cast.
+    - uri: /{integer('X')}
+      expect: 400
+    # Invalid String->Integer cast.
+    - uri: /{integer(string('X'))}
+      expect: 409
+      ignore: true
+    # Decimal cast (from untyped, string, integer, decimal and float).
+    - uri: /{decimal('1.5'),decimal(string('1.5')),
+             decimal(15),decimal(1.5),decimal(15e-1)}
+    # Float->Decimal cast.
+    - uri: /{decimal(65535e0),decimal(65535e10),decimal(65535e-10)}
+    # Float->Decimal cast with huge values.
+    - uri: /{decimal(1e300),decimal(1e-300)}
+    # Invalid Untyped->Decimal cast.
+    - uri: /{decimal('X')}
+      expect: 400
+    # Invalid String->Decimal cast.
+    - uri: /{decimal(string('X'))}
+      expect: 409
+      ignore: true
+    # Float cast (from untyped, string, integer, decimal and float).
+    - uri: /{float('1.5'),float(string('1.5')),
+             float(15),float(1.5),float(15e-1)}
+    # Decimal->Float cast with truncation.
+    - uri: /{float(123456789.123456789)}
+    # Invalid Untyped->Float cast.
+    - uri: /{float('X')}
+      expect: 400
+    # Invalid String->Float cast.
+    - uri: /{float(string('X'))}
+      expect: 409
+      ignore: true
+    # Unary plus and minus.
+    - uri: /{+2,+2.0,+2e0,-2,-2.0,-2e0,++1,+-1,-+1,--1}
+    # Addition.
+    - uri: /{2+2,2+2.0,2+2e0,2.0+2.0,2.0+2e0,2e0+2e0}
+    # Subtraction.
+    - uri: /{2-1,2-1.0,2-1e0,2.0-1.0,2.0-1e0,2e0-1e0}
+    # Multiplication.
+    - uri: /{5*5,5*5.0,5*5e0,5.0*5.0,5.0*5e0,5e0*5e0}
+    # Division.
+    - uri: /{1/2,1/2.0,1/2e0,1.0/2.0,1.0/2e0, 1e0/2e0}
+    # Division by zero.
+    - uri: /{1/0}
+      expect: 409
+      ignore: true
+    # Addition: invalid types.
+    - uri: /{1+'1'}
+      expect: 400
+    # Multiplication: overflow.
+    - uri: /{65536*65536}
+      expect: 409
+      ignore: true
+    # Round for decimal values.
+    - uri: /{round(65.536),round(65.536,0),
+             round(65.536,1),round(65.536,-1)}
+    # Round with integer values (implicitly cast to decimal).
+    - uri: /{round(65535),round(65536,-3)}
+    # Round for float values.
+    - uri: /{round(35536e-3)}
+    # Invalid Round call with float values and digits indicator.
+    - uri: /{round(35536e-3,1)}
+      expect: 400
+
+  - title: String functions and operators
+    tests:
+    # String cast (from untyped, string, integer, decimal, float).
+    - uri: /{string('X'),string(string('X')), string(1), string(1.0), string(1e0)}
+    # The Length method.
+    - uri: /{'HTSQL':length,'':length,null():length}
+    # The Contains operator.
+    - uri: /{'HTSQL'~'sql','HTSQL'~'HTTP','HTSQL'~'',
+             'HTSQL'~null(),null()~'HTSQL',null()~null()}
+
+  - title: Date functions and operators
+    tests:
+    # Date cast (from untyped, string).
+    - uri: /{date('2010-07-28'),date(string('2010-07-28'))}
+    # Date constructor.
+    - uri: /{date(2010,07,28)}
+    # Invalid untyped->date cast.
+    - uri: /{date('X')}
+      expect: 400
+    # Invalid string->date cast.
+    - uri: /{date(string('X'))}
+      expect: 409
+      ignore: true
+    # Invalid integer->date cast.
+    - uri: /{date(1)}
+      expect: 400
+      skip: true
+    # Invalid date constructor.
+    - uri: /{date(0,0,0)}
+      expect: 409
+      ignore: true
+      skip: true
+    # Date arithmetic operations.
+    - uri: /{date('2010-07-28')+1,date('2010-07-28')-1,
+             date('2010-07-28')-date('2009-07-28')}
+
+- title: Table functions
+  tests:
+
+  # Sort and Limit functions.
+  - title: Sorting and paging
+    tests:
+    - uri: /school
+    - uri: /school.limit(1)
+    - uri: /school.limit(2,5)
+    - uri: /school.sort(name).limit(1)
+    - uri: /school.limit(1).sort(name)
+    - uri: /course?department='acc'
+    - uri: /course.sort(credits)?department='acc'
+    - uri: /(course.sort(credits)?department='acc').limit(1,1)
+    - uri: /course.sort(credits).limit(1,1)?department='acc'
+
+  # The `as` operator.
+  - title: Title decorator
+    tests:
+    # Identifiers and string literals are accepted.
+    - uri: /{null() as Title, null() as 'Title with whitespaces'}
+    # The outer `as` overrides any others.
+    - uri: /{null() as 'Hidden title' as 'Visible title'}
+    # `as` in expressions is no-op.
+    - uri: /{('HT' as HT)+('SQL' as SQL)}
+    # Using `as` to decorate the segment base.
+    - uri: /(school as Schools)
+    # Also works over selectors and filters.
+    - uri: /(school as Schools){name as Title}?code='art'
+    # Expects a non-wildcard argument.
+    - uri: /school{* as Columns}
+      expect: 400
+    # Expects a string literal or an identifier.
+    - uri: /school{code as school.code}
+      expect: 400
+
+  # The order modifiers: `+` and `-`.
+  - title: Order modifiers
+    tests:
+    # Ascending order (default for this table).
+    - uri: /school{code+,name}
+    # Descending order.
+    - uri: /school{code-,name}
+    # Combinations of several order modifiers.
+    - uri: /school{code--,name}
+    # Combined with `as`.
+    - uri: /school{name as Title+}
+    - uri: /school{name+ as Title}
+      expect: 400
+    - uri: /school{(name+) as Title}
+    # Non-unique explicit ordering is complemented with the default ordering.
+    - uri: /course{department+,title,credits-}?number<200
+    # Same using the `sort` method.
+    - uri: /course.sort(department+,credits-){department,title,credits}?number<200
+

test/input/pgsql.yaml

 id: pgsql
 output: test/output/pgsql.yaml
 tests:
-# FIXME: need a regression schema and a set of tests.
 
 - title: Remove any existing regression database
   id: cleanup-pgsql
       port: ${PGSQL_PORT}
     sql-include: test/sql/regress-pgsql.sql
 
-- db: *connect
+- title: Run the test collection
+  id: test-pgsql
+  tests:
+  - define: pgsql
+  - db: *connect
+  # The Regression Schema
+  - include: test/input/schema.yaml
+  # Examples from the Tutorial
+  - include: test/input/tutorial.yaml
+  # Standard Data Types, Functions, and Operations
+  - include: test/input/library.yaml
+  # Edge Cases of HTSQL-to-SQL Translation
+  - include: test/input/translation.yaml
+  # Formatting Output Data
+  - include: test/input/format.yaml
 
-- title: Queries
-  id: queries-pgsql
-  tests:
-
-  # Display all tables in the regression schema.
-  - title: Regression schema
-    tests:
-    - uri: /school
-    - uri: /department
-    - uri: /program
-    - uri: /course
-    - uri: /instructor
-    - uri: /confidential
-    - uri: /appointment
-    - uri: /semester
-    - uri: /class
-    - uri: /student
-    - uri: /enrollment
-    - uri: /prerequisite
-    - uri: /classification
-    - uri: /course_classification
-    - uri: /program_requirement
-
-  # Empty and scalar queries.
-  - title: Scalar queries
-    tests:
-    - uri: /
-    - uri: /{'HT'+'SQL'}
-
-    - title: Literals
-      tests:
-      # String literals.
-      - uri: /{'','HTSQL','O''Reilly','λόγος',
-               '%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F',
-               '%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F'}
-      # Integer literals.
-      - uri: /{0,1,100,65536}
-      # Invalid integer literal (must be in range from -2**63 to 2**63-1).
-      - uri: /{115792089237316195423570985008687907853269984665640564039457584007913129639936}
-        expect: 400
-      # Decimal literals.
-      - uri: /{0.0,1.0,3.14,0.00000000001,1234567890.0987654321}
-      # Decimal values are of arbitrary size.
-      - uri: /{10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1}
-      # Float literals.
-      - uri: /{0e0,1e1,0.31415926535897931e1,2718281828459045e-16}
-      # Non-nuleric float values.
-      - uri: /{1e1024,-1e1024,1e1024/1e1024}
-
-    - title: Scalar functions
-      tests:
-      - title: Boolean constants and logical operators
-        tests:
-        # Boolean constants.
-        - uri: /{true(),false()}
-        # Boolean->Boolean cast (identity).
-        - uri: /{boolean(null()),boolean(true()),boolean(false())}
-        # Untyped boolean values.
-        - uri: /{boolean('true'),boolean('false')}
-        # Invalid untyped boolean values.
-        - uri: /{boolean('')}
-          expect: 400
-        - uri: /{boolean('X')}
-          expect: 400
-        # Number->Boolean cast (null => false, otherwise => true).
-        - uri: /{boolean(integer(null())),boolean(0),boolean(1),
-                 boolean(0.0),boolean(1.0),boolean(0e0),boolean(1e0)}
-        # String->Boolean cast (null, '' => false, otherwise => true).
-        - uri: /{boolean(string(null())),boolean(string('')),boolean(string('X')),
-                 boolean(string('true')),boolean(string('false'))}
-               # Note: boolean(string('false')) => true
-        # The AND operator.
-        - uri: /{false()&false(),false()&true(),true()&false(),true()&true(),
-                 null()&false(),null()&true(),null()&null()}
-        # Auto-cast of arguments (false,true).
-        - uri: /{string('')&string('X'),0&1}
-        # Auto-cast of NULL values (null,false,false).
-        - uri: /{null()&null(),integer(null())&null(),integer(null())&integer(null())}
-        # The OR operator.
-        - uri: /{false()|false(),false()|true(),true()|false(),true()|true(),
-                 null()|false(),null()|true(),null()|null()}
-        # Auto-cast of arguments (true,true,false).
-        - uri: /{string('')|string('X'),0|1,integer(null())|string(null())}
-        # The NOT operator.
-        - uri: /{!true(),!false(),!null()}
-        # Auto-cast of arguments (true,false,false,true).
-        - uri: /{!string(''),!string('X'),!1,!integer(null())}
-
-      - title: Comparison functions and operators
-        tests:
-        # Is NULL function (null => true, otherwise => false).
-        - uri: /{is_null(null()),is_null(true()),is_null(''),is_null(0)}
-        # Null If method (`this` is equal to one of the arguments => null, otherwise => `this`).
-        - uri: /{'Y':null_if('X'),'Y':null_if('Y'),
-                 (5):null_if(1,2,3),(5):null_if(1,2,3,4,5,6,7,8,9,10),
-                 null():null_if(null()),null():null_if('X')}
-          skip: true
-        - uri: /{'Y':null_if('X'),'Y':null_if('Y'),
-                 null():null_if(null()),null():null_if('X')}
-        # If Null method (`this` is null => the first non-null argument, otherwise => `this`).
-        - uri: /{'X':if_null('Y'),null():if_null('X'),null():if_null(null()),
-                 null():if_null(null(),null(),null()),
-                 null():if_null(null(),null(),0),
-                 null():if_null(0,1,2,3,null())}
-          skip: true
-        - uri: /{'X':if_null('Y'),null():if_null('X'),null():if_null(null())}
-        # If function (if(`cond1`,`then1`,[`cond2`,`then2`,...],[`else`])).
-        - uri: /{if(true(),1),if(false(),1),if(null(),1),
-                 if(true(),1,0),if(false(),1,0),if(null(),1,0),
-                 if(true(),1,true(),2),if(true(),1,false(),2),
-                 if(false(),1,true(),2),if(false(),1,false(),2),
-                 if(false(),1,false(),2,0)}
-        # Switch function (switch(`token`,`case1`,`then1`,[`case2`,`then2`,...],[`else`])).
-        - uri: /{switch('Y','X',1),switch('Y','Y',1),
-                 switch('Y','X',1,0),switch('Y','Y',1,0),
-                 switch(null(),null(),1,0),
-                 switch('Y','X',1,'Y',2,'Z',3),
-                 switch('Y','A',1,'B',2,'C',3,0)}
-        # Equality/Inequality.
-        - uri: /{1=1,1=0,1=null(),null()=null(),
-                 1!=1,1!=0,1!=null(),null()!=null(),
-                 1==1,1==0,1==null(),null()==null(),
-                 1!==1,1!==0,1!==null(),null()!==null()}
-        - uri: /{'X'='X',1=1.0,1=1e0,1.0=1e0,1='1'}
-        - uri: /{0={1,2,3},2={1,2,3},0!={1,2,3},2!={1,2,3}}
-        - uri: /{'X'!={'A','B','C'},1.0={0,1,2}}
-        - uri: /{integer('1')=string('1')}
-          expect: 400
-        # Less Than/Greater Than.
-        - uri: /{'X'<'Y','X'<='Y','X'>'Y','X'>='Y',
-                 'X'<null(),'X'<=null(),'X'>null(),'X'>=null()}
-        - uri: /{1<2,1<=2,1>2,1>=2,
-                 1<2.0,1<=2.0,1>2.0,1>=2.0,
-                 1e0<2.0,1e0<=2.0,1e0>2.0,1e0>2.0}
-        - uri: /{integer('1')<string('1')}
-          expect: 400
-
-      - title: Numeric functions and operators
-        tests:
-        # Integer cast (from untyped, string, integer, decimal and float).
-        - uri: /{integer('1'),integer(string('1')),
-                 integer(1),integer(1.0),integer(1e0)}
-        # Decimal->Integer and Float->Decimal casts.
-        - uri: /{integer(65536.0),integer(65.536),integer(655.36),
-                 integer(65536e0),integer(65536e-3),integer(65535e-2)}
-        # Decimal->Integer cast overflow.
-        - uri: /{integer(100000000000000000000.0)}
-          expect: 409
-          ignore: true
-        # Float->Integer cast overflow.
-        - uri: /{integer(1e100)}
-          expect: 409
-          ignore: true
-        # Invalid Untyped->Integer cast.
-        - uri: /{integer('X')}
-          expect: 400
-        # Invalid String->Integer cast.
-        - uri: /{integer(string('X'))}
-          expect: 409
-          ignore: true
-        # Decimal cast (from untyped, string, integer, decimal and float).
-        - uri: /{decimal('1.5'),decimal(string('1.5')),
-                 decimal(15),decimal(1.5),decimal(15e-1)}
-        # Float->Decimal cast.
-        - uri: /{decimal(65535e0),decimal(65535e10),decimal(65535e-10)}
-        # Float->Decimal cast with huge values.
-        - uri: /{decimal(1e300),decimal(1e-300)}
-        # Invalid Untyped->Decimal cast.
-        - uri: /{decimal('X')}
-          expect: 400
-        # Invalid String->Decimal cast.
-        - uri: /{decimal(string('X'))}
-          expect: 409
-          ignore: true
-        # Float cast (from untyped, string, integer, decimal and float).
-        - uri: /{float('1.5'),float(string('1.5')),
-                 float(15),float(1.5),float(15e-1)}
-        # Decimal->Float cast with truncation.
-        - uri: /{float(123456789.123456789)}
-        # Invalid Untyped->Float cast.
-        - uri: /{float('X')}
-          expect: 400
-        # Invalid String->Float cast.
-        - uri: /{float(string('X'))}
-          expect: 409
-          ignore: true
-        # Unary plus and minus.
-        - uri: /{+2,+2.0,+2e0,-2,-2.0,-2e0,++1,+-1,-+1,--1}
-        # Addition.
-        - uri: /{2+2,2+2.0,2+2e0,2.0+2.0,2.0+2e0,2e0+2e0}
-        # Subtraction.
-        - uri: /{2-1,2-1.0,2-1e0,2.0-1.0,2.0-1e0,2e0-1e0}
-        # Multiplication.
-        - uri: /{5*5,5*5.0,5*5e0,5.0*5.0,5.0*5e0,5e0*5e0}
-        # Division.
-        - uri: /{1/2,1/2.0,1/2e0,1.0/2.0,1.0/2e0, 1e0/2e0}
-        # Division by zero.
-        - uri: /{1/0}
-          expect: 409
-          ignore: true
-        # Addition: invalid types.
-        - uri: /{1+'1'}
-          expect: 400
-        # Multiplication: overflow.
-        - uri: /{65536*65536}
-          expect: 409
-          ignore: true
-        # Round for decimal values.
-        - uri: /{round(65.536),round(65.536,0),
-                 round(65.536,1),round(65.536,-1)}
-        # Round with integer values (implicitly cast to decimal).
-        - uri: /{round(65535),round(65536,-3)}
-        # Round for float values.
-        - uri: /{round(35536e-3)}
-        # Invalid Round call with float values and digits indicator.
-        - uri: /{round(35536e-3,1)}
-          expect: 400
-
-      - title: String functions and operators
-        tests:
-        # String cast (from untyped, string, integer, decimal, float).
-        - uri: /{string('X'),string(string('X')), string(1), string(1.0), string(1e0)}
-        # The Length method.
-        - uri: /{'HTSQL':length,'':length,null():length}
-        # The Contains operator.
-        - uri: /{'HTSQL'~'sql','HTSQL'~'HTTP','HTSQL'~'',
-                 'HTSQL'~null(),null()~'HTSQL',null()~null()}
-
-      - title: Date functions and operators
-        tests:
-        # Date cast (from untyped, string).
-        - uri: /{date('2010-07-28'),date(string('2010-07-28'))}
-        # Date constructor.
-        - uri: /{date(2010,07,28)}
-        # Invalid untyped->date cast.
-        - uri: /{date('X')}
-          expect: 400
-        # Invalid string->date cast.
-        - uri: /{date(string('X'))}
-          expect: 409
-          ignore: true
-        # Invalid integer->date cast.
-        - uri: /{date(1)}
-          expect: 400
-          skip: true
-        # Invalid date constructor.
-        - uri: /{date(0,0,0)}
-          expect: 409
-          ignore: true
-          skip: true
-        # Date arithmetic operations.
-        - uri: /{date('2010-07-28')+1,date('2010-07-28')-1,
-                 date('2010-07-28')-date('2009-07-28')}
-
-  # Sort and Limit functions.
-  - title: Sorting and paging
-    tests:
-    - uri: /school
-    - uri: /school.limit(1)
-    - uri: /school.limit(2,5)
-    - uri: /school.sort(name).limit(1)
-    - uri: /school.limit(1).sort(name)
-    - uri: /course?department='acc'
-    - uri: /course.sort(credits)?department='acc'
-    - uri: /(course.sort(credits)?department='acc').limit(1,1)
-    - uri: /course.sort(credits).limit(1,1)?department='acc'
-
-  # The `as` operator.
-  - title: Title decorator
-    tests:
-    # Identifiers and string literals are accepted.
-    - uri: /{null() as Title, null() as 'Title with whitespaces'}
-    # The outer `as` overrides any others.
-    - uri: /{null() as 'Hidden title' as 'Visible title'}
-    # `as` in expressions is no-op.
-    - uri: /{('HT' as HT)+('SQL' as SQL)}
-    # Using `as` to decorate the segment base.
-    - uri: /(school as Schools)
-    # Also works over selectors and filters.
-    - uri: /(school as Schools){name as Title}?code='art'
-    # Expects a non-wildcard argument.
-    - uri: /school{* as Columns}
-      expect: 400
-    # Expects a string literal or an identifier.
-    - uri: /school{code as school.code}
-      expect: 400
-
-  # The order modifiers: `+` and `-`.
-  - title: Order modifiers
-    tests:
-    # Ascending order (default for this table).
-    - uri: /school{code+,name}
-    # Descending order.
-    - uri: /school{code-,name}
-    # Combinations of several order modifiers.
-    - uri: /school{code--,name}
-    # Combined with `as`.
-    - uri: /school{name as Title+}
-    - uri: /school{name+ as Title}
-      expect: 400
-    - uri: /school{(name+) as Title}
-    # Non-unique explicit ordering is complemented with the default ordering.
-    - uri: /course{department+,title,credits-}?number<200
-    # Same using the `sort` method.
-    - uri: /course.sort(department+,credits-){department,title,credits}?number<200
-
-  # Simple (non-aggregate) filters.
-  - title: Simple filters
-    tests:
-    - uri: /school?code='ns'
-    - uri: /department?school.code='ns'
-    - uri: /department?school.code={'art','la'}
-    - uri: /program?school.code='ns'&code='uchem'
-    - uri: /course?credits=5
-    - uri: /department?school
-    # ENUM literal.
-    - uri: /semester?season='fall'
-    # Using the same term for a filter and a selector
-    - uri: /program{school.name,title}?school.code='art'
-
-  # Simple (non-aggregate) selectors.
-  - title: Simple selectors
-    tests:
-    - uri: /school{name}
-    - uri: /department{school.*,*}
-    - uri: /department{school.name+' - '+name}
-
-  # Plural links and aggregates.
-  - title: Aggregates
-    tests:
-    - uri: /exists(school)
-    - uri: /count(school)
-    - uri: /exists(school?exists(department))
-    - uri: /count(school?exists(department))
-    - uri: /{exists(school?!exists(department)),
-             count(school?!exists(department))}
-    - uri: /{count(course),min(course.credits),
-                           max(course.credits),
-                           avg(course.credits)}
-    - uri: /{count(school),count(department),count(course)}
-    - uri: /{count(department),count(department?exists(course))}
-    - uri: /department{code,count(course{credits=3})}
-    - uri: /department{code,count(course?credits=3)}
-    - uri: /school{code,count(department.course{credits=3})}
-    - uri: /school{code}?count(department.course{credits=3})=20
-    - uri: /department?exists(course.credits=5)
-    - uri: /department?every(course.credits=5)
-    - uri: /department{code,min(course.credits),max(course.credits)}
-    - uri: /department{code,avg(course.credits),
-                            sum(course.credits)/count(course.credits)}
-      skip: true
-    - uri: /department?exists(course)
-    - uri: /school?!exists(department)
-    - uri: /school{*,count(department)}
-    - uri: /school{*,count(department?exists(course))}
-    - uri: /school{*,count(department.exists(course))}
-    - uri: /school{code,count(department),count(program)}
-    - uri: /school{code,exists(department),exists(program)}
-      skip: true # broken until `reduce` is implemented
-    # Aggregates sharing the same spaces.
-    - uri: /department{sum(course.credits),count(course.credits),
-                       avg(course.credits)}?exists(course)
-    # Aggregates with non-native spaces.
-    # Triggers a bug in the Postgresql optimizer
-    # (see http://archives.postgresql.org/pgsql-bugs/2010-09/msg00265.php).
-    - uri: /department{code,school.code,
-                       count(school.program),school.count(program)}
-    - uri: /department{code,school.code,
-                       exists(school.program),school.exists(program)}
-    # Ensure that aggregates are not lost during the reduction process
-    - uri: /{count(school)&false(),count(school)|true()}
-    - uri: /{count(school)==null()}
-
-  - title: Root, This, Direct and Fiber functions
-    tests:
-    # Cross joins.
-    - uri: /{count(school)*count(department),count(school.direct(department))}
-    # Custom joins.
-    - uri: /{count(school.department),
-             count(school.direct(department)?school=root().school.code),
-             count(school.fiber(department,code,school.code))}
-    - uri: /school{code,count(department)}?count(department)=max(direct(school).count(department))
-    - uri: /school.program{school,code,count(student)}?count(student)=max(fiber(program,school).count(student))
-    # Lifting a unit to a dominating space.
-    - uri: /{count(school),this(){count(school)}?true(),this(){count(school)}?false()}
-    - uri: /school{code,name,this(){code}?name!~'art',
-                   root().school{code}?name!~'art'}
-    - uri: /school{name,count(department),this(){count(department)}?name!~'art'}
-    - uri: /school{name,exists(department),this(){exists(department)}?name!~'art'}
-
-  - title: Table Expressions
-    tests:
-    - uri: /(school?code='art').department
-    
-  # Demonstrate selection of a formatter based on the `Accept` header.
-  - title: Formatters
-    tests:
-    - uri: /school
-      headers:
-        Accept: application/json
-    - uri: /school
-      headers:
-        Accept: text/csv
-    - uri: /school
-      headers:
-        Accept: text/plain
-    - uri: /school
-      headers:
-        Accept: text/html
-    # Test how `as` interact with various formatters.
-    - uri: /(school as 'School Record'){code as 'Code name',
-                                        name as 'Long name'}
-      headers:
-        Accept: text/csv
-    - uri: /(school as 'School Record'){code as 'Code name',
-                                        name as 'Long name'}
-      headers:
-        Accept: text/plain
-    - uri: /(school as 'School Record'){code as 'Code name',
-                                        name as 'Long name'}
-      headers:
-        Accept: text/html
-    # Explicit format indicators.
-    - uri: /school/:json
-    - uri: /school/:csv
-    - uri: /school/:txt
-    - uri: /school/:html
-    - uri: /school/:unknown
-      expect: 400
-    # Verify that the filename in `Content-Disposition` field
-    # is properly escaped.
-    - uri: /{'%01%02...%1F%7F\"'}
-      headers:
-        Accept: text/csv
-
-  - title: Tutorial Tests
-    tests:
-    # Getting Started
-    - uri: /school
-    - uri: /program{school, code, title}
-    - uri: /department{name-, school}
-    - uri: /course{department+, number, credits-, title}
-    - uri: /course{department as 'Dept Code'+, number as 'No.', 
-                   credits-, title}
-    - uri: /program{school.name, title}
-    - uri: /program?degree={'ba','bs'}
-    - uri: /course{department.school.name, department.name, title}
-    - uri: /department{*,school.*}
-    - uri: /department?school='egn'  
-    - uri: /program?school='bus'&degree!='bs' 
-    - uri: /course{department, number, title}?
-              credits<3&department.school='ns'
-    - uri: /(course?credits<3&department.school='ns')
-              {department, number, title}
-    - uri: /school/:json
-    - uri: /course{department{code,name},number,title+}?
-               department.school='bus'/:csv
-    # Relating and Aggregating Data
-    - uri: /course{department.name, credits} # singular join
-    - uri: /department{name, course.credits} # invalid plural join
-      expect: 400
-    - uri: /department{name, max(course.credits)} 
-    - uri: /department{name, count(school)} # invalid singular join
-      expect: 400
-    - uri: /max(course.credits)
-    - uri: /school{name, count(program), count(department)}
-    - uri: /department{name, count(course?number>=400)}
-    - uri: /school{name, avg(department.count(course))}
-    - uri: /school{name, count(department?exists(course?credits>3))}
-    - uri: /department{name, avg((course?number>400).credits)}
-    - uri: /department{code, min(course.credits), 
-              max(course.credits), avg(course.credits)}
-    - uri: /department{name, avg(course.credits), count(course)} 
-              ?every(course.credits=3)
-    # Predicate Expressions
-    - uri: /course?title='Drawing'
-    - uri: /course?title~'lab'
-    - uri: /course?title!= 'Organic Chemistry Laboratory I'
-    - uri: /course{department,number,title}?number=101
-    - uri: /course?credits>3
-    - uri: /course?credits>=3
-    - uri: /department?code>'me'
-    - uri: /{true(), false(), null()}
-    - uri: /department{code, name}?is_null(school)
-    - uri: /department{code, name}?!is_null(school)
-    - uri: /course?department='acc'&credits<3
-    - uri: /course?credits>4|credits<3
-    - uri: /{true()|false()&false(),(true()|false())&false()} 
-    - uri: /course?(department='arthis'|department='stdart')&credits>3
-    - uri: /course?department='arthis'|department='stdart'&credits>3
-    - uri: /course?!(department='arthis'|department='stdart')
-    - uri: /course?department!={'arthis','stdart'}
-    - uri: /course?description
-    - uri: /course{department,number,description}? !description
-    - uri: /department?school==null()
-    - uri: /department?school!=='art'

test/input/routine.yaml

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+title: HTSQL-CTL Command-Line Tool
+id: routine
+output: test/output/routine.yaml
+tests:
+- ctl: []
+- ctl: [help]
+- ctl: [help, help]
+# FIXME: need comprehensive tests for each routine.
+

test/input/schema.yaml

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+title: The Regression Schema
+id: schema
+tests:
+
+- title: Tables
+  tests:
+  # Administrative Directory
+  - uri: /school
+  - uri: /department
+  - uri: /program
+  - uri: /course
+  # Instructor Directory
+  - uri: /instructor
+  - uri: /confidential
+  - uri: /appointment
+  # Class Directory
+  - uri: /semester
+  - uri: /class
+  # Enrollment Directory
+  - uri: /student
+  - uri: /enrollment
+  # Requirement Directory
+  - uri: /prerequisite
+  - uri: /classification
+  - uri: /course_classification
+  - uri: /program_requirement
+
+- title: Links
+  tests:
+  # FIXME: rewrite using locators once we have them.
+
+  # School -> Department
+  - uri: /(school?code='art').department
+  # School -> Program
+  - uri: /(school?code='art').program
+  # School -> Student (via Program)
+  - uri: /(school?code='art').student
+
+  # Department -> School (singular, optional)
+  - uri: /(department?code='ee').school
+  # Department -> Course
+  - uri: /(department?code='ee').course
+  # Department -> Appointment
+  - uri: /(department?code='ee').appointment
+  # Department -> Class (via Course)
+  - uri: /(department?code='ee').class
+
+  # Program -> School (parental)
+  - uri: /(program?school='egn'&code='umech').school
+  # Program -> Student
+  - uri: /(program?school='egn'&code='umech').student
+  # Program -> Program Requirement
+  - uri: /(program?school='egn'&code='umech').program_requirement
+
+  # Course -> Department (parental)
+  - uri: /(course?department='psych'&number=610).department
+  # Course -> Class
+  - uri: /(course?department='psych'&number=610).class
+  # Course -> Prerequisite
+  # FIXME: broken! Cannot choose between on_course and of_course links;
+  # need a mechanism to select a desired link among ambiguous links.
+  - uri: /(course?department='psych'&number=610).prerequisite
+    expect: 400
+  # Course -> Course Classification
+  - uri: /(course?department='psych'&number=610).course_classification
+
+  # Instructor -> Confidential (singular, optional)
+  - uri: /(instructor?code='kcavallaro').confidential
+  # Instructor -> Appointment
+  - uri: /(instructor?code='kcavallaro').appointment
+  # Instructor -> Class
+  - uri: /(instructor?code='kcavallaro').class
+
+  # Confidential -> Instructor
+  - uri: /(confidential?instructor='tobrien21').instructor
+
+  # Appointment -> Department (parental)
+  - uri: /(appointment?department='eng'&instructor='wyu112').department
+  # Appointment -> Instructor (parental)
+  - uri: /(appointment?department='eng'&instructor='wyu112').instructor
+
+  # Semester -> Class
+  - uri: /(semester?year=2010&season='fall').class
+
+  # Class -> Department (grand-parental, via Course)
+  - uri: /(class?class_seq=10086).department
+  # Class -> Course (parental)
+  - uri: /(class?class_seq=10086).course
+  # Class -> Semester (parental)
+  - uri: /(class?class_seq=10086).semester
+  # Aliases for Class -> Semester
+  - uri: /(class?class_seq=10086).year
+  - uri: /(class?class_seq=10086).season
+  # Class -> Instructor (singular, optional)
+  - uri: /(class?class_seq=10086).instructor
+  # Class -> Enrollment
+  - uri: /(class?class_seq=10086).enrollment
+
+  # Student -> School (singular, optional, via Program)
+  - uri: /(student?number=25371).school
+  # Student -> Program (singular, optional)
+  - uri: /(student?number=25371).program
+  # Student -> Enrollment
+  - uri: /(student?number=25371).enrollment
+
+  # Enrollment -> Student (parental)
+  - uri: /(enrollment?student=92039&class=10071).student
+  # Enrollment -> Class (parental)
+  - uri: /(enrollment?student=92039&class=10071).class
+
+  # Prerequisite -> Department (grand-parental, via Course, using of_department)
+  - uri: /(prerequisite?of_department='capmrk'&of_course=818
+                       &on_department='acc'&on_course=315).of_department
+  # Prerequisite -> Department (grand-parental, via Course, using on_department)
+  - uri: /(prerequisite?of_department='capmrk'&of_course=818
+                       &on_department='acc'&on_course=315).on_department
+  # Prerequisite -> Department (ambiguous)
+  - uri: /(prerequisite?of_department='capmrk'&of_course=818
+                       &on_department='acc'&on_course=315).department
+    expect: 400
+  # Prerequisite -> Course (parental, using of_course)
+  - uri: /(prerequisite?of_department='capmrk'&of_course=818
+                       &on_department='acc'&on_course=315).of_course
+  # Prerequisite -> Course (parental, using on_course)
+  - uri: /(prerequisite?of_department='capmrk'&of_course=818
+                       &on_department='acc'&on_course=315).on_course
+  # Prerequisite -> Course (ambiguous)
+  - uri: /(prerequisite?of_department='capmrk'&of_course=818
+                       &on_department='acc'&on_course=315).course
+    expect: 400
+
+  # Classification -> Classification (singular, self-referencial)
+  - uri: /(classification?code='modlanguage').part_of
+  # Classification -> Classification (reverse self-referential)
+  # FIXME: broken! No way to indicate a reverse self-referential link.
+  # The query below, in fact, produces a direct self-referential link.
+  - uri: /(classification?code='modlanguage').classification
+  # Classification -> Course Classification
+  - uri: /(classification?code='modlanguage').course_classification
+  # Classification -> Program Requirement
+  - uri: /(classification?code='modlanguage').program_requirement
+
+  # Course Classification -> Department (grand-parental, via Course)
+  - uri: /(course_classification?department='be'&course=112
+                                &classification='begeneral').department
+  # Course Classification -> Course (parental)
+  - uri: /(course_classification?department='be'&course=112
+                                &classification='begeneral').course
+  # Course Classification -> Classification (parental)
+  - uri: /(course_classification?department='be'&course=112
+                                &classification='begeneral').classification
+
+  # Program Requirement -> School (grand-parental, via Program)
+  - uri: /(program_requirement?school='bus'&program='uacct'
+                              &classification='accounting').school
+  # Program Requirement -> Program (parental)
+  - uri: /(program_requirement?school='bus'&program='uacct'
+                              &classification='accounting').program
+  # Program Requirement -> Classification (parental)
+  - uri: /(program_requirement?school='bus'&program='uacct'
+                              &classification='accounting').classification
+

test/input/sqlite.yaml

 id: sqlite
 output: test/output/sqlite.yaml
 tests:
-# FIXME: need a regression schema and a set of tests.
 
 - title: Remove any existing regression database
   id: cleanup-sqlite
       database: build/regress/regress-sqlite/htsql_regress.sqlite
     sql-include: test/sql/regress-sqlite.sql
 
-- db: *connect
+- title: Run the test collection
+  id: test-sqlite
+  tests:
+  - define: sqlite
+  - db: *connect
+  # The Regression Schema
+  - include: test/input/schema.yaml
+  # Examples from the Tutorial
+  - include: test/input/tutorial.yaml
+  # Standard Data Types, Functions, and Operations
+  - include: test/input/library.yaml
+    skip: true  # FIXME: poor support for standard functions
+  # Edge Cases of HTSQL-to-SQL Translation
+  - include: test/input/translation.yaml
+  # Formatting Output Data
+  - include: test/input/format.yaml
 
-- title: Queries
-  id: queries-sqlite
-  tests:
-
-  # Display all tables in the regression schema.
-  - title: Regression schema
-    tests:
-    - uri: /school
-    - uri: /department
-    - uri: /program
-    - uri: /course
-    - uri: /instructor
-    - uri: /confidential
-    - uri: /appointment
-    - uri: /semester
-    - uri: /class
-    - uri: /student
-    - uri: /enrollment
-    - uri: /prerequisite
-    - uri: /classification
-    - uri: /course_classification
-    - uri: /program_requirement
-
-  # Empty and scalar queries.
-  - title: Scalar queries
-    tests:
-    - uri: /
-    - uri: /{2+2}
-    - uri: /{'HT'+'SQL'}
-
-  # Simple (non-aggregate) filters.
-  - title: Simple filters
-    tests:
-    - uri: /school?code='ns'
-    - uri: /department?school.code='ns'
-    - uri: /program?school.code='ns'&code='uchem'
-    - uri: /course?credits=5
-
-  # Simple (non-aggregate) selectors.
-  - title: Simple selectors
-    tests:
-    - uri: /school{name}
-    - uri: /department{school.*,*}
-    - uri: /department{school.name+' - '+name}
-

test/input/translation.yaml

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+title: Edge Cases of HTSQL-to-SQL Translation
+id: translation
+tests:
+
+# FIXME: update and refurbish!
+
+- title: Random collection of tests
+  tests:
+
+  # Simple (non-aggregate) filters.
+  - title: Simple filters
+    tests:
+    - uri: /school?code='ns'
+    - uri: /department?school.code='ns'
+    - uri: /department?school.code={'art','la'}
+    - uri: /program?school.code='ns'&code='uchem'
+    - uri: /course?credits=5
+    - uri: /department?school
+    # ENUM literal.
+    - uri: /semester?season='fall'
+    # Using the same term for a filter and a selector
+    - uri: /program{school.name,title}?school.code='art'
+
+  # Simple (non-aggregate) selectors.
+  - title: Simple selectors
+    tests:
+    - uri: /school{name}
+    - uri: /department{school.*,*}
+    - uri: /department{school.name+' - '+name}
+
+  # Plural links and aggregates.
+  - title: Aggregates
+    tests:
+    - uri: /exists(school)
+    - uri: /count(school)
+    - uri: /exists(school?exists(department))
+    - uri: /count(school?exists(department))
+    - uri: /{exists(school?!exists(department)),
+             count(school?!exists(department))}
+    - uri: /{count(course),min(course.credits),
+                           max(course.credits),
+                           avg(course.credits)}
+    - uri: /{count(school),count(department),count(course)}
+    - uri: /{count(department),count(department?exists(course))}
+    - uri: /department{code,count(course{credits=3})}
+    - uri: /department{code,count(course?credits=3)}
+    - uri: /school{code,count(department.course{credits=3})}
+    - uri: /school{code}?count(department.course{credits=3})=20
+    - uri: /department?exists(course.credits=5)
+    - uri: /department?every(course.credits=5)
+    - uri: /department{code,min(course.credits),max(course.credits)}
+    - uri: /department{code,avg(course.credits),
+                            sum(course.credits)/count(course.credits)}
+      skip: true
+    - uri: /department?exists(course)
+    - uri: /school?!exists(department)
+    - uri: /school{*,count(department)}
+    - uri: /school{*,count(department?exists(course))}
+    - uri: /school{*,count(department.exists(course))}
+    - uri: /school{code,count(department),count(program)}
+    - uri: /school{code,exists(department),exists(program)}
+      skip: true # broken until `reduce` is implemented
+    # Aggregates sharing the same spaces.
+    - uri: /department{sum(course.credits),count(course.credits),
+                       avg(course.credits)}?exists(course)
+    # Aggregates with non-native spaces.
+    # Triggers a bug in the Postgresql optimizer
+    # (see http://archives.postgresql.org/pgsql-bugs/2010-09/msg00265.php).
+    - uri: /department{code,school.code,
+                       count(school.program),school.count(program)}
+    - uri: /department{code,school.code,
+                       exists(school.program),school.exists(program)}
+    # Ensure that aggregates are not lost during the reduction process
+    - uri: /{count(school)&false(),count(school)|true()}
+    - uri: /{count(school)==null()}
+
+  - title: Root, This, Direct and Fiber functions
+    tests:
+    # Cross joins.
+    - uri: /{count(school)*count(department),count(school.direct(department))}
+    # Custom joins.
+    - uri: /{count(school.department),
+             count(school.direct(department)?school=root().school.code),
+             count(school.fiber(department,code,school.code))}
+    - uri: /school{code,count(department)}?count(department)=max(direct(school).count(department))
+    - uri: /school.program{school,code,count(student)}?count(student)=max(fiber(program,school).count(student))
+    # Lifting a unit to a dominating space.
+    - uri: /{count(school),this(){count(school)}?true(),this(){count(school)}?false()}
+    - uri: /school{code,name,this(){code}?name!~'art',
+                   root().school{code}?name!~'art'}
+      ifndef: sqlite    # no support for `~` operator yet
+    - uri: /school{name,count(department),this(){count(department)}?name!~'art'}
+      ifndef: sqlite    # no support for `~` operator yet
+    - uri: /school{name,exists(department),this(){exists(department)}?name!~'art'}
+      ifndef: sqlite    # no support for `~` operator yet
+
+  - title: Table Expressions
+    tests:
+    - uri: /(school?code='art').department
+

test/input/tutorial.yaml

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+title: Examples from the Tutorial
+id: tutorial
+tests:
+
+- title: Getting Started
+  tests:
+  # Selecting Data
+  - uri: /school
+  - uri: /program{school, code, title}
+  - uri: /department{name-, school}
+  - uri: /course{department+, number, credits-, title}
+  - uri: /course{department as 'Dept Code'+, number as 'No.',
+                 credits-, title}
+  # Linking Data
+  - uri: /program{school.name, title}
+  - uri: /course{department.school.name, department.name, title}
+  - uri: /course{department{school.name, name}, title}
+  - uri: /department{*,school.*}
+  # Filtering Data
+  - uri: /department?school='egn'
+  - uri: /program?degree={'ba','bs'}
+  - uri: /program?school='bus'&degree!='bs'
+  - uri: /course{department, number, title}
+                ?credits<3&department.school='ns'
+  - uri: /(course?credits<3&department.school='ns')
+                {department, number, title}
+  # Formatters
+  - uri: /school/:json
+  # Putting it All Together
+  - uri: /course{department{code, name},number,title+}
+                ?department.school='bus'/:csv
+
+- title: Relating and Aggregating Data
+  tests:
+  # Basic Linking
+  - uri: /course{department.name, title}    # singular join
+  - uri: /department{name, course.credits}  # invalid plural join
+    expect: 400
+  - uri: /department{name, max(course.credits)}
+  - uri: /department{name, count(school)}   # invalid singular join
+    expect: 400
+  - uri: /max(course.credits)
+  # Aggregate Expressions
+  - uri: /school{name, count(program), count(department)}
+  - uri: /department{name, count(course?number>=400)}
+  - uri: /school{name, avg(department.count(course))}
+  - uri: /school{name, count(department?exists(course?credits>3))}
+  - uri: /department{name, avg((course?number>400).credits)}
+  - uri: /department{code, min(course.credits), max(course.credits),
+                           avg(course.credits)}
+  - uri: /department{name, avg(course.credits), count(course)}
+                ?every(course.credits=3)
+
+- title: Logical Expressions
+  tests:
+  # Comparison Operators
+  - uri: /course?title='Drawing'
+  - uri: /course?title~'lab'
+    ifndef: sqlite  # no support for `~` operator yet
+  - uri: /course?title!~'lab'
+    ifndef: sqlite  # no support for `~` operator yet
+  - uri: /course?title!='Organic Chemistry Laboratory I'
+  - uri: /course{department,number,title}?number=101
+  - uri: /course?department!={'arthis','stdart'}
+  - uri: /course?credits>3
+  - uri: /course?credits>=3
+  - uri: /department?code>'me'
+  # Boolean Expressions
+  - uri: /{true(), false(), null(), ''}
+  - uri: /department{code, name}?is_null(school)
+  - uri: /department{code, name}?!is_null(school)
+  - uri: /course?department='acc'&credits<3
+  - uri: /course?credits>4|credits<3
+  - uri: /course?(department='arthis'|department='stdart')&credits>3
+  - uri: /course?department='arthis'|department='stdart'&credits>3
+  - uri: /course?description
+  - uri: /course{department, number, description}?!description
+
+- title: Types and Functions
+  tests:
+  # Working with NULLs
+  - uri: /department?school=null()
+  - uri: /department?school==null()
+  - uri: /department?school!=='art'
+
+- title: Odds & Ends
+  tests:
+  - uri: /{1='1'}               # untyped literals
+  - uri: /{'Bursar''s Office'}  # single-quote escaping
+  - uri: /{'%25'}               # percent-encoding
+  - uri: /course.sort(credits)  # sort expression
+  - uri: /course.limit(5,20)    # limit/offset
+

test/output/ctl.yaml

-#
-# This file contains expected test output data for regression tests.
-# It was generated automatically by the `regress` routine.
-#
-
-id: ctl
-tests:
-- ctl: []
-  stdout: |
-    HTSQL-CTL - HTSQL command-line administrative application
-    Copyright (c) 2006-2010, Prometheus Research, LLC
-
-    Run `htsql-ctl help` for general usage and list of routines.
-    Run `htsql-ctl help <routine>` for help on a specific routine.
-  exit: 0
-- ctl: [help]
-  stdout: |+
-    HTSQL-CTL - HTSQL command-line administrative application
-    Copyright (c) 2006-2010, Prometheus Research, LLC
-    Usage: htsql-ctl <routine> [options] [arguments]
-
-    Run `htsql-ctl help` for general usage and list of routines.
-    Run `htsql-ctl help <routine>` for help on a specific routine.
-
-    Available routines:
-      help (h, ?)              : describe the usage of the application and its routines
-      version                  : display the version of the application
-      server (serve, s)        : start an HTTP server handling HTSQL requests
-      shell (sh)               : start an HTSQL shell
-      get                      : execute and render an HTSQL query
-      post                     : execute and render an HTSQL query with POST data
-      regress (test)           : run regression tests
-
-  exit: 0
-- ctl: [help, help]
-  stdout: |+
-    HELP - describe the usage of the application and its routines
-    Usage: htsql-ctl help [ROUTINE [FEATURE]]
-
-    Run 'htsql-ctl help' to describe the usage of the application and
-    get the list of available routines.
-
-    Run 'htsql-ctl help <routine>' to describe the usage of the
-    specified routine.
-
-    Some routines may contain separate descriptions of some features.
-    Run 'htsql-ctl help <routine> <feature>' to describe a specific
-    feature.
-
-    Arguments:
-      ROUTINE                  : the name of the routine to describe
-      FEATURE                  : the feature to describe
-
-  exit: 0
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.