velocity-python 0.0.142__py3-none-any.whl → 0.0.153__py3-none-any.whl

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.

Potentially problematic release.


This version of velocity-python might be problematic. Click here for more details.

velocity/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.142"
1
+ __version__ = version = "0.0.153"
2
2
 
3
3
  from . import aws
4
4
  from . import db
velocity/app/orders.py CHANGED
@@ -115,9 +115,7 @@ class Order:
115
115
  for key, default in defaults.items():
116
116
  if key not in target:
117
117
  target[key] = default() if callable(default) else default
118
- elif key == "updated_at":
119
- # Always update updated_at if present
120
- target[key] = default() if callable(default) else default
118
+
121
119
 
122
120
  def _validate(self):
123
121
  self._apply_defaults()
@@ -0,0 +1,192 @@
1
+ """
2
+ Error Handler Mixin for Lambda Handlers.
3
+
4
+ Provides standardized error handling, logging, and notification functionality
5
+ for Lambda handlers.
6
+ """
7
+
8
+ import copy
9
+ import os
10
+ import pprint
11
+ import time
12
+ from abc import ABC, abstractmethod
13
+ from typing import Dict, Any, Optional
14
+
15
+
16
+ class AwsSessionMixin(ABC):
17
+ """
18
+ Mixin class providing standardized error handling for Lambda handlers.
19
+
20
+ Handles error logging to sys_log table, email notifications to administrators,
21
+ and error metrics collection.
22
+ """
23
+
24
+ def handle_standard_error(self, tx, context, exception: Exception, tb_string: str):
25
+ """Handle errors with consistent logging and notification patterns"""
26
+
27
+ # Log to sys_log for centralized logging
28
+ self.log_error_to_system(tx, context, exception, tb_string)
29
+
30
+ # Determine if this error requires notification
31
+ if self._should_notify_error(exception):
32
+ self.send_error_notification(tx, context, exception, tb_string)
33
+
34
+ # Log error metrics for monitoring
35
+ self.log_error_metrics(tx, context, exception)
36
+
37
+ def log_error_to_system(self, tx, context, exception: Exception, tb_string: str):
38
+ """Log error to sys_log table"""
39
+ error_data = {
40
+ "level": "ERROR",
41
+ "message": str(exception),
42
+ "function": f"{self.__class__.__name__}.{context.action()}",
43
+ "traceback": tb_string,
44
+ "exception_type": exception.__class__.__name__,
45
+ "handler_name": self.__class__.__name__,
46
+ "action": context.action(),
47
+ "user_branch": os.environ.get("USER_BRANCH", "Unknown"),
48
+ "function_name": os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "Unknown"),
49
+ "app_name": os.environ.get("ProjectName", "Unknown"),
50
+ "user_agent": "AWS Lambda",
51
+ "device_type": "Lambda",
52
+ "sys_modified_by": "Lambda",
53
+ }
54
+
55
+ # Add user context if available
56
+ try:
57
+ if hasattr(self, 'current_user') and self.current_user:
58
+ error_data["user_email"] = self.current_user.get("email_address")
59
+ except:
60
+ pass
61
+
62
+ tx.table("sys_log").insert(error_data)
63
+
64
+ def send_error_notification(self, tx, context, exception: Exception, tb_string: str):
65
+ """Send error notification email to administrators"""
66
+ try:
67
+ # Import here to avoid circular dependency
68
+ from support.app import helpers
69
+
70
+ environment = os.environ.get('USER_BRANCH', 'Unknown').title()
71
+ function_name = os.environ.get('AWS_LAMBDA_FUNCTION_NAME', 'Unknown')
72
+
73
+ subject = f"{environment} Lambda Error - {function_name}"
74
+
75
+ body = f"""
76
+ Error Details:
77
+ - Handler: {self.__class__.__name__}
78
+ - Action: {context.action()}
79
+ - Exception: {exception.__class__.__name__}
80
+ - Message: {str(exception)}
81
+ - Environment: {environment}
82
+ - Function: {function_name}
83
+
84
+ Full Traceback:
85
+ {tb_string}
86
+
87
+ Request Details:
88
+ {self._get_error_context(context)}
89
+ """
90
+
91
+ sender = self._get_error_notification_sender()
92
+ recipients = self._get_error_notification_recipients()
93
+
94
+ helpers.sendmail(
95
+ tx,
96
+ subject=subject,
97
+ body=body,
98
+ html=None,
99
+ sender=sender,
100
+ recipient=recipients[0],
101
+ cc=recipients[1:] if len(recipients) > 1 else None,
102
+ bcc=None,
103
+ email_settings_id=1001,
104
+ )
105
+ except Exception as email_error:
106
+ print(f"Failed to send error notification email: {email_error}")
107
+
108
+ def _should_notify_error(self, exception: Exception) -> bool:
109
+ """Determine if an error should trigger email notifications"""
110
+ # Don't notify for user authentication errors or validation errors
111
+ non_notification_types = [
112
+ "AuthenticationError",
113
+ "ValidationError",
114
+ "ValueError",
115
+ "AlertError"
116
+ ]
117
+
118
+ exception_name = exception.__class__.__name__
119
+
120
+ # Check for authentication-related exceptions
121
+ if "Authentication" in exception_name or "Auth" in exception_name:
122
+ return False
123
+
124
+ return exception_name not in non_notification_types
125
+
126
+ @abstractmethod
127
+ def _get_error_notification_recipients(self) -> list:
128
+ """
129
+ Get list of email recipients for error notifications.
130
+
131
+ Must be implemented by the handler class.
132
+
133
+ Returns:
134
+ List of email addresses to notify when errors occur
135
+
136
+ Example:
137
+ return ["admin@company.com", "devops@company.com"]
138
+ """
139
+ pass
140
+
141
+ @abstractmethod
142
+ def _get_error_notification_sender(self) -> str:
143
+ """
144
+ Get email sender for error notifications.
145
+
146
+ Must be implemented by the handler class.
147
+
148
+ Returns:
149
+ Email address to use as sender for error notifications
150
+
151
+ Example:
152
+ return "no-reply@company.com"
153
+ """
154
+ pass
155
+
156
+ def _get_error_context(self, context) -> str:
157
+ """Get sanitized request context for error reporting"""
158
+ try:
159
+ postdata = context.postdata()
160
+ sanitized = copy.deepcopy(postdata)
161
+
162
+ # Remove sensitive data
163
+ if "payload" in sanitized and isinstance(sanitized["payload"], dict):
164
+ sanitized["payload"].pop("cognito_user", None)
165
+
166
+ return pprint.pformat(sanitized)
167
+ except:
168
+ return "Unable to retrieve request context"
169
+
170
+ def log_error_metrics(self, tx, context, exception: Exception):
171
+ """Log error metrics for monitoring and alerting"""
172
+ try:
173
+ metrics_data = {
174
+ "metric_type": "error_count",
175
+ "handler_name": self.__class__.__name__,
176
+ "action": context.action(),
177
+ "exception_type": exception.__class__.__name__,
178
+ "environment": os.environ.get("USER_BRANCH", "Unknown"),
179
+ "function_name": os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "Unknown"),
180
+ "timestamp": time.time(),
181
+ "sys_modified_by": "Lambda"
182
+ }
183
+
184
+ # Try to insert into metrics table if it exists
185
+ try:
186
+ tx.table("lambda_metrics").insert(metrics_data)
187
+ except:
188
+ # Metrics table might not exist yet, don't fail error handler
189
+ pass
190
+ except:
191
+ # Don't fail the error handler if metrics logging fails
192
+ pass
velocity/db/core/row.py CHANGED
@@ -44,7 +44,12 @@ class Row:
44
44
  def __setitem__(self, key, val):
45
45
  if key in self.pk:
46
46
  raise Exception("Cannot update a primary key.")
47
- self.table.upsert({key: val}, self.pk)
47
+ if hasattr(self.table, "updins"):
48
+ self.table.updins({key: val}, pk=self.pk)
49
+ elif hasattr(self.table, "upsert"):
50
+ self.table.upsert({key: val}, pk=self.pk)
51
+ else:
52
+ self.table.update({key: val}, pk=self.pk)
48
53
 
49
54
  def __delitem__(self, key):
50
55
  if key in self.pk:
@@ -121,7 +126,12 @@ class Row:
121
126
  if kwds:
122
127
  data.update(kwds)
123
128
  if data:
124
- self.table.upsert(data, self.pk)
129
+ if hasattr(self.table, "updins"):
130
+ self.table.updins(data, pk=self.pk)
131
+ elif hasattr(self.table, "upsert"):
132
+ self.table.upsert(data, pk=self.pk)
133
+ else:
134
+ self.table.update(data, pk=self.pk)
125
135
  return self
126
136
 
127
137
  def __cmp__(self, other):
velocity/db/core/table.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import sqlparse
2
+ from collections.abc import Iterable, Mapping
2
3
  from velocity.db import exceptions
3
4
  from velocity.db.core.row import Row
4
5
  from velocity.db.core.result import Result
@@ -119,6 +120,59 @@ class Table:
119
120
  return sql, vals
120
121
  self.tx.execute(sql, vals, cursor=self.cursor())
121
122
 
123
+ def create_indexes(self, indexes, **kwds):
124
+ """
125
+ Convenience wrapper to create multiple indexes in order.
126
+
127
+ Accepts an iterable of definitions. Each definition may be either:
128
+ - Mapping with a required "columns" entry plus optional "unique",
129
+ "direction", "where", and "lower" keys.
130
+ - A simple sequence/string of columns, in which case defaults apply.
131
+
132
+ When sql_only=True, a list of (sql, params) tuples is returned.
133
+ """
134
+
135
+ if indexes is None:
136
+ return [] if kwds.get("sql_only", False) else None
137
+
138
+ if not isinstance(indexes, Iterable) or isinstance(indexes, (str, bytes)):
139
+ raise TypeError("indexes must be an iterable of index definitions")
140
+
141
+ sql_only = kwds.get("sql_only", False)
142
+ statements = []
143
+
144
+ for definition in indexes:
145
+ if isinstance(definition, Mapping):
146
+ columns = definition.get("columns")
147
+ if not columns:
148
+ raise ValueError("Index definition requires a non-empty 'columns' entry")
149
+ params = {
150
+ "unique": definition.get("unique", False),
151
+ "direction": definition.get("direction"),
152
+ "where": definition.get("where"),
153
+ "lower": definition.get("lower"),
154
+ }
155
+ else:
156
+ columns = definition
157
+ params = {
158
+ "unique": False,
159
+ "direction": None,
160
+ "where": None,
161
+ "lower": None,
162
+ }
163
+
164
+ if isinstance(columns, str):
165
+ columns = columns.split(",")
166
+
167
+ if not columns:
168
+ raise ValueError("Index columns cannot be empty")
169
+
170
+ result = self.create_index(columns, **params, **kwds)
171
+ if sql_only:
172
+ statements.append(result)
173
+
174
+ return statements if sql_only else None
175
+
122
176
  @return_default(None)
123
177
  def drop_index(self, columns, **kwds):
124
178
  """
@@ -177,10 +231,14 @@ class Table:
177
231
  columns = []
178
232
 
179
233
  has_column = "sys_modified_count" in columns
180
- if has_column and not force:
234
+ has_row_column = "sys_modified_row" in columns
235
+
236
+ if has_column and has_row_column and not force:
181
237
  return
182
238
 
183
- sql, vals = self.sql.ensure_sys_modified_count(self.name, has_column=has_column)
239
+ sql, vals = self.sql.ensure_sys_modified_count(
240
+ self.name, has_column=has_column, has_row_column=has_row_column
241
+ )
184
242
  if kwds.get("sql_only", False):
185
243
  return sql, vals
186
244
  self.tx.execute(sql, vals, cursor=self.cursor())
@@ -289,10 +347,10 @@ class Table:
289
347
  return Row(self, result[0]["sys_id"], lock=lock)
290
348
 
291
349
  @return_default(None)
292
- def find(self, where, lock=None, use_where=False):
350
+ def find(self, where, lock=None, use_where=False, raise_if_missing=False):
293
351
  """
294
- Finds a single row matching `where`, or returns None if none found.
295
- Raises DuplicateRowsFoundError if multiple rows match.
352
+ Finds a single row matching `where`, or returns None if none found unless
353
+ ``raise_if_missing`` is True. Raises DuplicateRowsFoundError if multiple rows match.
296
354
  """
297
355
  if where is None:
298
356
  raise Exception("None is not allowed as a primary key.")
@@ -300,6 +358,10 @@ class Table:
300
358
  where = {"sys_id": where}
301
359
  result = self.select("sys_id", where=where, lock=lock).all()
302
360
  if not result:
361
+ if raise_if_missing:
362
+ raise LookupError(
363
+ f"No rows found in `{self.name}` for criteria: {where!r}"
364
+ )
303
365
  return None
304
366
  if len(result) > 1:
305
367
  sql = self.select("sys_id", sql_only=True, where=where, lock=lock)
@@ -487,6 +549,104 @@ class Table:
487
549
  result = self.tx.execute(sql, vals, cursor=self.cursor())
488
550
  return result.cursor.rowcount if result.cursor else 0
489
551
 
552
+ @create_missing
553
+ def update_or_insert(self, update_data, insert_data=None, where=None, pk=None, **kwds):
554
+ """
555
+ Attempts an UPDATE first; if no rows change, performs an INSERT guarded by NOT EXISTS.
556
+
557
+ :param update_data: Mapping of columns to update.
558
+ :param insert_data: Optional mapping used for the INSERT. When omitted, values are
559
+ derived from update_data combined with simple equality predicates
560
+ from ``where`` and primary key values.
561
+ :param where: Criteria for the UPDATE and existence check.
562
+ :param pk: Optional primary key mapping for UPDATE (merged into WHERE) and INSERT.
563
+ :param sql_only: When True, return the SQL/parameter tuples for both phases instead of executing.
564
+ :return: Number of rows affected, or a dict with ``update``/``insert`` entries when sql_only=True.
565
+ """
566
+ sql_only = kwds.get("sql_only", False)
567
+ if not isinstance(update_data, Mapping) or not update_data:
568
+ raise ValueError("update_data must be a non-empty mapping of column-value pairs.")
569
+ if where is None and pk is None:
570
+ raise ValueError("Either where or pk must be provided for update_or_insert.")
571
+
572
+ update_stmt = None
573
+ if sql_only:
574
+ update_stmt = self.update(update_data, where=where, pk=pk, sql_only=True)
575
+ else:
576
+ updated = self.update(update_data, where=where, pk=pk)
577
+ if updated:
578
+ return updated
579
+
580
+ if insert_data is not None:
581
+ if not isinstance(insert_data, Mapping):
582
+ raise ValueError("insert_data must be a mapping when provided.")
583
+ insert_payload = dict(insert_data)
584
+ else:
585
+ insert_payload = dict(update_data)
586
+ if isinstance(where, Mapping):
587
+ for key, val in where.items():
588
+ if not isinstance(key, str):
589
+ continue
590
+ if set("<>!=%").intersection(key):
591
+ continue
592
+ insert_payload.setdefault(key, val)
593
+ if isinstance(pk, Mapping):
594
+ for key, val in pk.items():
595
+ insert_payload.setdefault(key, val)
596
+
597
+ if not insert_payload:
598
+ raise ValueError("Unable to derive insert payload for update_or_insert.")
599
+
600
+ exists_where = None
601
+ if where is not None and pk is not None:
602
+ if isinstance(where, Mapping) and isinstance(pk, Mapping):
603
+ combined = dict(where)
604
+ combined.update(pk)
605
+ exists_where = combined
606
+ else:
607
+ exists_where = where
608
+ elif where is not None:
609
+ exists_where = where
610
+ else:
611
+ exists_where = pk
612
+
613
+ ins_builder = getattr(self.sql, "insnx", None) or getattr(
614
+ self.sql, "insert_if_not_exists", None
615
+ )
616
+ if ins_builder is None:
617
+ raise NotImplementedError(
618
+ "Current SQL dialect does not support insert-if-not-exists operations."
619
+ )
620
+
621
+ sql, vals = ins_builder(self.tx, self.name, insert_payload, exists_where)
622
+ if sql_only:
623
+ return {"update": update_stmt, "insert": (sql, vals)}
624
+ result = self.tx.execute(sql, vals, cursor=self.cursor())
625
+ return result.cursor.rowcount if result.cursor else 0
626
+
627
+ updins = update_or_insert
628
+
629
+ @create_missing
630
+ def insert_if_not_exists(self, data, where=None, **kwds):
631
+ """
632
+ Inserts `data` into the table only if the existence check (`where`) does not match any rows.
633
+
634
+ Usage:
635
+ table.insert_if_not_exists({'key_col': 'k', 'value': 'v'}, where={'key_col': 'k'})
636
+
637
+ :param data: dict of column -> value for insert
638
+ :param where: mapping/list/str used for the EXISTS check; if None primary keys are used and
639
+ must be present in `data`.
640
+ :return: rowcount (0 or 1) or (sql, params) when sql_only=True
641
+ """
642
+ sql, vals = self.sql.insert_if_not_exists(self.tx, self.name, data, where)
643
+ if kwds.get("sql_only", False):
644
+ return sql, vals
645
+ result = self.tx.execute(sql, vals, cursor=self.cursor())
646
+ return result.cursor.rowcount if result.cursor else 0
647
+
648
+ insnx = insert_if_not_exists
649
+
490
650
  upsert = merge
491
651
  indate = merge
492
652
 
@@ -164,17 +164,25 @@ class Transaction:
164
164
  """
165
165
  return Row(self.table(tablename), pk, lock=lock)
166
166
 
167
- def get(self, tablename, where, lock=None):
168
- """
169
- Shortcut to table.get().
170
- """
171
- return self.table(tablename).get(where, lock=lock)
172
-
173
- def find(self, tablename, where, lock=None):
174
- """
175
- Shortcut to table.find().
176
- """
177
- return self.table(tablename).find(where, lock=lock)
167
+ def get(self, tablename, where, lock=None, use_where=False):
168
+ """Shortcut to table.get() with optional ``use_where`` passthrough."""
169
+ return self.table(tablename).get(where, lock=lock, use_where=use_where)
170
+
171
+ def find(
172
+ self,
173
+ tablename,
174
+ where,
175
+ lock=None,
176
+ use_where=False,
177
+ raise_if_missing=False,
178
+ ):
179
+ """Shortcut to table.find() with ``use_where``/``raise_if_missing`` passthrough."""
180
+ return self.table(tablename).find(
181
+ where,
182
+ lock=lock,
183
+ use_where=use_where,
184
+ raise_if_missing=raise_if_missing,
185
+ )
178
186
 
179
187
  def column(self, tablename, colname):
180
188
  """
@@ -450,7 +450,7 @@ END;
450
450
  return "\n".join(statements), tuple()
451
451
 
452
452
  @classmethod
453
- def ensure_sys_modified_count(cls, name):
453
+ def ensure_sys_modified_count(cls, name, has_column=False, has_row_column=False):
454
454
  """Ensure sys_modified_count column and associated triggers exist for the table."""
455
455
  table_identifier = quote(name)
456
456
  base_name = name.split(".")[-1].replace("`", "")
@@ -18,39 +18,6 @@ TableHelper.reserved = reserved_words
18
18
  TableHelper.operators = OPERATORS
19
19
 
20
20
 
21
- def _get_table_helper(tx, table):
22
- """
23
- Utility function to create a TableHelper instance.
24
- Ensures consistent configuration across all SQL methods.
25
- """
26
- return TableHelper(tx, table)
27
-
28
-
29
- def _validate_table_name(table):
30
- """Validate table name format."""
31
- if not table or not isinstance(table, str):
32
- raise ValueError("Table name must be a non-empty string")
33
- # Add more validation as needed
34
- return table.strip()
35
-
36
-
37
- def _handle_predicate_errors(predicates, operation="WHERE"):
38
- """Process a list of predicates with error handling."""
39
- sql_parts = []
40
- vals = []
41
-
42
- for pred, val in predicates:
43
- sql_parts.append(pred)
44
- if val is None:
45
- pass
46
- elif isinstance(val, tuple):
47
- vals.extend(val)
48
- else:
49
- vals.append(val)
50
-
51
- return sql_parts, vals
52
-
53
-
54
21
  system_fields = [
55
22
  "sys_id",
56
23
  "sys_created",
@@ -143,7 +110,7 @@ class SQL(BaseSQLDialect):
143
110
  vals = []
144
111
 
145
112
  # Create table helper instance
146
- th = _get_table_helper(tx, table)
113
+ th = TableHelper(tx, table)
147
114
 
148
115
  # Handle columns and DISTINCT before aliasing
149
116
  if columns is None:
@@ -447,7 +414,7 @@ class SQL(BaseSQLDialect):
447
414
  if not isinstance(data, Mapping) or not data:
448
415
  raise ValueError("data must be a non-empty mapping of column-value pairs.")
449
416
 
450
- th = _get_table_helper(tx, table)
417
+ th = TableHelper(tx, table)
451
418
  set_clauses = []
452
419
  vals = []
453
420
 
@@ -595,51 +562,164 @@ class SQL(BaseSQLDialect):
595
562
 
596
563
  @classmethod
597
564
  def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
565
+ if not isinstance(data, Mapping) or not data:
566
+ raise ValueError("data must be a non-empty mapping of column-value pairs.")
567
+
568
+ table_helper = TableHelper(tx, table)
569
+ data = dict(data) # work with a copy to avoid mutating the caller's dict
570
+
598
571
  if pk is None:
599
572
  pkeys = tx.table(table).primary_keys()
600
573
  if not pkeys:
601
574
  raise ValueError("Primary key required for merge.")
602
- # If there are multiple primary keys, use all of them
603
- if len(pkeys) > 1:
604
- pk = {pk: data[pk] for pk in pkeys}
605
- else:
606
- pk = {pkeys[0]: data[pkeys[0]]}
607
- # Remove primary keys from data; they will be used in the conflict target
608
- data = {k: v for k, v in data.items() if k not in pk}
575
+ missing = [key for key in pkeys if key not in data]
576
+ if missing:
577
+ missing_cols = ", ".join(missing)
578
+ raise ValueError(
579
+ "Primary key values missing from data for merge: "
580
+ f"{missing_cols}. Provide pk=... or include the key values in data."
581
+ )
582
+ pk = {key: data[key] for key in pkeys}
583
+ else:
584
+ pk = dict(pk)
585
+ for key, value in pk.items():
586
+ if key in data and data[key] != value:
587
+ raise ValueError(
588
+ f"Conflicting values for primary key '{key}' between data and pk arguments."
589
+ )
609
590
 
610
- # Create a merged dictionary for insert (data + primary key columns)
611
- full_data = {}
612
- full_data.update(data)
613
- full_data.update(pk)
591
+ insert_data = dict(data)
592
+ insert_data.update(pk)
614
593
 
615
- sql, vals = cls.insert(table, full_data)
616
- sql = [sql]
617
- vals = list(vals) # Convert to a mutable list
594
+ update_data = {k: v for k, v in data.items() if k not in pk}
618
595
 
619
- if on_conflict_do_nothing != on_conflict_update:
620
- sql.append("ON CONFLICT")
621
- sql.append("(")
622
- sql.append(",".join(pk.keys()))
623
- sql.append(")")
624
- sql.append("DO")
625
- if on_conflict_do_nothing:
626
- sql.append("NOTHING")
627
- elif on_conflict_update:
628
- # Call update() with excluded=True to produce the SET clause for the upsert.
629
- sql_update, vals_update = cls.update(tx, table, data, pk, excluded=True)
630
- sql.append(sql_update)
631
- # Use list.extend to add the update values to vals.
632
- vals.extend(vals_update)
633
- else:
596
+ if not update_data and on_conflict_update:
597
+ # Nothing to update, fall back to a no-op on conflict resolution.
598
+ on_conflict_do_nothing = True
599
+ on_conflict_update = False
600
+
601
+ if on_conflict_do_nothing == on_conflict_update:
634
602
  raise Exception(
635
603
  "Update on conflict must have one and only one option to complete on conflict."
636
604
  )
637
605
 
606
+ sql, vals = cls.insert(table, insert_data)
607
+ sql = [sql]
608
+ vals = list(vals) # Convert to a mutable list
609
+
610
+ sql.append("ON CONFLICT")
611
+ conflict_columns = [TableHelper.quote(column) for column in pk.keys()]
612
+ sql.append("(")
613
+ sql.append(", ".join(conflict_columns))
614
+ sql.append(")")
615
+ sql.append("DO")
616
+ if on_conflict_do_nothing:
617
+ sql.append("NOTHING")
618
+ elif on_conflict_update:
619
+ sql_update, vals_update = cls.update(
620
+ tx, table, update_data, pk, excluded=True
621
+ )
622
+ sql.append(sql_update)
623
+ vals.extend(vals_update)
624
+
638
625
  import sqlparse
639
626
 
640
627
  final_sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
641
628
  return final_sql, tuple(vals)
642
629
 
630
+ @classmethod
631
+ def insnx(cls, tx, table, data, where=None):
632
+ """Insert only when the supplied predicate finds no existing row."""
633
+ if not table:
634
+ raise ValueError("Table name is required.")
635
+ if not isinstance(data, Mapping) or not data:
636
+ raise ValueError("data must be a non-empty mapping of column-value pairs.")
637
+
638
+ # Helper used for quoting and foreign key resolution
639
+ th = TableHelper(tx, table)
640
+ quote_helper = TableHelper(None, table)
641
+
642
+ columns_sql = []
643
+ select_parts = []
644
+ vals = []
645
+
646
+ for key, val in data.items():
647
+ columns_sql.append(quote_helper.quote(key.lower()))
648
+ if isinstance(val, str) and len(val) > 2 and val.startswith("@@") and val[2:]:
649
+ select_parts.append(val[2:])
650
+ else:
651
+ select_parts.append("%s")
652
+ vals.append(val)
653
+
654
+ if not select_parts:
655
+ raise ValueError("At least one column is required for insert.")
656
+
657
+ if where is None:
658
+ if tx is None:
659
+ raise ValueError(
660
+ "A transaction context is required when deriving WHERE from primary keys."
661
+ )
662
+ pk_cols = tx.table(table).primary_keys()
663
+ if not pk_cols:
664
+ raise ValueError("Primary key required to derive WHERE clause.")
665
+ missing = [pk for pk in pk_cols if pk not in data]
666
+ if missing:
667
+ raise ValueError(
668
+ "Missing primary key value(s) for insert condition: " + ", ".join(missing)
669
+ )
670
+ where = {pk: data[pk] for pk in pk_cols}
671
+
672
+ where_clauses = []
673
+ where_vals = []
674
+
675
+ if isinstance(where, Mapping):
676
+ compiled = []
677
+ for key, val in where.items():
678
+ compiled.append(th.make_predicate(key, val))
679
+ where = compiled
680
+
681
+ if isinstance(where, str):
682
+ where_clauses.append(where)
683
+ else:
684
+ try:
685
+ for predicate, value in where:
686
+ where_clauses.append(predicate)
687
+ if value is None:
688
+ continue
689
+ if isinstance(value, tuple):
690
+ where_vals.extend(value)
691
+ else:
692
+ where_vals.append(value)
693
+ except (TypeError, ValueError) as exc:
694
+ raise ValueError(
695
+ "Invalid WHERE clause format. Expected mapping, SQL string, or iterable of predicate/value pairs."
696
+ ) from exc
697
+
698
+ vals.extend(where_vals)
699
+
700
+ exists_sql = [
701
+ "SELECT 1 FROM",
702
+ TableHelper.quote(table),
703
+ ]
704
+ if where_clauses:
705
+ exists_sql.append("WHERE " + " AND ".join(where_clauses))
706
+
707
+ sql_parts = [
708
+ "INSERT INTO",
709
+ TableHelper.quote(table),
710
+ f"({','.join(columns_sql)})",
711
+ "SELECT",
712
+ ", ".join(select_parts),
713
+ "WHERE NOT EXISTS (",
714
+ " ".join(exists_sql),
715
+ ")",
716
+ ]
717
+
718
+ final_sql = sqlparse.format(" ".join(sql_parts), reindent=True, keyword_case="upper")
719
+ return final_sql, tuple(vals)
720
+
721
+ insert_if_not_exists = insnx
722
+
643
723
  @classmethod
644
724
  def version(cls):
645
725
  return "select version()", tuple()
@@ -737,6 +817,7 @@ class SQL(BaseSQLDialect):
737
817
  sys_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
738
818
  sys_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
739
819
  sys_modified_by TEXT NOT NULL DEFAULT 'SYSTEM',
820
+ sys_modified_row TIMESTAMP NOT NULL DEFAULT CLOCK_TIMESTAMP(),
740
821
  sys_modified_count INTEGER NOT NULL DEFAULT 0,
741
822
  sys_dirty BOOLEAN NOT NULL DEFAULT FALSE,
742
823
  sys_table TEXT NOT NULL,
@@ -751,8 +832,9 @@ class SQL(BaseSQLDialect):
751
832
  BEGIN
752
833
  IF (TG_OP = 'INSERT') THEN
753
834
  NEW.sys_table := TG_TABLE_NAME;
754
- NEW.sys_created := clock_timestamp();
755
- NEW.sys_modified := clock_timestamp();
835
+ NEW.sys_created := transaction_timestamp();
836
+ NEW.sys_modified := transaction_timestamp();
837
+ NEW.sys_modified_row := clock_timestamp();
756
838
  NEW.sys_modified_count := 0;
757
839
  ELSIF (TG_OP = 'UPDATE') THEN
758
840
  NEW.sys_table := TG_TABLE_NAME;
@@ -764,7 +846,7 @@ class SQL(BaseSQLDialect):
764
846
  ELSE
765
847
  NEW.sys_dirty := TRUE;
766
848
  END IF;
767
- NEW.sys_modified := clock_timestamp();
849
+ NEW.sys_modified := transaction_timestamp();
768
850
  NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
769
851
  END IF;
770
852
  END IF;
@@ -794,28 +876,52 @@ class SQL(BaseSQLDialect):
794
876
  return sql, tuple()
795
877
 
796
878
  @classmethod
797
- def ensure_sys_modified_count(cls, name):
798
- """Return SQL to backfill sys_modified_count and refresh the on_sys_modified trigger."""
879
+ def ensure_sys_modified_count(
880
+ cls, name, has_column=False, has_row_column=False
881
+ ):
882
+ """Return SQL to backfill sys_modified_count/sys_modified_row and refresh the on_sys_modified trigger."""
799
883
  if "." in name:
800
- fqtn = TableHelper.quote(name)
884
+ schema_name, table_name = name.split(".", 1)
801
885
  else:
802
- fqtn = f"public.{TableHelper.quote(name)}"
803
- schema, _ = fqtn.split(".")
804
- trigger_name = f"on_update_row_{fqtn.replace('.', '_')}"
886
+ schema_name = cls.default_schema
887
+ table_name = name
888
+
889
+ schema_identifier = TableHelper.quote(schema_name)
890
+ table_identifier = TableHelper.quote(table_name)
891
+ fqtn = f"{schema_identifier}.{table_identifier}"
892
+
893
+ trigger_name = (
894
+ f"on_update_row_{schema_name}_{table_name}"
895
+ .replace(".", "_")
896
+ .replace('"', "")
897
+ )
898
+ trigger_identifier = TableHelper.quote(trigger_name)
805
899
  column_name = TableHelper.quote("sys_modified_count")
900
+ row_column_name = TableHelper.quote("sys_modified_row")
806
901
 
807
- sql = [
808
- f"ALTER TABLE {fqtn} ADD COLUMN {column_name} INTEGER NOT NULL DEFAULT 0;",
902
+ statements = []
903
+ if not has_column:
904
+ statements.append(
905
+ f"ALTER TABLE {fqtn} ADD COLUMN {column_name} INTEGER NOT NULL DEFAULT 0;"
906
+ )
907
+ if not has_row_column:
908
+ statements.append(
909
+ f"ALTER TABLE {fqtn} ADD COLUMN {row_column_name} TIMESTAMPTZ;"
910
+ )
911
+
912
+ statements.extend([
809
913
  f"UPDATE {fqtn} SET {column_name} = 0 WHERE {column_name} IS NULL;",
914
+ f"UPDATE {fqtn} SET {row_column_name} = COALESCE({row_column_name}, clock_timestamp());",
810
915
  f"""
811
- CREATE OR REPLACE FUNCTION {schema}.on_sys_modified()
916
+ CREATE OR REPLACE FUNCTION {schema_identifier}.on_sys_modified()
812
917
  RETURNS TRIGGER AS
813
918
  $BODY$
814
919
  BEGIN
815
920
  IF (TG_OP = 'INSERT') THEN
816
921
  NEW.sys_table := TG_TABLE_NAME;
817
- NEW.sys_created := clock_timestamp();
818
- NEW.sys_modified := clock_timestamp();
922
+ NEW.sys_created := transaction_timestamp();
923
+ NEW.sys_modified := transaction_timestamp();
924
+ NEW.sys_modified_row := clock_timestamp();
819
925
  NEW.sys_modified_count := 0;
820
926
  ELSIF (TG_OP = 'UPDATE') THEN
821
927
  NEW.sys_table := TG_TABLE_NAME;
@@ -827,7 +933,8 @@ class SQL(BaseSQLDialect):
827
933
  ELSE
828
934
  NEW.sys_dirty := TRUE;
829
935
  END IF;
830
- NEW.sys_modified := clock_timestamp();
936
+ NEW.sys_modified := transaction_timestamp();
937
+ NEW.sys_modified_row := clock_timestamp();
831
938
  NEW.sys_modified_count := COALESCE(OLD.sys_modified_count, 0) + 1;
832
939
  END IF;
833
940
  END IF;
@@ -837,15 +944,17 @@ class SQL(BaseSQLDialect):
837
944
  LANGUAGE plpgsql VOLATILE
838
945
  COST 100;
839
946
  """,
840
- f"DROP TRIGGER IF EXISTS {trigger_name} ON {fqtn};",
947
+ f"DROP TRIGGER IF EXISTS {trigger_identifier} ON {fqtn};",
841
948
  f"""
842
- CREATE TRIGGER {trigger_name}
949
+ CREATE TRIGGER {trigger_identifier}
843
950
  BEFORE INSERT OR UPDATE ON {fqtn}
844
- FOR EACH ROW EXECUTE PROCEDURE {schema}.on_sys_modified();
951
+ FOR EACH ROW EXECUTE PROCEDURE {schema_identifier}.on_sys_modified();
845
952
  """,
846
- ]
953
+ ])
847
954
 
848
- sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
955
+ sql = sqlparse.format(
956
+ " ".join(statements), reindent=True, keyword_case="upper"
957
+ )
849
958
  return sql, tuple()
850
959
 
851
960
  @classmethod
@@ -431,7 +431,7 @@ END;
431
431
  return "\n".join(statements), tuple()
432
432
 
433
433
  @classmethod
434
- def ensure_sys_modified_count(cls, name):
434
+ def ensure_sys_modified_count(cls, name, has_column=False, has_row_column=False):
435
435
  """Ensure sys_modified_count exists for SQLite tables."""
436
436
  table_identifier = quote(name)
437
437
  base_name = name.split(".")[-1].replace('"', "")
@@ -485,7 +485,9 @@ END;
485
485
  return "\n".join(statements), tuple()
486
486
 
487
487
  @classmethod
488
- def ensure_sys_modified_count(cls, name):
488
+ def ensure_sys_modified_count(
489
+ cls, name, has_column=False, has_row_column=False
490
+ ):
489
491
  """Ensure sys_modified_count exists for SQL Server tables along with maintenance triggers."""
490
492
  if "." in name:
491
493
  schema, table_name = name.split(".", 1)
@@ -499,8 +501,13 @@ END;
499
501
  table_name_sql = table_name.replace("'", "''")
500
502
  trigger_prefix = re.sub(r"[^0-9A-Za-z_]+", "_", f"CC_SYS_MOD_{table_name}")
501
503
 
502
- statements = [
503
- f"IF COL_LENGTH(N'{object_name}', 'sys_modified_count') IS NULL BEGIN ALTER TABLE {table_identifier} ADD sys_modified_count INT NOT NULL CONSTRAINT DF_{trigger_prefix}_COUNT DEFAULT (0); END;",
504
+ statements = []
505
+ if not has_column:
506
+ statements.append(
507
+ f"IF COL_LENGTH(N'{object_name}', 'sys_modified_count') IS NULL BEGIN ALTER TABLE {table_identifier} ADD sys_modified_count INT NOT NULL CONSTRAINT DF_{trigger_prefix}_COUNT DEFAULT (0); END;"
508
+ )
509
+
510
+ statements.extend([
504
511
  f"UPDATE {table_identifier} SET sys_modified_count = 0 WHERE sys_modified_count IS NULL;",
505
512
  f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_insert', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_insert;",
506
513
  f"IF OBJECT_ID(N'{schema_identifier}.{trigger_prefix}_update', N'TR') IS NOT NULL DROP TRIGGER {schema_identifier}.{trigger_prefix}_update;",
@@ -539,7 +546,7 @@ BEGIN
539
546
  INNER JOIN deleted AS d ON d.sys_id = i.sys_id;
540
547
  END;
541
548
  """.strip(),
542
- ]
549
+ ])
543
550
 
544
551
  return "\n".join(statements), tuple()
545
552
 
@@ -1,8 +1,11 @@
1
1
  import unittest
2
2
  import decimal
3
+ from types import SimpleNamespace
4
+ from unittest import mock
3
5
  from velocity.db.servers.postgres.sql import SQL
4
6
  from velocity.db.servers.tablehelper import TableHelper
5
7
  from velocity.db.servers.postgres.types import TYPES
8
+ from velocity.db.core.table import Table
6
9
 
7
10
 
8
11
  class MockTx:
@@ -21,6 +24,51 @@ class MockTable:
21
24
  def column(self, column_name):
22
25
  return MockColumn()
23
26
 
27
+ def primary_keys(self):
28
+ return ["id"]
29
+
30
+
31
+ class DummyCursor:
32
+ def __init__(self, rowcount=0):
33
+ self.rowcount = rowcount
34
+
35
+
36
+ class DummyResult:
37
+ def __init__(self, rowcount=0):
38
+ self.cursor = DummyCursor(rowcount)
39
+
40
+
41
+ class DummyTx:
42
+ def __init__(self):
43
+ self.engine = SimpleNamespace(sql=SimpleNamespace(), schema_locked=False)
44
+ self.executed = []
45
+ self.next_results = []
46
+
47
+ def cursor(self):
48
+ return DummyCursor()
49
+
50
+ def create_savepoint(self, cursor=None):
51
+ sp_id = f"sp_{len(self.executed)}"
52
+ return sp_id
53
+
54
+ def release_savepoint(self, sp, cursor=None):
55
+ return None
56
+
57
+ def rollback_savepoint(self, sp, cursor=None):
58
+ return None
59
+
60
+ def execute(self, sql, params, cursor=None):
61
+ self.executed.append((sql, params))
62
+ if self.next_results:
63
+ return self.next_results.pop(0)
64
+ return DummyResult(0)
65
+
66
+ def table(self, table_name):
67
+ return MockTable()
68
+
69
+ def primary_keys(self):
70
+ return ["id"]
71
+
24
72
  class MockColumn:
25
73
  def __init__(self):
26
74
  self.py_type = str
@@ -218,6 +266,147 @@ class TestSQLModule(unittest.TestCase):
218
266
  self.assertIn("SET", sql_query)
219
267
  self.assertEqual(params, ("value1", 1))
220
268
 
269
+ def test_sql_insnx_with_explicit_where(self):
270
+ mock_tx = MockTx()
271
+ sql_query, params = SQL.insnx(
272
+ mock_tx,
273
+ table="my_table",
274
+ data={"id": 1, "column1": "value1"},
275
+ where={"column1": "value1"},
276
+ )
277
+ self.assertIn("INSERT INTO", sql_query)
278
+ self.assertIn("WHERE NOT EXISTS", sql_query)
279
+ self.assertIn("SELECT 1 FROM my_table", sql_query)
280
+ self.assertEqual(params, (1, "value1", "value1"))
281
+
282
+ def test_sql_insert_if_not_exists_alias(self):
283
+ mock_tx = MockTx()
284
+ sql_alias, params_alias = SQL.insert_if_not_exists(
285
+ mock_tx,
286
+ table="my_table",
287
+ data={"id": 1, "column1": "value1"},
288
+ where={"column1": "value1"},
289
+ )
290
+ sql_main, params_main = SQL.insnx(
291
+ mock_tx,
292
+ table="my_table",
293
+ data={"id": 1, "column1": "value1"},
294
+ where={"column1": "value1"},
295
+ )
296
+ self.assertEqual(sql_alias, sql_main)
297
+ self.assertEqual(params_alias, params_main)
298
+
299
+ def test_table_update_or_insert_updates_only(self):
300
+ tx = DummyTx()
301
+ table = Table(tx, "my_table")
302
+ table.cursor = mock.MagicMock(return_value=None)
303
+ table.update = mock.MagicMock(return_value=1)
304
+ ins_builder = mock.MagicMock()
305
+ table.sql = SimpleNamespace(insnx=ins_builder, insert_if_not_exists=ins_builder)
306
+
307
+ affected = table.update_or_insert(
308
+ update_data={"value": "new"},
309
+ insert_data={"id": 1, "value": "new"},
310
+ where={"id": 1},
311
+ )
312
+
313
+ self.assertEqual(affected, 1)
314
+ table.update.assert_called_once()
315
+ ins_builder.assert_not_called()
316
+
317
+ def test_table_update_or_insert_falls_back_to_insert(self):
318
+ tx = DummyTx()
319
+ table = Table(tx, "my_table")
320
+ table.cursor = mock.MagicMock(return_value=None)
321
+ table.update = mock.MagicMock(return_value=0)
322
+
323
+ captured = {}
324
+
325
+ def fake_insnx(tx_ctx, table_name, data, where):
326
+ captured["tx"] = tx_ctx
327
+ captured["table"] = table_name
328
+ captured["data"] = dict(data)
329
+ captured["where"] = where
330
+ return ("INSERT", ("a", "b"))
331
+
332
+ ins_builder = mock.MagicMock(side_effect=fake_insnx)
333
+ table.sql = SimpleNamespace(insnx=ins_builder, insert_if_not_exists=ins_builder)
334
+ tx.next_results.append(DummyResult(1))
335
+
336
+ affected = table.update_or_insert(
337
+ update_data={"value": "new"},
338
+ where={"id": 1},
339
+ pk={"id": 1},
340
+ )
341
+
342
+ self.assertEqual(affected, 1)
343
+ table.update.assert_called_once()
344
+ ins_builder.assert_called_once()
345
+ self.assertEqual(captured["table"], "my_table")
346
+ self.assertEqual(captured["data"], {"value": "new", "id": 1})
347
+ self.assertEqual(captured["where"], {"id": 1})
348
+
349
+ def test_table_update_or_insert_sql_only(self):
350
+ tx = DummyTx()
351
+ table = Table(tx, "my_table")
352
+ table.cursor = mock.MagicMock(return_value=None)
353
+ table.update = mock.MagicMock(return_value=("UPDATE sql", ("u",)))
354
+
355
+ ins_builder = mock.MagicMock(return_value=("INSERT sql", ("i",)))
356
+ table.sql = SimpleNamespace(insnx=ins_builder, insert_if_not_exists=ins_builder)
357
+
358
+ result = table.update_or_insert(
359
+ update_data={"value": "new"},
360
+ where={"id": 1},
361
+ pk={"id": 1},
362
+ sql_only=True,
363
+ )
364
+
365
+ self.assertEqual(result["update"], ("UPDATE sql", ("u",)))
366
+ self.assertEqual(result["insert"], ("INSERT sql", ("i",)))
367
+ table.update.assert_called_once_with({"value": "new"}, where={"id": 1}, pk={"id": 1}, sql_only=True)
368
+ ins_builder.assert_called_once()
369
+
370
+ def test_sql_merge_conflict_columns_are_quoted(self):
371
+ mock_tx = MockTx()
372
+ sql_query, _ = SQL.merge(
373
+ mock_tx,
374
+ table="my_table",
375
+ data={"payload": "value"},
376
+ pk={"select": 1},
377
+ on_conflict_do_nothing=False,
378
+ on_conflict_update=True,
379
+ )
380
+ self.assertIn('on conflict ("select")'.upper(), sql_query.upper())
381
+
382
+ def test_sql_merge_missing_auto_pk_values(self):
383
+ mock_tx = MockTx()
384
+ with self.assertRaisesRegex(
385
+ ValueError, "Primary key values missing from data for merge"
386
+ ):
387
+ SQL.merge(
388
+ mock_tx,
389
+ table="my_table",
390
+ data={"column1": "value1"},
391
+ pk=None,
392
+ on_conflict_do_nothing=False,
393
+ on_conflict_update=True,
394
+ )
395
+
396
+ def test_sql_merge_auto_pk_without_update_columns_falls_back_to_do_nothing(self):
397
+ mock_tx = MockTx()
398
+ sql_query, params = SQL.merge(
399
+ mock_tx,
400
+ table="my_table",
401
+ data={"id": 1},
402
+ pk=None,
403
+ on_conflict_do_nothing=False,
404
+ on_conflict_update=True,
405
+ )
406
+ self.assertIn("DO NOTHING", sql_query)
407
+ self.assertNotIn(" DO UPDATE", sql_query)
408
+ self.assertEqual(params, (1,))
409
+
221
410
  def test_get_type_mapping(self):
222
411
  self.assertEqual(TYPES.get_type("string"), "TEXT")
223
412
  self.assertEqual(TYPES.get_type(123), "BIGINT")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.142
3
+ Version: 0.0.153
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -497,7 +497,7 @@ def update_user(tx):
497
497
  # Find and update using dictionary syntax
498
498
  user = users.find(123) # Returns a row that behaves like a dict
499
499
  user['name'] = 'Updated Name' # Direct assignment like a dict
500
- user['updated_at'] = datetime.now() # No special methods needed
500
+ user['important_date'] = datetime.now() # No special methods needed
501
501
 
502
502
  # Check if columns exist before updating
503
503
  if 'phone' in user:
@@ -1,7 +1,7 @@
1
- velocity/__init__.py,sha256=SkDlRMRyhFqq5GpLQPd_olQYdhQHS2BT3Y4ioTVxupg,147
1
+ velocity/__init__.py,sha256=9s-bNUFexw6PJX1-0uXQBwF6tuficIqPpR43ZzohYyo,147
2
2
  velocity/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  velocity/app/invoices.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- velocity/app/orders.py,sha256=fr1oTBjSFfyeMBUXRG06LV4jgwrlwYNL5mbEBleFwf0,6328
4
+ velocity/app/orders.py,sha256=C7ewngMpO8nD3ul_82o4FhZBdRkWvJtnuEbEJUKDCno,6151
5
5
  velocity/app/payments.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  velocity/app/purchase_orders.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  velocity/app/tests/__init__.py,sha256=mqbNes8CjWTYLgCEgmu3EZudF6HZj671friAsAa4m_E,19
@@ -19,6 +19,7 @@ velocity/aws/handlers/response.py,sha256=s2Kw7yv5zAir1mEmfv6yBVIvRcRQ__xyryf1Srv
19
19
  velocity/aws/handlers/sqs_handler.py,sha256=azuV8DrFOh0hM13EnPzyYVBS-3fLe2fn9OPc4ho7sGc,3375
20
20
  velocity/aws/handlers/mixins/__init__.py,sha256=_zyEpsnKikF7D7X-F0GA4cyIrQ6wBq7k5j6Vhp17vaQ,623
21
21
  velocity/aws/handlers/mixins/activity_tracker.py,sha256=vyQ_8kpSprjzLoALDv7g2rVkfstn89Tbsg6Zb9GmVOk,6579
22
+ velocity/aws/handlers/mixins/aws_session_mixin.py,sha256=yTa2-n4zgv23wbW3uZUp-L4CUJy8vSL8IMMNjMlYFVg,6806
22
23
  velocity/aws/handlers/mixins/error_handler.py,sha256=uN2YF9v-3LzS3o_HdVpO-XMcPy3sS7SHjUg_LfbsG7Q,6803
23
24
  velocity/aws/handlers/mixins/legacy_mixin.py,sha256=_YhiPU-zzXQjGNSAKhoUwfTFlnczmU-3BkwNFqr0hYE,2117
24
25
  velocity/aws/handlers/mixins/standard_mixin.py,sha256=-wBX0PFlZAnxQsaMDEWr-xmU8TcRbQ4BZD3wmAKR2d0,2489
@@ -34,10 +35,10 @@ velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,
34
35
  velocity/db/core/decorators.py,sha256=quhjMoEmK_l2jF7jXyL5Fgv8uisIpBz34Au5d3U6UHs,5276
35
36
  velocity/db/core/engine.py,sha256=mNlaFPruHO935phKPVrsxZprGYUvxW-zp2sBcBZ-KCg,20666
36
37
  velocity/db/core/result.py,sha256=b0ie3yZAOj9S57x32uFFGKZ95zhImmZ0iXl0X1qYszc,12813
37
- velocity/db/core/row.py,sha256=yqxm03uEDy3oSbnkCtKyiqFdSqG3zXTq2HIHYKOvPY4,7291
38
+ velocity/db/core/row.py,sha256=GOWm-HEBPCBwdqMHMBRc41m0Hoht4vRVQLkvdogX1fU,7729
38
39
  velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
39
- velocity/db/core/table.py,sha256=zzyVZA8azaxB0QPxNjGFUOEwhk62emAZvHhkFlnACyY,35450
40
- velocity/db/core/transaction.py,sha256=unjmVkkfb7D8Wow6V8V8aLaxUZo316i--ksZxc4-I1Q,6613
40
+ velocity/db/core/table.py,sha256=_OXKXG7abhVUBja-LVlhHcINmMbKZ-VRN_I_FDfbZ9M,42164
41
+ velocity/db/core/transaction.py,sha256=VrEp37b2d_rRDHKYYm-0D0BiVtYZVltM3zooer25Klg,6918
41
42
  velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
43
  velocity/db/servers/tablehelper.py,sha256=Q48ObN5KD_U2sBP0GUcjaQjKeE4Hr351sPQirwQ0_1s,22163
43
44
  velocity/db/servers/base/__init__.py,sha256=5--XJUeEAm7O6Ns2C_ODCr5TjFhdAge-zApZCT0LGTQ,285
@@ -48,28 +49,28 @@ velocity/db/servers/base/types.py,sha256=3LBxFCD35eeIsIqftpAJh0JjUVonDYemz2n6AMt
48
49
  velocity/db/servers/mysql/__init__.py,sha256=mASO5JB0xkzYngwx2X79yyKifYRqxIdfKFWutIHuw7k,2661
49
50
  velocity/db/servers/mysql/operators.py,sha256=wHmVSPxlPGbOdvQEmsfKhD25H8djovSbNcmacLHDVkI,1273
50
51
  velocity/db/servers/mysql/reserved.py,sha256=s-aFMwYJpZ_1FBcCMU8fOdhml2ET58-59ZnUm7iw5OU,3312
51
- velocity/db/servers/mysql/sql.py,sha256=_Ea84IJwILV4Mf70zM3r3PdFL2VHU2jWSCujFhiJlQM,22170
52
+ velocity/db/servers/mysql/sql.py,sha256=CxskGe86I-8idLNZmcG7IPqlc-BQM9cpJU6WS5KdCA0,22210
52
53
  velocity/db/servers/mysql/types.py,sha256=BMQf4TpsRo1JN-yOl1nSItTO-Juu2piSTNy5o_djBeM,3486
53
54
  velocity/db/servers/postgres/__init__.py,sha256=6YcTLXposmsrEaJgdUAM_QgD1TZDSILQrGcwWZ-dibk,2457
54
55
  velocity/db/servers/postgres/operators.py,sha256=y9k6enReeR5hJxU_lYYR2epoaw4qCxEqmYJJ5jjaVWA,1166
55
56
  velocity/db/servers/postgres/reserved.py,sha256=5tKLaqFV-HrWRj-nsrxl5KGbmeM3ukn_bPZK36XEu8M,3648
56
- velocity/db/servers/postgres/sql.py,sha256=iZxvALFr3dpJm9BX_WE97E9RdM2RDCmi4M35HwXknVA,50141
57
+ velocity/db/servers/postgres/sql.py,sha256=oF0Bll75sTOrRQhBNo3dklRpUFhLixil4i09eVC9Y8Y,54450
57
58
  velocity/db/servers/postgres/types.py,sha256=W71x8iRx-IIJkQSjb29k-KGkqp-QS6SxB0BHYXd4k8w,6955
58
59
  velocity/db/servers/sqlite/__init__.py,sha256=EIx09YN1-Vm-4CXVcEf9DBgvd8FhIN9rEqIaSRrEcIk,2293
59
60
  velocity/db/servers/sqlite/operators.py,sha256=VzZgph8RrnHkIVqqWGqnJwcafgBzc_8ZQp-M8tMl-mw,1221
60
61
  velocity/db/servers/sqlite/reserved.py,sha256=4vOI06bjt8wg9KxdzDTF-iOd-ewY23NvSzthpdty2fA,1298
61
- velocity/db/servers/sqlite/sql.py,sha256=GD5m4oxvWTisfn-umnKtM0Qz8VUXTUnAqQWi9Uv2trQ,20908
62
+ velocity/db/servers/sqlite/sql.py,sha256=iAENHbN8mfVsQHoqnEppynVMP_PdqXJX8jZQDNzr0ro,20948
62
63
  velocity/db/servers/sqlite/types.py,sha256=jpCJeV25x4Iytf6D6GXgK3hVYFAAFV4WKJC-d-m4kdU,3102
63
64
  velocity/db/servers/sqlserver/__init__.py,sha256=LN8OycN7W8da_ZPRYnPQ-O3Bv_xjret9qV1ZCitZlOU,2708
64
65
  velocity/db/servers/sqlserver/operators.py,sha256=xK8_doDLssS38SRs1NoAI7XTO0-gNGMDS76nTVru4kE,1104
65
66
  velocity/db/servers/sqlserver/reserved.py,sha256=Gn5n9DjxcjM-7PsIZPYigD6XLvMAYGnz1IrPuN7Dp2Y,2120
66
- velocity/db/servers/sqlserver/sql.py,sha256=sdmFFzt_Np9mVRGPkFsnWfYPD8z88V9Sp5no3-maPHM,26208
67
+ velocity/db/servers/sqlserver/sql.py,sha256=h4fnVuNWaQE2c2sEEkLSIlBGf3xZP-lDtwILhF2-g3c,26368
67
68
  velocity/db/servers/sqlserver/types.py,sha256=FAODYEO137m-WugpM89f9bQN9q6S2cjjUaz0a9gfE6M,3745
68
69
  velocity/db/tests/__init__.py,sha256=7-hilWb43cKnSnCeXcjFG-6LpziN5k443IpsIvuevP0,24
69
70
  velocity/db/tests/common_db_test.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
71
  velocity/db/tests/test_cursor_rowcount_fix.py,sha256=mZRL1SBb9Knh67CSFyvfwj_LAarE_ilfVwpQHW18Yy8,5507
71
72
  velocity/db/tests/test_db_utils.py,sha256=mSbEQXYKpWidX1FEnjrmt3q3K4ra0YTtQclrS46ufEE,8426
72
- velocity/db/tests/test_postgres.py,sha256=xp2gqsefCsvBZaG3ADT0lCPI-I2FbZeZ7GvXr77XvWc,9315
73
+ velocity/db/tests/test_postgres.py,sha256=NoBydNkGmXn8olXwva4C4sYV3cKzERd6Df0wHixxoyE,15554
73
74
  velocity/db/tests/test_postgres_unchanged.py,sha256=rNcy7S_HXazi_MjU8QjRZO4q8dULMeG4tg6eN-rPPz8,2998
74
75
  velocity/db/tests/test_process_error_robustness.py,sha256=CZr_co_o6PK7dejOr_gwdn0iKTzjWPTY5k-PwJ6oh9s,11361
75
76
  velocity/db/tests/test_result_caching.py,sha256=DgsGXWL4G79MZOslCjq_t8qtdhCcXkHjQqV5zsF6i6M,8960
@@ -121,8 +122,8 @@ velocity/misc/tests/test_merge.py,sha256=Vm5_jY5cVczw0hZF-3TYzmxFw81heJOJB-dvhCg
121
122
  velocity/misc/tests/test_oconv.py,sha256=fy4DwWGn_v486r2d_3ACpuBD-K1oOngNq1HJCGH7X-M,4694
122
123
  velocity/misc/tests/test_original_error.py,sha256=iWSd18tckOA54LoPQOGV5j9LAz2W-3_ZOwmyZ8-4YQc,1742
123
124
  velocity/misc/tests/test_timer.py,sha256=l9nrF84kHaFofvQYKInJmfoqC01wBhsUB18lVBgXCoo,2758
124
- velocity_python-0.0.142.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
125
- velocity_python-0.0.142.dist-info/METADATA,sha256=lOnWDl2eeeGGfq1hv56p2mGXGz-LzGodk7wWdVpRoH0,34262
126
- velocity_python-0.0.142.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
127
- velocity_python-0.0.142.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
128
- velocity_python-0.0.142.dist-info/RECORD,,
125
+ velocity_python-0.0.153.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
126
+ velocity_python-0.0.153.dist-info/METADATA,sha256=WkqWt5YdgAqOWMGp0ZErqq44s1vA9PDoh-D01hCTOhY,34266
127
+ velocity_python-0.0.153.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
128
+ velocity_python-0.0.153.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
129
+ velocity_python-0.0.153.dist-info/RECORD,,