Skip to content

Latest commit

 

History

History
380 lines (287 loc) · 11.8 KB

queryset.md

File metadata and controls

380 lines (287 loc) · 11.8 KB

QuerySets

QuerySets represent the construction of a query. A queryset is immutable — all methods of the queryset will return a new queryset instance. The queryset will not execute until an invoking method is called. QuerySets return two potential things:

  • streams
  • promises

The rule of thumb is:

  • consume a stream when returning N rows,
  • and consume a promise when returning a finite number of rows.

QuerySet<Model>#all()

  • Returns: QuerySet<Model>

Return a queryset representing all rows of the backing table. Creating an all() queryset can be useful in situations where one iteratively builds up a query by passing the queryset between several APIs.

Example:

const onlyTheBest = require('./only-the-best-filter')
const onlyConsonants = require('./only-consonants')

// get the best consonant letters
let myLetters = LetterObjects.all()
myLetters = onlyTheBest(onlyConsonants(myLetters))

// get the best letters from the first half of the alphabet
let firstLetters = LetterObjects.all().slice(0, 13)
firstLetters = onlyTheBest(firstLetters)

QuerySet<Model>#none()

  • Returns: QuerySet<Model>

Return a queryset representing none of the rows of the backing table. This can be useful when you wish to modify an incoming queryset such that no rows will be returned.

Example:

function whatThingsCanThisUserSee (user, queryset) {
  if (!user.isStaff()) {
    // if they're not staff, eliminate all results from
    // the queryset!
    return queryset.none()
  }
  return queryset
}

QuerySet#get(Clause)

Return a promise of a single row representation, throwing an error if zero or more than one rows are represented in the result.

QuerySet#filter(Clause)

  • Returns: QuerySet<Model>

Return a new queryset representing a set of rows where Clause is true, in addition to all previously added Clauses. See Clause for more info on the operations available in a where clause.

QuerySet#exclude(Clause)

  • Returns: QuerySet<Model>

The antithesis of filter — instead of including rows where Clause is true, include only rows where Clause is false.

QuerySet#create(Data)

QuerySet#update(Data)

QuerySet#delete(Clause)

QuerySet#count()

QuerySet#slice([start][, end])

  • Argument: start — an integer offset into the results of the query
  • Argument: end — an integer offset into the results of the query.
  • Returns: QuerySet<Model>

This method bounds the query relative to previous bounds, if any, or absolutely if no previous bounds exist.

QuerySet#order(order)

  • Argument: order — may be a String or Array<String> representing the fields by which to order the query.
  • Returns: QuerySet<Model>

The column format accepted by order() is the same as Clause, with the addition of an optional prefixed '-' to indicate "order by column descending."

// order primarily by the owner fk's name, descending, then
// secondarily by publish date
packageDAO.order(['-owner.name', 'published'])

QuerySet#values(values)

  • Argument: values — may be a String or Array<String> representing the fields to contribute to outgoing stream objects.
  • Returns: QuerySet<Model>

The .values method disables the default object mapping that ormnomnom does. Instead, plain objects will be emitted, only containing the keys specified by values.

userDAO.values('id', 'username').createStream().on('data', console.log)
// results are [{id: 1, username: 'bloop'}, {id: 3, username: 'jonbonjovi'}]

QuerySet#valuesList(values)

  • Argument: values — may be a String or Array<String> representing the fields to output directly into the stream.
  • Returns: QuerySet<Model>

.valuesList operations like .values, but returns the columns directly into the output stream instead of associating them with an object first. This is super handy for, e.g., generating in queries:

QuerySet#distinct(columns)

  • Argument: columns — may be a String or Array<String> representing the columns to consider when deduplicating. Defaults to "id".
  • Returns: QuerySet<Model>

Returns a queryset that will run SELECT DISTINCT ON (columns) instead of SELECT.

Only applies to SELECT operations.

PackageData.objects.filter('owner_id:in', UserData.filter({
  'name:startsWith': 'bulletproo'
}).valuesList('id'))

QuerySet#sql → Promise<String>

Returns a string holding the potential SQL query that this queryset represents.

Clauses

Clauses may be objects or arrays of objects. Pairs of keys and values in clause objects represent a column and its relation to a value.

{ 'columnSpec<Model>[:relation]': value }

// examples:
{ 'title': 'exact match' }
{ 'title:contains': 'of the ' }
{ 'author.name:startsWith': 'Gary' }
{ 'book.author.published:lt': new Date(2014, 1, 1) }

A columnSpec may be any column named to the DAO when it was created. Additionally, it may be a foreignKeyName.columnSpec, where the columnSpec now refers to the target table.

Clause objects (and arrays) may contain promises. They will be settled before the query is executed. If an error is thrown by a clause item, it will be bubbled through the query:

bookDAO.filter({
  title: new Promise((resolve, reject) => reject(new Error('oh no')))
}).catch(err => {
  console.log(err.message)  // oh no
})

Clause Relations

The built in relations are as follows:

Relation Name Description Effect on validation
(nothing) Omitted relations are treated as "eq". Applied from DAO DDL
eq Column should equal value. Applied from DAO DDL
neq Column should not equal value. Applied from DAO DDL
contains Column should contain string. Must be string
iContains Column should contain string, case-insensitive. Must be string
startsWith Column should start with string. Must be string
iStartsWith Column should start with string, case-insensitive. Must be string
endsWith Column should end with string. Must be string
iEndsWith Column should end with string, case-insensitive. Must be string
regex Column should match regular expression. Must be string
in Column should be one of the provided values. DAO DDL applied to each array element
notIn Column should not be any of the provided values. DAO DDL applied to each array element
isNull Presence of NULL in column should match value. Must be boolean
lt Column must be less than value. Must be string or date
gt Column must be greater than value. Must be string or date
lte Column must be less than or equal to value. Must be string or date
gte Column must be greater than or equal to value. Must be string or date
raw User-defined. No validation.

raw may be used for missing operations. An example:

bookDAO.filter({
  'name:raw' (column, addValue) {
    return `char_length(${column}) = $${addValue(100)}`
  }
})

Aggregation, Grouping, and Annotation

As of v3.0.0, ORMnomnom supports grouping, aggregates, and annotations. We'll use the following models for all examples below:

const orm = require('ormnomnom')

class Invoice {
  constructor ({id, name, date}) {
    this.id = id
    this.name = name
    this.date = date
  }
}

Invoice.objects = orm(Invoice, {
  id: { type: 'number' },
  name: { type: 'string' },
  date: { type: 'string', format: 'date-time' }
})

class LineItem {
  constructor ({id, subtotal, discount, invoice_id, invoice}) {
    this.id = id
    this.subtotal = subtotal
    this.discount = discount
    this.invoice_id = invoice_id
    this.invoice = invoice
  }
}

LineItem.objects = orm(LineItem, {
  id: { type: 'number' },
  invoice: orm.fk(Invoice),
  subtotal: { anyOf: [{type: 'null'}, {type: 'number}], default: null },
  discount: { anyOf: [{type: 'null'}, {type: 'number}], default: null }
})

Annotations are extra data that you wish to include in your query, derived from the columns of the queryset. For example, you could "annotate" a query with the sum of two fields:

LineItem.objects.all().annotate({
  total (ref) {
    return `${ref('subtotal')} - ${ref('discount')}`
  }
}).then(results => {
  results.map(([lineItem, annotation]) => {
    console.log(
      '%s - %s = %s',
      lineItem.subtotal,
      lineItem.discount,
      annotation.total
    )
  })
})

Aggregations are extra data derived across the entire set of rows represented by a query. For example, QuerySet#count is implemented as an aggregate. The only result of an aggregation query is the aggregated value itself.

Invoice.objects.all().aggregate(
  ref => `array_agg(distinct ${ref('date')})`
).then(dates => {
  // what dates are available?
})

Grouping is handy for collapsing foreign relations into a set of items, especially when combined with Aggregation:

Invoice.objects.all().group().annotate({
  lineItems: ref => `json_agg(${ref('line_items.*')})`
}).then(results => {
  results.map(([invoice, {lineItems}]) => {
    // invoice will be an Invoice object,
    // lineItems will be an Array of Objects suitable for passing to `new LineItem`
  })
})

QuerySet#aggregate(fn)

fn should be of the form:

const fn = (ref, push) => {
  return `count(*)`
}

When called on a QuerySet, the queryset will be modified to select a single column, the result of the aggregate fn. Only the first row will be returned.

QuerySet#connection(connection)

When running the query, use the provided Postgres Client instead of asking for a global postgres client. Does not return the client to the pool after use.

const client = pg.connect()
await MyModel.objects.connection(client).filter({blah: 'whatever'})

QuerySet#annotate(obj)

obj should be of the form:

const obj = {
  nameOfField: (ref, push) {
    return 'SQL()'
  }
}

Each key of obj should refer to an annotation function.

Each annotation function should return a string and take two arguments, ref and push.

  • ref is used to refer to target columns: ref('path.to.column') or ref('fk.*') both work.
  • push is used to pass values from JS to SQL, like :raw.

Every key in obj will appear in the annotations of the queryset.

If values or valuesList are given, the keys of annotation object will appear in-line if they are selected. Otherwise, the queryset will return an array of [ModelInstance, Object] for each matching row.

QuerySet#group(by)

Groups a query by a set of columns. If by is not given, it will be set to the primary key of the QuerySet's target table, if any. by may be given as a string or an array of strings representing column lookups.

When group is called: the columns selected by the queryset will be limited to:

  • If valuesList or values was called on the queryset, the column selection will not be affected by calls to group().
  • Otherwise, if by includes the primary key of the target table, all of the target table's columns and annotations.