Skip to content

Window Offset Functions (LEAD/LAG/FIRST_VALUE/LAST_VALUE) Not ImplementedΒ #2409

@mathiasrw

Description

@mathiasrw

Summary

Window offset functions LEAD(), LAG(), FIRST_VALUE(), and LAST_VALUE() are not implemented in AlaSQL. These are essential SQL:2003 standard window functions that allow accessing rows relative to the current row within a partition.

Current Behavior

// Attempting to use LEAD():
alasql('SELECT category, amount, LEAD(amount) OVER (PARTITION BY category ORDER BY amount) AS next_amt FROM data');

// Error: alasql.fn.LEAD is not a function

The same error occurs for LAG(), FIRST_VALUE(), and LAST_VALUE().

Expected Behavior

LEAD() - Access Next Row Value

var data = [
  {category: 'A', amount: 10},
  {category: 'A', amount: 20},
  {category: 'A', amount: 30},
  {category: 'B', amount: 40}
];

alasql('SELECT category, amount, LEAD(amount) OVER (PARTITION BY category ORDER BY amount) AS next_amt FROM ?', [data]);

// Expected output:
[
  {category: 'A', amount: 10, next_amt: 20},   // Next in partition
  {category: 'A', amount: 20, next_amt: 30},   // Next in partition
  {category: 'A', amount: 30, next_amt: null}, // Last in partition
  {category: 'B', amount: 40, next_amt: null}  // Last in partition
]

LAG() - Access Previous Row Value

alasql('SELECT category, amount, LAG(amount) OVER (PARTITION BY category ORDER BY amount) AS prev_amt FROM ?', [data]);

// Expected output:
[
  {category: 'A', amount: 10, prev_amt: null}, // First in partition
  {category: 'A', amount: 20, prev_amt: 10},   // Previous in partition
  {category: 'A', amount: 30, prev_amt: 20},   // Previous in partition
  {category: 'B', amount: 40, prev_amt: null}  // First in partition
]

FIRST_VALUE() - Access First Row in Window

alasql('SELECT category, amount, FIRST_VALUE(amount) OVER (PARTITION BY category ORDER BY amount) AS first_amt FROM ?', [data]);

// Expected output:
[
  {category: 'A', amount: 10, first_amt: 10}, // First in partition
  {category: 'A', amount: 20, first_amt: 10}, // First in partition
  {category: 'A', amount: 30, first_amt: 10}, // First in partition
  {category: 'B', amount: 40, first_amt: 40}  // First in partition
]

LAST_VALUE() - Access Last Row in Window

alasql('SELECT category, amount, LAST_VALUE(amount) OVER (PARTITION BY category ORDER BY amount) AS last_amt FROM ?', [data]);

// Expected output (with proper frame specification):
[
  {category: 'A', amount: 10, last_amt: 30}, // Last in partition
  {category: 'A', amount: 20, last_amt: 30}, // Last in partition
  {category: 'A', amount: 30, last_amt: 30}, // Last in partition
  {category: 'B', amount: 40, last_amt: 40}  // Last in partition
]

Use Cases

1. Calculate Period-over-Period Change

// Calculate month-over-month sales change
alasql(`
  SELECT 
    month, 
    sales,
    sales - LAG(sales) OVER (ORDER BY month) AS mom_change,
    (sales - LAG(sales) OVER (ORDER BY month)) / LAG(sales) OVER (ORDER BY month) * 100 AS mom_pct
  FROM monthly_sales
`);

2. Find Next Event Time

// Find time until next customer visit
alasql(`
  SELECT 
    customer_id,
    visit_date,
    LEAD(visit_date) OVER (PARTITION BY customer_id ORDER BY visit_date) AS next_visit,
    DATEDIFF(day, visit_date, LEAD(visit_date) OVER (PARTITION BY customer_id ORDER BY visit_date)) AS days_until_next
  FROM customer_visits
`);

3. Compare with Baseline

// Compare each employee salary to department min/max
alasql(`
  SELECT 
    dept,
    emp_name,
    salary,
    FIRST_VALUE(salary) OVER (PARTITION BY dept ORDER BY salary) AS dept_min,
    LAST_VALUE(salary) OVER (PARTITION BY dept ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS dept_max
  FROM employees
`);

Function Signatures

LEAD(expression [, offset [, default]]) OVER (...)

  • expression: Column or expression to retrieve from the lead row
  • offset: Number of rows forward (default: 1)
  • default: Value to return when lead row doesn't exist (default: NULL)

LAG(expression [, offset [, default]]) OVER (...)

  • expression: Column or expression to retrieve from the lag row
  • offset: Number of rows backward (default: 1)
  • default: Value to return when lag row doesn't exist (default: NULL)

FIRST_VALUE(expression) OVER (...)

  • expression: Column or expression to retrieve from first row in window frame

LAST_VALUE(expression) OVER (...)

  • expression: Column or expression to retrieve from last row in window frame
  • Note: Requires proper frame specification (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)

Implementation Requirements

1. Parser Updates

Add function tokens to src/alasqlparser.jison:

LEAD | LAG | FIRST_VALUE | LAST_VALUE

2. Function Registration

Register functions in src/55functions.js or similar:

stdlib.LEAD = function() { /* placeholder */ };
stdlib.LAG = function() { /* placeholder */ };
stdlib.FIRST_VALUE = function() { /* placeholder */ };
stdlib.LAST_VALUE = function() { /* placeholder */ };

3. Window Function Execution

Implement in src/424select.js or new src/window-functions.js:

  • Detect offset functions during compilation
  • After partitioning and ordering:
    • For LEAD: Access row at current_index + offset
    • For LAG: Access row at current_index - offset
    • For FIRST_VALUE: Access row at partition_start
    • For LAST_VALUE: Access row at partition_end (or frame_end)
  • Handle default values when offset exceeds partition bounds

4. ORDER BY Requirement

These functions require ORDER BY in the OVER clause:

// Should work:
LEAD(amount) OVER (PARTITION BY category ORDER BY date)

// Should error or warn:
LEAD(amount) OVER (PARTITION BY category)  // No ORDER BY

Test Cases

describe('Window Offset Functions', function() {
  var data = [
    {id: 1, category: 'A', value: 100, seq: 1},
    {id: 2, category: 'A', value: 200, seq: 2},
    {id: 3, category: 'A', value: 300, seq: 3},
    {id: 4, category: 'B', value: 150, seq: 1},
    {id: 5, category: 'B', value: 250, seq: 2}
  ];
  
  it('LEAD() with default offset of 1', function(done) {
    var res = alasql(
      'SELECT id, value, LEAD(value) OVER (PARTITION BY category ORDER BY seq) AS next_value FROM ?',
      [data]
    );
    assert.equal(res[0].next_value, 200);
    assert.equal(res[1].next_value, 300);
    assert.equal(res[2].next_value, null);
    done();
  });
  
  it('LAG() with custom offset', function(done) {
    var res = alasql(
      'SELECT id, value, LAG(value, 2) OVER (PARTITION BY category ORDER BY seq) AS prev_value_2 FROM ?',
      [data]
    );
    assert.equal(res[2].prev_value_2, 100); // Row 3 looks back 2 rows to row 1
    done();
  });
  
  it('LEAD() with default value', function(done) {
    var res = alasql(
      'SELECT id, value, LEAD(value, 1, 0) OVER (PARTITION BY category ORDER BY seq) AS next_value FROM ?',
      [data]
    );
    assert.equal(res[2].next_value, 0); // Default value instead of null
    done();
  });
  
  it('FIRST_VALUE() returns first in partition', function(done) {
    var res = alasql(
      'SELECT id, value, FIRST_VALUE(value) OVER (PARTITION BY category ORDER BY seq) AS first FROM ?',
      [data]
    );
    assert.equal(res[0].first, 100);
    assert.equal(res[1].first, 100);
    assert.equal(res[2].first, 100);
    done();
  });
  
  it('LAST_VALUE() with full frame', function(done) {
    var res = alasql(
      'SELECT id, value, LAST_VALUE(value) OVER (PARTITION BY category ORDER BY seq ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last FROM ?',
      [data]
    );
    assert.equal(res[0].last, 300);
    assert.equal(res[1].last, 300);
    assert.equal(res[2].last, 300);
    done();
  });
});

Implementation Priority

Medium-High - These are commonly used window functions in standard SQL and are expected by users familiar with PostgreSQL, SQL Server, and other modern databases.

References

Related Issues

  • Issue #[original] - Request for window functions including LEAD/LAG
  • Window aggregate functions issue (COUNT/MAX/MIN OVER)

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions