diff --git a/src/adql/parser/grammar/adqlGrammar201.jj b/src/adql/parser/grammar/adqlGrammar201.jj
index 8ed7059cf7ca2f10310a77e2cb050f8bd0f130f3..5eb28a82c09309a5fd24ec914e05387639215d7c 100644
--- a/src/adql/parser/grammar/adqlGrammar201.jj
+++ b/src/adql/parser/grammar/adqlGrammar201.jj
@@ -301,6 +301,7 @@ TOKEN : {
 |	< ORDER: "ORDER" >   { matchedToken.adqlReserved = true; }
 |	< ASC: "ASC" >       { matchedToken.adqlReserved = true; }
 |	< DESC: "DESC" >     { matchedToken.adqlReserved = true; }
+|   < OFFSET: "OFFSET" > { matchedToken.adqlReserved = true; }
 }
 
 /* ************* */
@@ -439,6 +440,7 @@ ADQLQuery QueryExpression(): {TextPosition endPos = null;} {
 	[GroupBy() {endPos = query.getGroupBy().getPosition();}]
 	[Having()  {endPos = query.getHaving().getPosition();}]
 	[OrderBy() {endPos = query.getOrderBy().getPosition();}]
+	[Offset()  {endPos = new TextPosition(token);}]
 	{
 		// set the position of the query:
 		query.setPosition(new TextPosition(query.getSelect().getPosition(), endPos));
@@ -577,6 +579,17 @@ void OrderBy(): {ClauseADQL<ADQLOrder> orderBy = query.getOrderBy(); ADQLOrder o
 	{ orderBy.setPosition(new TextPosition(start, token)); }
 }
 
+void Offset(): { Token t; } {
+	<OFFSET> t=<UNSIGNED_INTEGER>
+	{
+		try{
+			query.setOffset(Integer.parseInt(t.image));
+		}catch(NumberFormatException nfe){
+			throw new ParseException("The OFFSET limit (\""+t.image+"\") isn't a regular unsigned integer!", new TextPosition(t));
+		}
+	}
+}
+
 /* *************************** */
 /* COLUMN AND TABLE REFERENCES */
 /* *************************** */
diff --git a/src/adql/query/ADQLQuery.java b/src/adql/query/ADQLQuery.java
index eb230f8ccfbc495a49cd46824c12389c116d7afd..6d1b75423ad30c00ada4c822217f330064b672d5 100644
--- a/src/adql/query/ADQLQuery.java
+++ b/src/adql/query/ADQLQuery.java
@@ -52,7 +52,7 @@ import adql.search.ISearchHandler;
  * </p>
  *
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (07/2019)
+ * @version 2.0 (08/2019)
  */
 public class ADQLQuery implements ADQLObject {
 
@@ -82,6 +82,10 @@ public class ADQLQuery implements ADQLObject {
 	/** The ADQL clause ORDER BY. */
 	private ClauseADQL<ADQLOrder> orderBy;
 
+	/** The ADQL clause OFFSET.
+	 * @since 2.0 */
+	private int offset;
+
 	/** Position of this Query (or sub-query) inside the whole given ADQL query
 	 * string.
 	 * @since 1.4 */
@@ -112,6 +116,7 @@ public class ADQLQuery implements ADQLObject {
 		groupBy = new ClauseADQL<ADQLColumn>("GROUP BY");
 		having = new ClauseConstraints("HAVING");
 		orderBy = new ClauseADQL<ADQLOrder>("ORDER BY");
+		offset = -1;
 	}
 
 	/**
@@ -130,6 +135,7 @@ public class ADQLQuery implements ADQLObject {
 		groupBy = (ClauseADQL<ADQLColumn>)toCopy.groupBy.getCopy();
 		having = (ClauseConstraints)toCopy.having.getCopy();
 		orderBy = (ClauseADQL<ADQLOrder>)toCopy.orderBy.getCopy();
+		offset = toCopy.offset;
 		position = (toCopy.position == null) ? null : new TextPosition(toCopy.position);
 	}
 
@@ -160,6 +166,7 @@ public class ADQLQuery implements ADQLObject {
 		groupBy.clear();
 		having.clear();
 		orderBy.clear();
+		offset = -1;
 		position = null;
 	}
 
@@ -331,6 +338,62 @@ public class ADQLQuery implements ADQLObject {
 		position = null;
 	}
 
+	/**
+	 * Gets the OFFSET value of this query.
+	 *
+	 * @return	Its OFFSET value,
+	 *        	or a negative value if no OFFSET is set.
+	 *
+	 * @since 2.0
+	 */
+	public final int getOffset() {
+		return offset;
+	}
+
+	/**
+	 * Tell whether an OFFSET is set in this query.
+	 *
+	 * @return	<code>true</code> if an OFFSET is set,
+	 *        	<code>false</code> otherwise.
+	 *
+	 * @since 2.0
+	 */
+	public final boolean hasOffset() {
+		return (offset > -1);
+	}
+
+	/**
+	 * Remove the OFFSET value of this query.
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	The position of the query is erased.
+	 * </i></p>.
+	 *
+	 * @since 2.0
+	 */
+	public void setNoOffset() {
+		offset = -1;
+		position = null;
+	}
+
+	/**
+	 * Replaces its OFFSET value by the given one.
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	The position of the query is erased.
+	 * </i></p>
+	 *
+	 * @param newOffset	The new OFFSET value.
+	 *                 	<i><b>Note:</b> a negative value removes the OFFSET from
+	 *                 	this query.</i>
+	 *
+	 * @since 2.0
+	 */
+	public void setOffset(final int newOffset) {
+		offset = newOffset;
+		position = null;
+	}
+
 	@Override
 	public final TextPosition getPosition() {
 		return position;
@@ -580,6 +643,9 @@ public class ADQLQuery implements ADQLObject {
 		if (!orderBy.isEmpty())
 			adql.append('\n').append(orderBy.toADQL());
 
+		if (hasOffset())
+			adql.append("\nOFFSET ").append(offset);
+
 		return adql.toString();
 	}
 
diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java
index 7cf11859431b251d9ef332a885f1bd33d3aa6a79..f31476be4fedf2dd1f0df6f61a453cf0bfea7cc9 100644
--- a/src/adql/translator/JDBCTranslator.java
+++ b/src/adql/translator/JDBCTranslator.java
@@ -375,7 +375,10 @@ public abstract class JDBCTranslator implements ADQLTranslator {
 			sql.append('\n').append(translate(query.getOrderBy()));
 
 		if (query.getSelect().hasLimit())
-			sql.append("\nLimit ").append(query.getSelect().getLimit());
+			sql.append("\nLIMIT ").append(query.getSelect().getLimit());
+
+		if (query.hasOffset())
+			sql.append("\nOFFSET ").append(query.getOffset());
 
 		return sql.toString();
 	}
diff --git a/src/adql/translator/SQLServerTranslator.java b/src/adql/translator/SQLServerTranslator.java
index 3a045b0d41e96fc7caeb25773128dbf00f01fbac..21c6b9bbf1cc9960c65621637d289ffc7ef7fe86 100644
--- a/src/adql/translator/SQLServerTranslator.java
+++ b/src/adql/translator/SQLServerTranslator.java
@@ -134,10 +134,13 @@ public class SQLServerTranslator extends JDBCTranslator {
 
 	/**
 	 * For SQL Server, {@link #translate(ClauseSelect)} must be overridden for
-	 * TOP/LIMIT handling. We must not add the LIMIT at the end of the query, it
-	 * must go in the SELECT.
+	 * LIMIT and OFFSET handling.
 	 *
-	 * @see #translate(ClauseSelect)
+	 * <p><i><b>Implementation note:</b>
+	 * 	LIMIT is replaced by FETCH NEXT instead of TOP because of the addition
+	 * 	of OFFSET support in ADQL-2.1 grammar. With MS-SQLServer, TOP can not be
+	 * 	used with OFFSET...it must be OFFSET...LIMIT....
+	 * </i></p>
 	 */
 	@Override
 	public String translate(ADQLQuery query) throws TranslationException {
@@ -157,6 +160,16 @@ public class SQLServerTranslator extends JDBCTranslator {
 		if (!query.getOrderBy().isEmpty())
 			sql.append('\n').append(translate(query.getOrderBy()));
 
+		if (query.getSelect().hasLimit()) {
+			if (query.hasOffset())
+				sql.append('\n').append("OFFSET ").append(query.getOffset()).append(" ROWS");
+			else
+				sql.append('\n').append("OFFSET 0 ROWS");
+			sql.append(" FETCH NEXT ").append(query.getSelect().getLimit()).append(" ROWS ONLY");
+		} else if (query.hasOffset()) {
+			sql.append('\n').append("OFFSET ").append(query.getOffset()).append(" ROWS");
+		}
+
 		return sql.toString();
 	}
 
@@ -165,9 +178,9 @@ public class SQLServerTranslator extends JDBCTranslator {
 		String sql = null;
 
 		for(int i = 0; i < clause.size(); i++) {
-			if (i == 0) {
-				sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : "") + (clause.hasLimit() ? " TOP " + clause.getLimit() + " " : "");
-			} else
+			if (i == 0)
+				sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : "");
+			else
 				sql += " " + clause.getSeparator(i);
 
 			sql += " " + translate(clause.get(i));
diff --git a/test/adql/parser/TestADQLParser.java b/test/adql/parser/TestADQLParser.java
index 13ff99194cc46e9945b183c9994a52a66baf6eeb..b17a38c65d91711516a89420fb1f9d1843a70515 100644
--- a/test/adql/parser/TestADQLParser.java
+++ b/test/adql/parser/TestADQLParser.java
@@ -51,6 +51,45 @@ public class TestADQLParser {
 	public void tearDown() throws Exception {
 	}
 
+	@Test
+	public void testOffset() {
+
+		// CASE: No OFFSET in ADQL-2.0
+		ADQLParser parser = new ADQLParser(ADQLVersion.V2_0);
+		try {
+			parser.parseQuery("SELECT * FROM foo ORDER BY id OFFSET 10");
+			fail("OFFSET should not be allowed with ADQL-2.0!");
+		} catch(Exception ex) {
+			assertEquals(ParseException.class, ex.getClass());
+			assertEquals(" Encountered \"OFFSET\". Was expecting one of: <EOF> \",\" \";\" \"ASC\" \"DESC\" ", ex.getMessage());
+		}
+
+		// CASE: OFFSET allowed in ADQL-2.1
+		parser = new ADQLParser(ADQLVersion.V2_1);
+		try {
+			assertEquals("SELECT *\nFROM foo\nOFFSET 10", parser.parseQuery("SELECT * FROM foo OFFSET 10").toADQL());
+			assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nOFFSET 10", parser.parseQuery("SELECT * FROM foo ORDER BY id OFFSET 10").toADQL());
+			assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nOFFSET 0", parser.parseQuery("SELECT * FROM foo ORDER BY id OFFSET 0").toADQL());
+			assertEquals("SELECT TOP 5 *\nFROM foo\nORDER BY id ASC\nOFFSET 10", parser.parseQuery("SELECT TOP 5 * FROM foo ORDER BY id OFFSET 10").toADQL());
+		} catch(Exception ex) {
+			ex.printStackTrace();
+			fail("Unexpected error with a valid OFFSET syntax! (see console for more details)");
+		}
+
+		// CASE: Only an unsigned integer constant is allowed
+		String[] offsets = new String[]{ "-1", "colOffset", "2*5" };
+		String[] expectedErrors = new String[]{ " Encountered \"-\". Was expecting: <UNSIGNED_INTEGER> ", " Encountered \"colOffset\". Was expecting: <UNSIGNED_INTEGER> ", " Encountered \"*\". Was expecting one of: <EOF> \";\" " };
+		for(int i = 0; i < offsets.length; i++) {
+			try {
+				parser.parseQuery("SELECT * FROM foo OFFSET " + offsets[i]);
+				fail("Incorrect offset expression (\"" + offsets[i] + "\"). This test should have failed.");
+			} catch(Exception ex) {
+				assertEquals(ParseException.class, ex.getClass());
+				assertEquals(expectedErrors[i], ex.getMessage());
+			}
+		}
+	}
+
 	@Test
 	public void testColumnReference() {
 		ADQLParser parser = new ADQLParser();
diff --git a/test/adql/query/constraint/TestIN.java b/test/adql/query/constraint/TestIN.java
index ab385dbf0840dc3be68ac02acbf25a1e32dbeec3..4b3aab52b22a15e50ba936b90fa793a1e516caee 100644
--- a/test/adql/query/constraint/TestIN.java
+++ b/test/adql/query/constraint/TestIN.java
@@ -13,7 +13,6 @@ import adql.query.ADQLObject;
 import adql.query.ADQLOrder;
 import adql.query.ADQLQuery;
 import adql.query.ClauseSelect;
-import adql.query.constraint.In;
 import adql.query.from.ADQLTable;
 import adql.query.operand.ADQLColumn;
 import adql.query.operand.ADQLOperand;
@@ -28,20 +27,20 @@ public class TestIN {
 	private static ADQLTranslator translator = null;
 
 	@BeforeClass
-	public static void setUpBeforeClass(){
+	public static void setUpBeforeClass() {
 		translator = new PostgreSQLTranslator();
 	}
 
 	@Test
-	public void testIN(){
+	public void testIN() {
 		// Test with a simple list of values (here, string constants):
-		In myIn = new In(new ADQLColumn("typeObj"), new ADQLOperand[]{new StringConstant("galaxy"),new StringConstant("star"),new StringConstant("planet"),new StringConstant("nebula")}, true);
+		In myIn = new In(new ADQLColumn("typeObj"), new ADQLOperand[]{ new StringConstant("galaxy"), new StringConstant("star"), new StringConstant("planet"), new StringConstant("nebula") }, true);
 		// check the ADQL:
 		assertEquals("typeObj NOT IN ('galaxy' , 'star' , 'planet' , 'nebula')", myIn.toADQL());
 		// check the SQL translation:
-		try{
+		try {
 			assertEquals(myIn.toADQL(), translator.translate(myIn));
-		}catch(Exception ex){
+		} catch(Exception ex) {
 			ex.printStackTrace();
 			fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!");
 		}
@@ -63,23 +62,23 @@ public class TestIN {
 		// check the ADQL:
 		assertEquals("typeObj NOT IN (SELECT DISTINCT TOP 10 typeObj\nFROM Objects\nORDER BY 1 ASC)", myIn.toADQL());
 		// check the SQL translation:
-		try{
-			assertEquals("typeObj NOT IN (SELECT DISTINCT typeObj AS \"typeObj\"\nFROM Objects\nORDER BY 1 ASC\nLimit 10)", translator.translate(myIn));
-		}catch(Exception ex){
+		try {
+			assertEquals("typeObj NOT IN (SELECT DISTINCT typeObj AS \"typeObj\"\nFROM Objects\nORDER BY 1 ASC\nLIMIT 10)", translator.translate(myIn));
+		} catch(Exception ex) {
 			ex.printStackTrace();
 			fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!");
 		}
 
 		// Test after replacement inside this IN statement:
-		IReplaceHandler sHandler = new SimpleReplaceHandler(true){
+		IReplaceHandler sHandler = new SimpleReplaceHandler(true) {
 
 			@Override
-			public boolean match(ADQLObject obj){
+			public boolean match(ADQLObject obj) {
 				return (obj instanceof ADQLColumn) && ((ADQLColumn)obj).getColumnName().equals("typeObj");
 			}
 
 			@Override
-			public ADQLObject getReplacer(ADQLObject objToReplace){
+			public ADQLObject getReplacer(ADQLObject objToReplace) {
 				return new ADQLColumn("type");
 			}
 		};
diff --git a/test/adql/translator/TestJDBCTranslator.java b/test/adql/translator/TestJDBCTranslator.java
index 1df654f5a8f272eef7dc7e88fe536cbdd817edc5..0d11e374da834278c0d823ae394e3b60d41fb2cd 100644
--- a/test/adql/translator/TestJDBCTranslator.java
+++ b/test/adql/translator/TestJDBCTranslator.java
@@ -9,6 +9,8 @@ import org.junit.Test;
 import adql.db.DBType;
 import adql.db.FunctionDef;
 import adql.db.STCS.Region;
+import adql.parser.ADQLParser;
+import adql.parser.ADQLParser.ADQLVersion;
 import adql.parser.grammar.ParseException;
 import adql.query.IdentifierField;
 import adql.query.operand.ADQLColumn;
@@ -34,6 +36,34 @@ public class TestJDBCTranslator {
 	public void setUp() throws Exception {
 	}
 
+	@Test
+	public void testTranslateOffset() {
+		JDBCTranslator tr = new AJDBCTranslator();
+		ADQLParser parser = new ADQLParser(ADQLVersion.V2_1);
+
+		try {
+
+			// CASE: Only OFFSET
+			assertEquals("SELECT *\nFROM foo\nOFFSET 10", tr.translate(parser.parseQuery("Select * From foo OffSet 10")));
+
+			// CASE: Only OFFSET = 0
+			assertEquals("SELECT *\nFROM foo\nOFFSET 0", tr.translate(parser.parseQuery("Select * From foo OffSet 0")));
+
+			// CASE: TOP + OFFSET
+			assertEquals("SELECT *\nFROM foo\nLIMIT 5\nOFFSET 10", tr.translate(parser.parseQuery("Select Top 5 * From foo OffSet 10")));
+
+			// CASE: TOP + ORDER BY + OFFSET
+			assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nLIMIT 5\nOFFSET 10", tr.translate(parser.parseQuery("Select Top 5 * From foo Order By id Asc OffSet 10")));
+
+		} catch(ParseException pe) {
+			pe.printStackTrace(System.err);
+			fail("Unexpected failed query parsing! (see console for more details)");
+		} catch(Exception e) {
+			e.printStackTrace(System.err);
+			fail("There should have been no problem to translate a query with offset into SQL.");
+		}
+	}
+
 	@Test
 	public void testTranslateStringConstant() {
 		JDBCTranslator tr = new AJDBCTranslator();
diff --git a/test/adql/translator/TestSQLServerTranslator.java b/test/adql/translator/TestSQLServerTranslator.java
index 7a523eed7338dde0de1cdb46892114a8db8a94ca..f8eda775f0bb598364a584d7052885bfa616d11a 100644
--- a/test/adql/translator/TestSQLServerTranslator.java
+++ b/test/adql/translator/TestSQLServerTranslator.java
@@ -14,6 +14,7 @@ import adql.db.DBTable;
 import adql.db.DefaultDBColumn;
 import adql.db.DefaultDBTable;
 import adql.parser.ADQLParser;
+import adql.parser.ADQLParser.ADQLVersion;
 import adql.parser.SQLServer_ADQLQueryFactory;
 import adql.parser.grammar.ParseException;
 import adql.query.ADQLQuery;
@@ -37,6 +38,34 @@ public class TestSQLServerTranslator {
 		tables.add(t);
 	}
 
+	@Test
+	public void testTranslateOffset() {
+		SQLServerTranslator tr = new SQLServerTranslator();
+		ADQLParser parser = new ADQLParser(ADQLVersion.V2_1);
+
+		try {
+
+			// CASE: Only OFFSET
+			assertEquals("SELECT *\nFROM foo\nOFFSET 10 ROWS", tr.translate(parser.parseQuery("Select * From foo OffSet 10")));
+
+			// CASE: Only OFFSET = 0
+			assertEquals("SELECT *\nFROM foo\nOFFSET 0 ROWS", tr.translate(parser.parseQuery("Select * From foo OffSet 0")));
+
+			// CASE: TOP + OFFSET
+			assertEquals("SELECT *\nFROM foo\nOFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY", tr.translate(parser.parseQuery("Select Top 5 * From foo OffSet 10")));
+
+			// CASE: TOP + ORDER BY + OFFSET
+			assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nOFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY", tr.translate(parser.parseQuery("Select Top 5 * From foo Order By id Asc OffSet 10")));
+
+		} catch(ParseException pe) {
+			pe.printStackTrace(System.err);
+			fail("Unexpected failed query parsing! (see console for more details)");
+		} catch(Exception e) {
+			e.printStackTrace(System.err);
+			fail("There should have been no problem to translate a query with offset into SQL.");
+		}
+	}
+
 	@Test
 	public void testNaturalJoin() {
 		final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;";