Class: Tina4::DatabaseResult

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/tina4/database_result.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(records = [], sql: "", columns: [], count: nil, limit: 0, offset: 0, affected_rows: 0, last_id: nil, error: nil, db: nil) ⇒ DatabaseResult

limit is the row cap ACTUALLY APPLIED to the statement that produced these records — what Database#fetch appended — and 0 means none was (an explicit no-limit read, SQL carrying its own trailing LIMIT, or a write). The default used to be 10: Database#fetch_direct never passed limit:/offset:, so that untouched default was reported on EVERY fetch whatever limit ran — and 10 is the stale v2 documentation number that the buried PHP row-cap test also asserted as the cap. 0 says "no cap recorded" instead of quietly naming a number nothing applied.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/tina4/database_result.rb', line 19

def initialize(records = [], sql: "", columns: [], count: nil, limit: 0, offset: 0,
               affected_rows: 0, last_id: nil, error: nil, db: nil)
  @records = records || []
  @sql = sql
  @columns = columns.empty? && !@records.empty? ? @records.first.keys : columns
  @count = count || @records.length
  @limit = limit
  @offset = offset
  @affected_rows = affected_rows
  @last_id = last_id
  @error = error
  @db = db
  @column_info_cache = nil
end

Instance Attribute Details

#affected_rowsObject (readonly)

Returns the value of attribute affected_rows.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def affected_rows
  @affected_rows
end

#columnsObject (readonly)

Returns the value of attribute columns.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def columns
  @columns
end

#countObject (readonly)

Returns the value of attribute count.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def count
  @count
end

#errorObject (readonly)

Returns the value of attribute error.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def error
  @error
end

#last_idObject (readonly)

Returns the value of attribute last_id.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def last_id
  @last_id
end

#limitObject (readonly)

Returns the value of attribute limit.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def limit
  @limit
end

#offsetObject (readonly)

Returns the value of attribute offset.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def offset
  @offset
end

#recordsObject (readonly)

Returns the value of attribute records.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def records
  @records
end

#sqlObject (readonly)

Returns the value of attribute sql.



8
9
10
# File 'lib/tina4/database_result.rb', line 8

def sql
  @sql
end

Instance Method Details

#[](*args) ⇒ Object

Index / slice access into the result rows.

result[0] is documented (book ch5 §4 "Index Access"). Delegating straight to the materialised rows means every Array subscript form works — result[0], result[-1], result[1, 2] and result[1..3] — and matches Python's DatabaseResult.__getitem__, which forwards to its records list.



57
58
59
# File 'lib/tina4/database_result.rb', line 57

def [](*args)
  @records[*args]
end

#column_infoObject

Return column metadata for the query's table.

Lazy — only queries the database when explicitly called. Caches the result so subsequent calls return immediately without re-querying.

Returns an array of hashes with keys:

name, type, size, decimals, nullable, primary_key


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/tina4/database_result.rb', line 171

def column_info
  return @column_info_cache if @column_info_cache

  table = extract_table_from_sql

  if @db && table
    begin
      @column_info_cache = (table)
      return @column_info_cache
    rescue StandardError
      # Fall through to fallback
    end
  end

  @column_info_cache = fallback_column_info
  @column_info_cache
end

#each(&block) ⇒ Object



34
35
36
# File 'lib/tina4/database_result.rb', line 34

def each(&block)
  @records.each(&block)
end

#empty?Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/tina4/database_result.rb', line 46

def empty?
  @records.empty?
end

#firstObject



38
39
40
# File 'lib/tina4/database_result.rb', line 38

def first
  @records.first
end

#lastObject



42
43
44
# File 'lib/tina4/database_result.rb', line 42

def last
  @records.last
end

#lengthObject



67
68
69
# File 'lib/tina4/database_result.rb', line 67

def length
  @count
end

#sizeObject



71
72
73
# File 'lib/tina4/database_result.rb', line 71

def size
  @count
end

#success?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'lib/tina4/database_result.rb', line 75

def success?
  @error.nil?
end

#to_arrayObject Also known as: to_a



79
80
81
82
83
# File 'lib/tina4/database_result.rb', line 79

def to_array
  @records.map do |record|
    record.is_a?(Hash) ? record : record.to_h
  end
end

#to_aryObject

Implicit array coercion, so a DatabaseResult can be splatted and used anywhere an Array is expected (a, b = result, [*result]).



63
64
65
# File 'lib/tina4/database_result.rb', line 63

def to_ary
  @records.dup
end

#to_crud(table_name: "data", primary_key: "id", editable: true) ⇒ Object



159
160
161
162
# File 'lib/tina4/database_result.rb', line 159

def to_crud(table_name: "data", primary_key: "id", editable: true)
  Tina4::Crud.generate_table(@records, table_name: table_name,
                              primary_key: primary_key, editable: editable)
end

#to_csv(separator: ",", headers: true) ⇒ Object



91
92
93
94
95
96
97
98
99
100
# File 'lib/tina4/database_result.rb', line 91

def to_csv(separator: ",", headers: true)
  return "" if @records.empty?
  lines = []
  cols = @records.first.keys
  lines << cols.join(separator) if headers
  @records.each do |row|
    lines << cols.map { |c| escape_csv(row[c], separator) }.join(separator)
  end
  lines.join("\n")
end

#to_json(*_args) ⇒ Object



87
88
89
# File 'lib/tina4/database_result.rb', line 87

def to_json(*_args)
  JSON.generate(to_array)
end

#to_paginate(*args, **kwargs) ⇒ Object

Describe the page this result already IS. Takes NO arguments (ADR-0043).

Every field is derived from the query that produced this result:

per_page    = the query's limit
page        = floor(offset / limit) + 1
total       = the true total for the filter (@count, from the COUNT probe
            Database#fetch runs when it applied the limit), NEVER the
            number of rows this page returned
total_pages = ceil(total / per_page)
records     = the rows the query returned, VERBATIM, never re-sliced
limit/offset= the SQL limit/offset actually applied

The envelope is EXACTLY seven snake_case keys, identical in all four frameworks: records, total, page, per_page, total_pages, limit, offset. The old duplicate/camelCase spellings (data, count, totalPages, has_next, has_prev) are gone - a JSON key is data and does not change spelling by host language, so the same integer never ships twice under two names.

PASSING ANY ARGUMENT RAISES. A DatabaseResult holds no connection, so an argument could only re-slice rows already in memory and then report total_pages for pages it can never reach. To read page N, FETCH page N (limit + offset) and call this with no arguments. The removed page:/per_page: slicing mode is a hard error, never a silent reinterpretation - superseding the in-memory slice GitHub #106 asked for.

MEASURED 2026-08-05 on a real 250-row table read with limit=20 offset=40 (page 3 of 13): the old two-mode method re-sliced @records by the ABSOLUTE offset (40) against an array already only 20 long and returned ZERO records for a valid page, while still shipping a page number and total - an envelope that looked authoritative and was empty.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/tina4/database_result.rb', line 132

def to_paginate(*args, **kwargs)
  unless args.empty? && kwargs.empty?
    raise ArgumentError,
          "to_paginate takes no arguments (ADR-0043): it describes the page " \
          "this result already IS, derived from the query that produced it. A " \
          "DatabaseResult holds no connection, so an argument could only " \
          "re-slice rows already in memory and lie about total_pages. To read " \
          "another page, FETCH it - fetch(sql, limit: per_page, offset: " \
          "(page - 1) * per_page) - then call to_paginate with no arguments."
  end

  per_page    = @limit.to_i > 0 ? @limit.to_i : @records.size
  page        = per_page > 0 ? (@offset.to_i / per_page) + 1 : 1
  total       = @count
  total_pages = per_page > 0 ? [1, (total.to_f / per_page).ceil].max : 1

  {
    records: @records,
    total: total,
    page: page,
    per_page: per_page,
    total_pages: total_pages,
    limit: per_page,
    offset: @offset.to_i
  }
end