lowdata 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/rubocop.yml +14 -0
- data/lib/data_expression/source.rb +27 -0
- data/lib/data_expression/writes.rb +45 -0
- data/lib/data_expression.rb +31 -14
- data/lib/filter_compiler.rb +12 -0
- data/lib/inversion.rb +42 -11
- data/lib/lowdata.rb +12 -2
- data/lib/nested_join.rb +82 -0
- data/lib/nesting.rb +83 -20
- data/lib/nesting_path.rb +108 -0
- data/lib/nesting_tree.rb +57 -72
- data/lib/order_compiler.rb +3 -1
- data/lib/query_compiler.rb +58 -25
- data/lib/rows.rb +3 -3
- data/lib/schema.rb +25 -4
- data/lib/table_reference/writes.rb +70 -0
- data/lib/table_reference.rb +61 -14
- data/lib/version.rb +1 -1
- data/lib/write_checks.rb +78 -0
- data/lib/writer.rb +155 -0
- metadata +11 -3
data/lib/table_reference.rb
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class LowData
|
|
4
|
-
# A table named by a constant, such as `Users
|
|
4
|
+
# A table named by a constant, such as `Users`. Square brackets choose its columns, `Users[:username]`, and on its own
|
|
5
|
+
# it's every column. See ADR 0006.
|
|
5
6
|
# An inverted reference is `Table`, standing in for the repository's table and any table related to it.
|
|
6
7
|
# A reference can carry filters from round brackets, `Posts(published: true)`, and be negated with `~Posts`.
|
|
7
8
|
class TableReference
|
|
8
9
|
FILTER_KEYWORD_MESSAGE = 'Filters go in round brackets, e.g. Posts(published: true)[:title], not %<keywords>s'
|
|
10
|
+
BARE_TABLE_MESSAGE = 'Table joins the tables that own the columns it selects, so name them, e.g. Table[:title, :username]. ' \
|
|
11
|
+
"For every column of the repository's table, name the table, e.g. Posts"
|
|
12
|
+
OPERATORS = %i[+ & < > <=> ^ * /].freeze
|
|
13
|
+
|
|
14
|
+
# A bare table on its own is every column, as a data expression: `Users[:username] + Posts`.
|
|
15
|
+
def self.expression(value) = value.is_a?(TableReference) ? value.to_expression : value
|
|
9
16
|
|
|
10
17
|
attr_reader :table, :filter, :negated
|
|
11
18
|
|
|
@@ -17,18 +24,22 @@ class LowData
|
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
# Each argument's type decides its meaning:
|
|
20
|
-
# Posts[:title] # A symbol is a column.
|
|
21
|
-
# Users[:username, Posts[:title]] # A data expression is a nested table
|
|
27
|
+
# Posts[:title] # A symbol is a column. Empty brackets select no columns.
|
|
28
|
+
# Users[:username, Posts[:title]] # A data expression is a nested table, and a bare table nests
|
|
29
|
+
# Users[:username, Posts] # every column.
|
|
22
30
|
# Reviews[:rating, author: Users[:username]] # A keyword with a data expression names a nested table.
|
|
31
|
+
# Posts[:id, headline: :title] # A keyword with a symbol renames a column.
|
|
23
32
|
def [](*arguments, **keywords)
|
|
24
|
-
|
|
33
|
+
check_negated!
|
|
25
34
|
return inverted(arguments:, keywords:) if @inverted
|
|
26
35
|
|
|
36
|
+
arguments = arguments.map { |argument| TableReference.expression(argument) }
|
|
27
37
|
expressions, columns = arguments.partition { |argument| argument.is_a?(DataExpression) }
|
|
28
38
|
check_columns!(columns:)
|
|
29
|
-
|
|
39
|
+
named, renames = named(keywords:, columns:)
|
|
40
|
+
nested = nested(pairs: expressions.map { |expression| [nil, expression] } + named)
|
|
30
41
|
|
|
31
|
-
DataExpression.new(node: DataExpression::Source.new(table:, columns:, nested:, filter:))
|
|
42
|
+
DataExpression.new(node: DataExpression::Source.new(table:, columns:, renames:, nested:, filter:))
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
# Each argument's type decides its meaning:
|
|
@@ -37,8 +48,9 @@ class LowData
|
|
|
37
48
|
# Users(Posts), Users(~Posts) # A table keeps rows with, or without, related rows.
|
|
38
49
|
# Users(Posts => 3..) # A table as a key keeps rows with that many related rows.
|
|
39
50
|
# Reviews(author: Users(username: 'alice')) # A keyword with a table names the foreign key to follow.
|
|
51
|
+
# Posts(:user_id) # A symbol names the foreign key a nested join links by.
|
|
40
52
|
def where(*arguments, **keywords, &block)
|
|
41
|
-
related = arguments.map { |argument| related(key: nil, reference: argument) }
|
|
53
|
+
related = arguments.grep_v(Symbol).map { |argument| related(key: nil, reference: argument) }
|
|
42
54
|
values = {}
|
|
43
55
|
|
|
44
56
|
keywords.each do |key, value|
|
|
@@ -49,7 +61,7 @@ class LowData
|
|
|
49
61
|
end
|
|
50
62
|
|
|
51
63
|
conditions = block ? [Sequel.virtual_row(&block)] : []
|
|
52
|
-
with(filter: filter.merge(DataExpression::Filter.new(values:, conditions:, related:)))
|
|
64
|
+
with(filter: filter.merge(DataExpression::Filter.new(values:, conditions:, related:, foreign_keys: arguments.grep(Symbol))))
|
|
53
65
|
end
|
|
54
66
|
|
|
55
67
|
# `~Posts` keeps rows without related posts.
|
|
@@ -57,12 +69,35 @@ class LowData
|
|
|
57
69
|
with(negated: !negated)
|
|
58
70
|
end
|
|
59
71
|
|
|
72
|
+
# A bare table reads and joins as every column of the table:
|
|
73
|
+
# Posts # SELECT posts.* FROM posts
|
|
74
|
+
# Users[:username] + Posts # Each user with every column of their posts.
|
|
75
|
+
# Posts ^ -Col[:views] * 10 # The 10 most viewed posts.
|
|
76
|
+
def to_expression
|
|
77
|
+
raise ArgumentError, BARE_TABLE_MESSAGE if @inverted
|
|
78
|
+
|
|
79
|
+
check_negated!
|
|
80
|
+
DataExpression.new(node: source)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
OPERATORS.each do |operator|
|
|
84
|
+
define_method(operator) { |other| to_expression.public_send(operator, other) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
%i[to_a sql dataset].each do |name|
|
|
88
|
+
define_method(name) { |**options| to_expression.public_send(name, **options) }
|
|
89
|
+
end
|
|
90
|
+
|
|
60
91
|
def source
|
|
61
|
-
DataExpression::Source.new(table:, columns: [], filter:)
|
|
92
|
+
DataExpression::Source.new(table:, columns: [], filter:, bare: true)
|
|
62
93
|
end
|
|
63
94
|
|
|
64
95
|
private
|
|
65
96
|
|
|
97
|
+
def check_negated!
|
|
98
|
+
raise ArgumentError, "~#{LowData.constant_name(table)} filters by related rows, so it can't select columns" if negated
|
|
99
|
+
end
|
|
100
|
+
|
|
66
101
|
def with(filter: @filter, negated: @negated)
|
|
67
102
|
self.class.new(table:, inverted: @inverted, filter:, negated:)
|
|
68
103
|
end
|
|
@@ -76,11 +111,20 @@ class LowData
|
|
|
76
111
|
DataExpression::Related.new(key:, source: reference.source, count: count || (reference.negated ? 0 : 1..))
|
|
77
112
|
end
|
|
78
113
|
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
# Returns the named nested tables and the renamed columns.
|
|
115
|
+
def named(keywords:, columns:)
|
|
116
|
+
keywords = keywords.transform_values { |value| TableReference.expression(value) }
|
|
117
|
+
named, others = keywords.partition { |_key, value| value.is_a?(DataExpression) }
|
|
118
|
+
renames, filters = others.partition { |_key, value| value.is_a?(Symbol) }
|
|
81
119
|
raise ArgumentError, format(FILTER_KEYWORD_MESSAGE, keywords: filters.to_h) if filters.any?
|
|
82
120
|
|
|
83
|
-
named
|
|
121
|
+
check_renames!(names: columns + named.map(&:first), renames: renames.to_h)
|
|
122
|
+
[named, renames.to_h]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def check_renames!(names:, renames:)
|
|
126
|
+
duplicate = (renames.keys & names).first
|
|
127
|
+
raise ArgumentError, "#{LowData.constant_name(table)}[] returns #{duplicate} twice. Name each value once" if duplicate
|
|
84
128
|
end
|
|
85
129
|
|
|
86
130
|
def nested(pairs:)
|
|
@@ -88,10 +132,13 @@ class LowData
|
|
|
88
132
|
end
|
|
89
133
|
|
|
90
134
|
def inverted(arguments:, keywords:)
|
|
135
|
+
raise ArgumentError, BARE_TABLE_MESSAGE if arguments.empty? && keywords.empty?
|
|
136
|
+
|
|
91
137
|
check_columns!(columns: arguments)
|
|
92
|
-
raise ArgumentError, 'Table[] takes column names only'
|
|
138
|
+
raise ArgumentError, 'Table[] takes column names only, and renames such as headline: :title' unless keywords.values.all?(Symbol)
|
|
93
139
|
|
|
94
|
-
|
|
140
|
+
check_renames!(names: arguments, renames: keywords)
|
|
141
|
+
DataExpression.new(node: DataExpression::Inversion.new(table:, columns: arguments, renames: keywords, filter:))
|
|
95
142
|
end
|
|
96
143
|
|
|
97
144
|
def check_columns!(columns:)
|
data/lib/version.rb
CHANGED
data/lib/write_checks.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LowData
|
|
4
|
+
# Checks a write before it runs, so a mistake raises rather than writing the wrong rows. See Writer.
|
|
5
|
+
module WriteChecks
|
|
6
|
+
ROWS_MESSAGE = '%<operator>s takes a hash or an array of hashes, e.g. Posts %<operator>s { title: \'Hello\' }, not %<rows>s'
|
|
7
|
+
INSERT_FILTER_MESSAGE = 'An insert adds new rows, so it takes no round brackets. To insert a row or update the one ' \
|
|
8
|
+
'matching the round brackets, upsert with <=, e.g. Posts(slug: \'hello\') <= { title: \'Hello\' }'
|
|
9
|
+
UPSERT_FILTER_MESSAGE = 'An upsert matches one row by plain values, e.g. Posts(slug: \'hello\') <= row, so it takes no ' \
|
|
10
|
+
'ranges, lists, blocks or tables in round brackets'
|
|
11
|
+
FOREIGN_KEY_MESSAGE = "A symbol in round brackets names a foreign key for a nested join, so it can't choose rows to %<action>s"
|
|
12
|
+
# `Posts[:id] <= Users[:id]` reads like a comparison or a join, so it says what `<=` does instead.
|
|
13
|
+
EXPRESSION_MESSAGES = {
|
|
14
|
+
'<<' => "<< inserts rows, e.g. Posts << { title: 'Hello' }, not a data expression",
|
|
15
|
+
'<=' => "<= upserts rows, e.g. Posts(slug: 'hello') <= { title: 'Hello' }, so it can't compare or join data expressions. " \
|
|
16
|
+
'Join them with &, <, > or <=>'
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def check_rows!(rows:, operator:)
|
|
22
|
+
return if rows.is_a?(Hash) || (rows.is_a?(Array) && rows.all?(Hash))
|
|
23
|
+
raise ArgumentError, EXPRESSION_MESSAGES.fetch(operator) if rows.is_a?(DataExpression)
|
|
24
|
+
|
|
25
|
+
raise ArgumentError, format(ROWS_MESSAGE, operator:, rows: rows.inspect)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check_insert_filter!(filter:)
|
|
29
|
+
raise ArgumentError, INSERT_FILTER_MESSAGE if chooses_rows?(filter:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Whether the round brackets choose rows. Foreign keys only name how a nested join links.
|
|
33
|
+
def chooses_rows?(filter:)
|
|
34
|
+
filter.values.any? || filter.conditions.any? || filter.related.any?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check_upsert_filter!(filter:)
|
|
38
|
+
plain = filter.values.values.none? { |value| value.is_a?(Range) || value.is_a?(Enumerable) || value.is_a?(Sequel::SQL::Expression) }
|
|
39
|
+
return if plain && filter.conditions.empty? && filter.related.empty? && filter.foreign_keys.empty?
|
|
40
|
+
|
|
41
|
+
raise ArgumentError, UPSERT_FILTER_MESSAGE
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# An update or delete without round brackets would change every row, so it raises. `dataset` can still do that.
|
|
45
|
+
def check_chosen!(source:, action:, example:)
|
|
46
|
+
filter = source.filter
|
|
47
|
+
raise ArgumentError, format(FOREIGN_KEY_MESSAGE, action:) if filter.foreign_keys.any?
|
|
48
|
+
return if chooses_rows?(filter:)
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "Choose the #{source.table} to #{action} with round brackets, e.g. #{example}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# MySQL's ON DUPLICATE KEY UPDATE matches any unique key, so the upsert must match on one, and the row mustn't set
|
|
54
|
+
# every column of another, which could match a different row. An auto-increment id left out of the row can't match.
|
|
55
|
+
def check_mysql_keys!(table:, keys:, row:, target:)
|
|
56
|
+
matched, others = keys.partition { |key| key.sort == target.sort }
|
|
57
|
+
raise Error, "MySQL upserts match on a unique key, and #{target.join(', ')} isn't one of #{table}'s" if matched.empty?
|
|
58
|
+
|
|
59
|
+
clashing = others.find { |key| key.none? { |column| row[column].nil? } }
|
|
60
|
+
return unless clashing
|
|
61
|
+
|
|
62
|
+
raise Error, "MySQL matches an upsert on any unique key, and the row also sets #{clashing.join(', ')}, which could match " \
|
|
63
|
+
"another #{table} row. Leave #{clashing.join(', ')} out of the row, or match on it"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Checked before the write, so a typo doesn't write a row and then fail to return it.
|
|
67
|
+
def check_returned!(source:, columns:)
|
|
68
|
+
missing = (source.columns + source.renames.values) - columns
|
|
69
|
+
raise ArgumentError, "#{source.table} has no #{missing.join(', ')} column to return" if missing.any?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def assignment(values:)
|
|
73
|
+
return "[:#{values.keys.first}] = #{values.values.first.inspect}" if values.one?
|
|
74
|
+
|
|
75
|
+
"[] = #{values.inspect}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/writer.rb
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'write_checks'
|
|
4
|
+
|
|
5
|
+
class LowData
|
|
6
|
+
# Runs writes as soon as they're written, and returns what they wrote. See ADR 0007:
|
|
7
|
+
# Posts << { title: 'Hello' } # INSERT, returning the primary key
|
|
8
|
+
# Posts[:id, :title] << { title: 'Hello' } # INSERT, returning the columns in square brackets
|
|
9
|
+
# Posts(slug: 'hello') <= { title: 'Hello' } # INSERT ... ON CONFLICT (slug) DO UPDATE, returning the primary key
|
|
10
|
+
# Posts(id: 1)[:title] = 'New' # UPDATE ... WHERE (posts.id = 1)
|
|
11
|
+
# Posts(id: 1).delete # DELETE ... WHERE (posts.id = 1), returning the number of rows deleted
|
|
12
|
+
class Writer
|
|
13
|
+
include WriteChecks
|
|
14
|
+
|
|
15
|
+
def initialize(database:)
|
|
16
|
+
raise ConfigError, ConfigError::MISSING_DATABASE if database.nil?
|
|
17
|
+
|
|
18
|
+
@database = database
|
|
19
|
+
@schema = Schema.new(database:)
|
|
20
|
+
@filters = FilterCompiler.new(database:, schema: @schema)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def insert(source:, rows:)
|
|
24
|
+
check_rows!(rows:, operator: '<<')
|
|
25
|
+
check_insert_filter!(filter: source.filter)
|
|
26
|
+
check_returned!(source:, columns: @schema.columns(table: source.table))
|
|
27
|
+
return insert_all(source:, rows:) if source.no_columns? && rows.is_a?(Array) && rows.map(&:keys).uniq.one?
|
|
28
|
+
|
|
29
|
+
each_row(rows:) { |row| insert_row(source:, row:) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The round brackets name the columns to match on, and their values are written too.
|
|
33
|
+
def upsert(source:, rows:)
|
|
34
|
+
check_rows!(rows:, operator: '<=')
|
|
35
|
+
check_upsert_filter!(filter: source.filter)
|
|
36
|
+
check_returned!(source:, columns: @schema.columns(table: source.table))
|
|
37
|
+
|
|
38
|
+
each_row(rows:) { |row| upsert_row(source:, row: source.filter.values.merge(row)) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update(source:, values:)
|
|
42
|
+
check_chosen!(source:, action: 'update', example: "#{constant(source:)}(id: 1)#{assignment(values:)}")
|
|
43
|
+
|
|
44
|
+
chosen(source:).update(values)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(source:)
|
|
48
|
+
check_chosen!(source:, action: 'delete', example: "#{constant(source:)}(id: 1).delete")
|
|
49
|
+
|
|
50
|
+
chosen(source:).delete
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# A hash writes one row and returns a hash, and an array writes each row in a transaction and returns an array.
|
|
56
|
+
def each_row(rows:, &)
|
|
57
|
+
return yield(rows) if rows.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
@database.transaction { rows.map(&) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Empty brackets return nothing, so rows with the same columns go in one INSERT, as nothing needs matching back to
|
|
63
|
+
# them. Otherwise rows insert one by one, because RETURNING doesn't promise the inserted order, and a multi-row
|
|
64
|
+
# INSERT takes its columns from the first row, so a column missing from it would be dropped.
|
|
65
|
+
def insert_all(source:, rows:)
|
|
66
|
+
@database[source.table].multi_insert(rows)
|
|
67
|
+
rows.map { {} }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def insert_row(source:, row:)
|
|
71
|
+
dataset = @database[source.table]
|
|
72
|
+
return dataset.returning(*returned(source:)).insert(row).first if returning?(dataset:, source:)
|
|
73
|
+
|
|
74
|
+
id = dataset.insert(row)
|
|
75
|
+
read_back(source:, keys: inserted_keys(table: source.table, row:, id:))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def upsert_row(source:, row:)
|
|
79
|
+
target = conflict_target(source:, row:)
|
|
80
|
+
return mysql_upsert(source:, row:, target:) if @database.database_type == :mysql
|
|
81
|
+
|
|
82
|
+
dataset = @database[source.table]
|
|
83
|
+
raise Error, "LowData can't upsert on #{@database.database_type}" unless dataset.respond_to?(:insert_conflict)
|
|
84
|
+
|
|
85
|
+
dataset = dataset.insert_conflict(target:, update: updates(row:, target:))
|
|
86
|
+
return dataset.returning(*returned(source:)).insert(row).first if returning?(dataset:, source:)
|
|
87
|
+
|
|
88
|
+
dataset.insert(row)
|
|
89
|
+
read_back(source:, keys: row.slice(*target))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def mysql_upsert(source:, row:, target:)
|
|
93
|
+
check_mysql_keys!(table: source.table, keys: unique_keys(table: source.table), row:, target:)
|
|
94
|
+
@database[source.table].on_duplicate_key_update(*updates(row:, target:).keys).insert(row)
|
|
95
|
+
read_back(source:, keys: row.slice(*target))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def unique_keys(table:)
|
|
99
|
+
indexes = @database.indexes(table).values.select { |index| index[:unique] }.map { |index| index[:columns] }
|
|
100
|
+
([@schema.primary_key(table:)] + indexes).reject(&:empty?)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Without round brackets, an upsert matches on the primary key.
|
|
104
|
+
def conflict_target(source:, row:)
|
|
105
|
+
return source.filter.values.keys if source.filter.values.any?
|
|
106
|
+
|
|
107
|
+
primary_key = @schema.primary_key(table: source.table)
|
|
108
|
+
missing = primary_key - row.keys
|
|
109
|
+
return primary_key if primary_key.any? && missing.empty?
|
|
110
|
+
|
|
111
|
+
raise ArgumentError, "#{constant(source:)} <= row matches on the primary key, so the row needs #{missing.join(', ')}. " \
|
|
112
|
+
"Or name the columns to match in round brackets, e.g. #{constant(source:)}(slug: 'hello') <= row"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A row of only the matched columns still updates one of them, so the database returns the row rather than nothing.
|
|
116
|
+
def updates(row:, target:)
|
|
117
|
+
columns = row.keys - target
|
|
118
|
+
(columns.empty? ? [target.first] : columns).to_h { |column| [column, Sequel[:excluded][column]] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Square brackets choose the columns returned, and a bare table returns its primary key, or every column without one.
|
|
122
|
+
def returned(source:)
|
|
123
|
+
return @schema.primary_key(table: source.table) if source.every_column?
|
|
124
|
+
|
|
125
|
+
source.columns + source.renames.map { |name, column| Sequel.as(column, name) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Empty brackets return no columns, and databases without RETURNING read the row back by its keys.
|
|
129
|
+
def read_back(source:, keys:)
|
|
130
|
+
return {} if source.no_columns?
|
|
131
|
+
|
|
132
|
+
@database[source.table].where(keys).select(*returned(source:)).first
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def returning?(dataset:, source:)
|
|
136
|
+
dataset.supports_returning?(:insert) && !source.no_columns?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def inserted_keys(table:, row:, id:)
|
|
140
|
+
primary_key = @schema.primary_key(table:)
|
|
141
|
+
return row if primary_key.empty?
|
|
142
|
+
return { primary_key.first => id } if primary_key.one? && !row.key?(primary_key.first)
|
|
143
|
+
|
|
144
|
+
row.slice(*primary_key)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def chosen(source:)
|
|
148
|
+
@filters.apply(dataset: @database[source.table], source:)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def constant(source:)
|
|
152
|
+
LowData.constant_name(source.table)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lowdata
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.4'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0.
|
|
25
|
+
version: '0.4'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: lowtype
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,20 +58,28 @@ executables: []
|
|
|
58
58
|
extensions: []
|
|
59
59
|
extra_rdoc_files: []
|
|
60
60
|
files:
|
|
61
|
+
- config/rubocop.yml
|
|
61
62
|
- lib/col.rb
|
|
62
63
|
- lib/data_expression.rb
|
|
64
|
+
- lib/data_expression/source.rb
|
|
65
|
+
- lib/data_expression/writes.rb
|
|
63
66
|
- lib/errors.rb
|
|
64
67
|
- lib/filter_compiler.rb
|
|
65
68
|
- lib/inversion.rb
|
|
66
69
|
- lib/lowdata.rb
|
|
70
|
+
- lib/nested_join.rb
|
|
67
71
|
- lib/nesting.rb
|
|
72
|
+
- lib/nesting_path.rb
|
|
68
73
|
- lib/nesting_tree.rb
|
|
69
74
|
- lib/order_compiler.rb
|
|
70
75
|
- lib/query_compiler.rb
|
|
71
76
|
- lib/rows.rb
|
|
72
77
|
- lib/schema.rb
|
|
73
78
|
- lib/table_reference.rb
|
|
79
|
+
- lib/table_reference/writes.rb
|
|
74
80
|
- lib/version.rb
|
|
81
|
+
- lib/write_checks.rb
|
|
82
|
+
- lib/writer.rb
|
|
75
83
|
homepage: https://github.com/low-rb/lowdata
|
|
76
84
|
licenses: []
|
|
77
85
|
metadata:
|